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] = {