diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
new file mode 100644
index 00000000..cb8f3e59
--- /dev/null
+++ b/.github/workflows/cd.yml
@@ -0,0 +1,30 @@
+name: Create docker images
+
+on: [create]
+
+jobs:
+
+ build:
+ name: Build, push, and deploy
+ if: ${{ startsWith(github.ref, 'refs/tags/v') }}
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ db-type: [postgresql, mysql]
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Set env
+ run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
+
+ - uses: mr-smithers-excellent/docker-build-push@v5
+ name: Build & push Docker image for ${{ matrix.db-type }}
+ with:
+ image: umami
+ tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
+ buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
+ registry: ghcr.io/${{ github.actor }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..d204d88c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,41 @@
+# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
+
+name: Node.js CI
+
+on: [push]
+
+env:
+ DATABASE_TYPE: postgresql
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ include:
+ - node-version: 12.x
+ db-type: postgresql
+ - node-version: 12.x
+ db-type: mysql
+ - node-version: 14.x
+ db-type: postgresql
+ - node-version: 14.x
+ db-type: mysql
+ - node-version: 16.x
+ db-type: postgresql
+ - node-version: 16.x
+ db-type: mysql
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v2
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: 'npm'
+ env:
+ DATABASE_TYPE: ${{ matrix.db-type }}
+ - run: npm install
+ - run: npm run build --if-present
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
deleted file mode 100644
index ec7dfa4c..00000000
--- a/.github/workflows/main.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-on:
- push:
- branches:
- - master
-
-jobs:
-
- build:
- name: Build, push, and deploy
- runs-on: ubuntu-latest
- steps:
-
- - name: Checkout master
- uses: actions/checkout@v2
-
- - name: Build PostgreSQL container image
- run: |
- docker build --build-arg DATABASE_TYPE=postgresql \
- --tag ghcr.io/$GITHUB_ACTOR/umami:postgresql-$(echo $GITHUB_SHA | head -c7) \
- --tag ghcr.io/$GITHUB_ACTOR/umami:postgresql-latest \
- .
-
- - name: Build MySQL container image
- run: |
- docker build --build-arg DATABASE_TYPE=mysql \
- --tag ghcr.io/$GITHUB_ACTOR/umami:mysql-$(echo $GITHUB_SHA | head -c7) \
- --tag ghcr.io/$GITHUB_ACTOR/umami:mysql-latest \
- .
-
- - name: Docker login
- run: >-
- echo "${{ secrets.GITHUB_TOKEN }}"
- | docker login -u "${{ github.actor }}" --password-stdin ghcr.io
-
- - name: Push image to GitHub
- run: |
- # Push each image individually, avoiding pushing to umami:latest
- # as MySQL or PostgreSQL are required
- docker push ghcr.io/$GITHUB_ACTOR/umami:postgresql-$(echo $GITHUB_SHA | head -c7)
- docker push ghcr.io/$GITHUB_ACTOR/umami:postgresql-latest
- docker push ghcr.io/$GITHUB_ACTOR/umami:mysql-$(echo $GITHUB_SHA | head -c7)
- docker push ghcr.io/$GITHUB_ACTOR/umami:mysql-latest
diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js
index d8065fe3..e1fa6891 100644
--- a/components/metrics/MetricsTable.js
+++ b/components/metrics/MetricsTable.js
@@ -30,7 +30,7 @@ export default function MetricsTable({
const {
resolve,
router,
- query: { url },
+ query: { url, referrer },
} = usePageQuery();
const { data, loading, error } = useFetch(
@@ -41,12 +41,13 @@ export default function MetricsTable({
start_at: +startDate,
end_at: +endDate,
url,
+ referrer,
},
onDataLoad,
delay: DEFAULT_ANIMATION_DURATION,
headers: { [TOKEN_HEADER]: shareToken?.token },
},
- [modified],
+ [modified, url, referrer],
);
const filteredData = useMemo(() => {
diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js
index c7079e8d..93cdb956 100644
--- a/components/metrics/ReferrersTable.js
+++ b/components/metrics/ReferrersTable.js
@@ -19,7 +19,7 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters,
const [filter, setFilter] = useState(FILTER_COMBINED);
const {
resolve,
- query: { ref: currentRef },
+ query: { referrer: currentRef },
} = usePageQuery();
const buttons = [
@@ -37,7 +37,7 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters,
const renderLink = ({ w: link, x: label }) => {
return (
-
+
{
@@ -88,7 +88,7 @@ export default function WebsiteChart({
stickyClassName={styles.sticky}
enabled={stickyHeader}
>
-
+
diff --git a/lang/fr-FR.json b/lang/fr-FR.json
index b7cee440..6698521d 100644
--- a/lang/fr-FR.json
+++ b/lang/fr-FR.json
@@ -28,7 +28,7 @@
"label.enable-share-url": "Activer le partage d'URL",
"label.invalid": "Invalide",
"label.invalid-domain": "Domaine invalide",
- "label.language": "Language",
+ "label.language": "Langage",
"label.last-days": "{x} derniers jours",
"label.last-hours": "{x} dernières heures",
"label.logged-in-as": "Connecté en tant que {username}",
@@ -51,7 +51,7 @@
"label.settings": "Paramètres",
"label.share-url": "Partager l'URL",
"label.single-day": "Journée",
- "label.theme": "Theme",
+ "label.theme": "Thème",
"label.this-month": "Ce mois ci",
"label.this-week": "Cette semaine",
"label.this-year": "Cette année",
diff --git a/lang/zh-CN.json b/lang/zh-CN.json
index 2efe028c..506c99b8 100644
--- a/lang/zh-CN.json
+++ b/lang/zh-CN.json
@@ -4,8 +4,8 @@
"label.add-website": "添加网站",
"label.administrator": "管理员",
"label.all": "所有",
- "label.all-events": "All events",
- "label.all-time": "All time",
+ "label.all-events": "所有事件",
+ "label.all-time": "所有时间段",
"label.all-websites": "全部网站",
"label.back": "返回",
"label.cancel": "取消",
@@ -28,7 +28,7 @@
"label.enable-share-url": "启用共享链接",
"label.invalid": "输入无效",
"label.invalid-domain": "无效域名",
- "label.language": "Language",
+ "label.language": "语言",
"label.last-days": "最近 {x} 天",
"label.last-hours": "最近 {x} 小时",
"label.logged-in-as": "登录名: {username}",
@@ -37,7 +37,7 @@
"label.more": "更多",
"label.name": "名字",
"label.new-password": "新密码",
- "label.owner": "Owner",
+ "label.owner": "所有者",
"label.password": "密码",
"label.passwords-dont-match": "密码不一致",
"label.profile": "个人资料",
@@ -46,12 +46,12 @@
"label.refresh": "刷新",
"label.required": "必填",
"label.reset": "重置",
- "label.reset-website": "Reset statistics",
+ "label.reset-website": "重置统计数据",
"label.save": "保存",
"label.settings": "设置",
"label.share-url": "共享链接",
"label.single-day": "单日",
- "label.theme": "Theme",
+ "label.theme": "主题",
"label.this-month": "本月",
"label.this-week": "本周",
"label.this-year": "今年",
@@ -64,7 +64,7 @@
"label.websites": "网站",
"message.active-users": "当前在线 {x} 人",
"message.confirm-delete": "你确定要删除 {target} 吗?",
- "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
+ "message.confirm-reset": "您确定要重置 {target} 的数据吗?",
"message.copied": "复制成功!",
"message.delete-warning": "所有相关数据将会被删除。",
"message.failure": "出现错误。",
@@ -78,10 +78,10 @@
"message.no-websites-configured": "你还没有设置任何网站。",
"message.page-not-found": "网页未找到。",
"message.powered-by": "由 {name} 提供支持",
- "message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
+ "message.reset-warning": "本网站的所有统计数据将被删除,但您的跟踪代码将保持不变。",
"message.save-success": "保存成功。",
"message.share-url": "这是 {target} 的共享链接。",
- "message.toggle-charts": "Toggle charts",
+ "message.toggle-charts": "切换图表",
"message.track-stats": "把以下代码放到你的网站的 {head} 部分来收集 {target} 的数据。",
"message.type-delete": "在下方输入框输入 {delete} 以确认删除。",
"message.type-reset": "在下方输入框输入 {reset} 以确认删除。",
@@ -99,7 +99,7 @@
"metrics.filter.combined": "总和",
"metrics.filter.domain-only": "只看域名",
"metrics.filter.raw": "原始",
- "metrics.languages": "Languages",
+ "metrics.languages": "语言",
"metrics.operating-systems": "操作系统",
"metrics.page-views": "页面浏览量",
"metrics.pages": "网页",
diff --git a/lib/filters.js b/lib/filters.js
index e9723b42..fb6b435a 100644
--- a/lib/filters.js
+++ b/lib/filters.js
@@ -1,5 +1,5 @@
import { BROWSERS } from './constants';
-import { removeTrailingSlash, removeWWW } from './url';
+import { removeTrailingSlash, removeWWW, getDomainName } from './url';
export const urlFilter = (data, { raw }) => {
const isValidUrl = url => {
@@ -46,11 +46,14 @@ export const urlFilter = (data, { raw }) => {
};
export const refFilter = (data, { domain, domainOnly, raw }) => {
- const regex = new RegExp(`http[s]?://([a-z0-9-]+\\.)*${domain}`);
+ const domainName = getDomainName(domain);
+ const regex = new RegExp(`http[s]?://([a-z0-9-]+\\.)*${domainName}`);
const links = {};
- const isValidRef = ref => {
- return ref !== '' && ref !== null && !ref.startsWith('/') && !ref.startsWith('#');
+ const isValidRef = referrer => {
+ return (
+ referrer !== '' && referrer !== null && !referrer.startsWith('/') && !referrer.startsWith('#')
+ );
};
const cleanUrl = url => {
@@ -71,8 +74,8 @@ export const refFilter = (data, { domain, domainOnly, raw }) => {
if (protocol.startsWith('http')) {
const path = removeTrailingSlash(pathname);
- const ref = searchParams.get('ref');
- const query = ref ? `?ref=${ref}` : '';
+ const referrer = searchParams.get('referrer');
+ const query = referrer ? `?referrer=${referrer}` : '';
return removeTrailingSlash(`${removeWWW(hostname)}${path}`) + query;
}
diff --git a/lib/queries.js b/lib/queries.js
index 5bcd18ba..ed644874 100644
--- a/lib/queries.js
+++ b/lib/queries.js
@@ -335,7 +335,7 @@ export async function getEvents(websites, start_at) {
export function getWebsiteStats(website_id, start_at, end_at, filters = {}) {
const params = [website_id, start_at, end_at];
- const { url, ref } = filters;
+ const { url, referrer } = filters;
let urlFilter = '';
let refFilter = '';
@@ -344,9 +344,9 @@ export function getWebsiteStats(website_id, start_at, end_at, filters = {}) {
params.push(decodeURIComponent(url));
}
- if (ref) {
+ if (referrer) {
refFilter = `and referrer like $${params.length + 1}`;
- params.push(`%${decodeURIComponent(ref)}%`);
+ params.push(`%${decodeURIComponent(referrer)}%`);
}
return rawQuery(
@@ -382,7 +382,7 @@ export function getPageviewStats(
filters = {},
) {
const params = [website_id, start_at, end_at];
- const { url, ref } = filters;
+ const { url, referrer } = filters;
let urlFilter = '';
let refFilter = '';
@@ -392,9 +392,9 @@ export function getPageviewStats(
params.push(decodeURIComponent(url));
}
- if (ref) {
+ if (referrer) {
refFilter = `and referrer like $${params.length + 1}`;
- params.push(`%${decodeURIComponent(ref)}%`);
+ params.push(`%${decodeURIComponent(referrer)}%`);
}
return rawQuery(
@@ -444,10 +444,11 @@ export function getSessionMetrics(website_id, start_at, end_at, field, filters =
export function getPageviewMetrics(website_id, start_at, end_at, field, table, filters = {}) {
const params = [website_id, start_at, end_at];
- const { domain, url } = filters;
+ const { domain, url, referrer } = filters;
let domainFilter = '';
let urlFilter = '';
+ let refFilter = '';
if (domain) {
domainFilter = `and referrer not like $${params.length + 1} and referrer not like '/%'`;
@@ -459,6 +460,11 @@ export function getPageviewMetrics(website_id, start_at, end_at, field, table, f
params.push(decodeURIComponent(url));
}
+ if (referrer) {
+ refFilter = `and referrer like $${params.length + 1}`;
+ params.push(`%${decodeURIComponent(referrer)}%`);
+ }
+
return rawQuery(
`
select ${field} x, count(*) y
@@ -467,6 +473,7 @@ export function getPageviewMetrics(website_id, start_at, end_at, field, table, f
and created_at between $2 and $3
${domainFilter}
${urlFilter}
+ ${refFilter}
group by 1
order by 2 desc
`,
diff --git a/pages/api/website/[id]/metrics.js b/pages/api/website/[id]/metrics.js
index 5e16e350..675427db 100644
--- a/pages/api/website/[id]/metrics.js
+++ b/pages/api/website/[id]/metrics.js
@@ -30,7 +30,7 @@ export default async (req, res) => {
return unauthorized(res);
}
- const { id, type, start_at, end_at, url } = req.query;
+ const { id, type, start_at, end_at, url, referrer } = req.query;
const websiteId = +id;
const startDate = new Date(+start_at);
@@ -75,6 +75,7 @@ export default async (req, res) => {
{
domain,
url: type !== 'url' && url,
+ referrer,
},
);
diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js
index 22966ca7..e2069f8c 100644
--- a/pages/api/website/[id]/pageviews.js
+++ b/pages/api/website/[id]/pageviews.js
@@ -11,7 +11,7 @@ export default async (req, res) => {
return unauthorized(res);
}
- const { id, start_at, end_at, unit, tz, url, ref } = req.query;
+ const { id, start_at, end_at, unit, tz, url, referrer } = req.query;
const websiteId = +id;
const startDate = new Date(+start_at);
@@ -22,10 +22,10 @@ export default async (req, res) => {
}
const [pageviews, sessions] = await Promise.all([
- getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', { url, ref }),
+ getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', { url, referrer }),
getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', {
url,
- ref,
+ referrer,
}),
]);
diff --git a/pages/api/website/[id]/stats.js b/pages/api/website/[id]/stats.js
index 9dd82361..cdb374e2 100644
--- a/pages/api/website/[id]/stats.js
+++ b/pages/api/website/[id]/stats.js
@@ -8,7 +8,7 @@ export default async (req, res) => {
return unauthorized(res);
}
- const { id, start_at, end_at, url, ref } = req.query;
+ const { id, start_at, end_at, url, referrer } = req.query;
const websiteId = +id;
const startDate = new Date(+start_at);
@@ -18,8 +18,11 @@ export default async (req, res) => {
const prevStartDate = new Date(+start_at - distance);
const prevEndDate = new Date(+end_at - distance);
- const metrics = await getWebsiteStats(websiteId, startDate, endDate, { url, ref });
- const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, { url, ref });
+ const metrics = await getWebsiteStats(websiteId, startDate, endDate, { url, referrer });
+ const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, {
+ url,
+ referrer,
+ });
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
obj[key] = {