diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index 4ebe5a0d..60367050 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -65,9 +65,9 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) const tableProps = { ...dataProps, + websiteDomain: data?.domain, limit: 10, onExpand: handleExpand, - websiteDomain: data?.domain, }; const DetailsComponent = expand?.component; diff --git a/components/forms/WebsiteEditForm.js b/components/forms/WebsiteEditForm.js index 4d7c452a..bc972856 100644 --- a/components/forms/WebsiteEditForm.js +++ b/components/forms/WebsiteEditForm.js @@ -8,7 +8,8 @@ import FormLayout, { FormMessage, FormRow, } from 'components/layout/FormLayout'; -import Checkbox from '../common/Checkbox'; +import Checkbox from 'components/common/Checkbox'; +import { DOMAIN_REGEX } from 'lib/constants'; const initialValues = { name: '', @@ -24,6 +25,8 @@ const validate = ({ name, domain }) => { } if (!domain) { errors.domain = 'Required'; + } else if (!DOMAIN_REGEX.test(domain)) { + errors.domain = 'Invalid domain'; } return errors; diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 566ceed3..fa51d1db 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -14,6 +14,7 @@ export default function MetricsTable({ title, metric, websiteId, + websiteDomain, startDate, endDate, type, @@ -47,6 +48,7 @@ export default function MetricsTable({ type, start_at: +startDate, end_at: +endDate, + domain: websiteDomain, }); setData(data); diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js index abaa1208..e49c9dc0 100644 --- a/components/metrics/ReferrersTable.js +++ b/components/metrics/ReferrersTable.js @@ -30,6 +30,7 @@ export default function Referrers({ metric="Views" headerComponent={limit ? null : } websiteId={websiteId} + websiteDomain={websiteDomain} startDate={startDate} endDate={endDate} limit={limit} diff --git a/lib/constants.js b/lib/constants.js index f1c151e7..1c63ae18 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,5 +1,7 @@ export const AUTH_COOKIE_NAME = 'umami.auth'; +export const DOMAIN_REGEX = /((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}/; + export const DESKTOP_SCREEN_WIDTH = 1920; export const LAPTOP_SCREEN_WIDTH = 1024; export const MOBILE_SCREEN_WIDTH = 479; diff --git a/lib/date.js b/lib/date.js index 80980e27..6e10f610 100644 --- a/lib/date.js +++ b/lib/date.js @@ -98,12 +98,6 @@ export function getDateArray(data, startDate, endDate, unit) { function findData(t) { const x = data.find(e => { - console.log( - new Date(e.t), - getLocalTime(new Date(e.t)), - getLocalTime(new Date(e.t)).getTime(), - normalize(new Date(t)).getTime(), - ); return getLocalTime(new Date(e.t)).getTime() === normalize(new Date(t)).getTime(); }); diff --git a/lib/filters.js b/lib/filters.js index 09f27e70..20d712df 100644 --- a/lib/filters.js +++ b/lib/filters.js @@ -1,8 +1,8 @@ import firstBy from 'thenby'; import { BROWSERS, ISO_COUNTRIES, DEVICES } from './constants'; -import { removeTrailingSlash } from './format'; +import { removeTrailingSlash, getDomainName } from './url'; -export const urlFilter = (data, { domain, raw }) => { +export const urlFilter = (data, { raw }) => { const isValidUrl = url => { return url !== '' && !url.startsWith('#'); }; @@ -30,7 +30,7 @@ export const urlFilter = (data, { domain, raw }) => { return obj; } - const url = cleanUrl(x.startsWith('/') ? `http://${domain}${x}` : x); + const url = cleanUrl(`http://x${x}`); if (url) { if (!obj[url]) { @@ -49,7 +49,8 @@ export const urlFilter = (data, { domain, raw }) => { }; export const refFilter = (data, { domain, domainOnly, raw }) => { - const regex = new RegExp(domain.startsWith('http') ? domain : `http[s]?://${domain}`); + const domainName = getDomainName(domain); + const regex = new RegExp(`http[s]?://${domainName}`); const isValidRef = ref => { return ref !== '' && !ref.startsWith('/') && !ref.startsWith('#'); @@ -63,7 +64,7 @@ export const refFilter = (data, { domain, domainOnly, raw }) => { try { const { hostname, origin, pathname, searchParams, protocol } = new URL(url); - if (hostname === domain || regex.test(url)) { + if (hostname === domainName) { return null; } diff --git a/lib/format.js b/lib/format.js index e3bf1e8e..b031509b 100644 --- a/lib/format.js +++ b/lib/format.js @@ -62,7 +62,3 @@ export function formatLongNumber(value) { return formatNumber(n); } - -export function removeTrailingSlash(url) { - return url.length > 1 && url.endsWith('/') ? url.slice(0, -1) : url; -} diff --git a/lib/queries.js b/lib/queries.js index 7c8a9875..0b8dde89 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -347,9 +347,11 @@ export function getPageviews( return Promise.resolve([]); } -export function getRankings(website_id, start_at, end_at, type, table) { +export function getRankings(website_id, start_at, end_at, type, table, domain) { const db = getDatabase(); + const filter = domain ? `and ${type} not like '%${domain}%'` : ''; + if (db === POSTGRESQL) { return prisma.$queryRaw( ` @@ -357,6 +359,7 @@ export function getRankings(website_id, start_at, end_at, type, table) { from ${table} where website_id=$1 and created_at between $2 and $3 + ${filter} group by 1 order by 2 desc `, @@ -373,6 +376,7 @@ export function getRankings(website_id, start_at, end_at, type, table) { from ${table} where website_id=? and created_at between ? and ? + ${filter} group by 1 order by 2 desc `, diff --git a/lib/url.js b/lib/url.js new file mode 100644 index 00000000..e29243fb --- /dev/null +++ b/lib/url.js @@ -0,0 +1,11 @@ +export function removeTrailingSlash(url) { + return url.length > 1 && url.endsWith('/') ? url.slice(0, -1) : url; +} + +export function getDomainName(str) { + try { + return new URL(str).hostname; + } catch { + return str; + } +} diff --git a/package.json b/package.json index b9c7a59e..1e3add00 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.17.0", + "version": "0.18.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", @@ -25,11 +25,13 @@ }, "lint-staged": { "**/*.js": [ - "prettier --write" + "prettier --write", + "eslint" ], "**/*.css": [ "stylelint --fix", - "prettier --write" + "prettier --write", + "eslint" ] }, "husky": { diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js index 8d1d95bf..4e613d0d 100644 --- a/pages/api/website/[id]/rankings.js +++ b/pages/api/website/[id]/rankings.js @@ -1,5 +1,6 @@ import { getRankings } from 'lib/queries'; import { ok, badRequest } from 'lib/response'; +import { DOMAIN_REGEX } from '../../../../lib/constants'; const sessionColumns = ['browser', 'os', 'device', 'country']; const pageviewColumns = ['url', 'referrer']; @@ -24,12 +25,18 @@ function getColumn(type) { } export default async (req, res) => { - const { id, type, start_at, end_at } = req.query; + const { id, type, start_at, end_at, domain } = req.query; const websiteId = +id; const startDate = new Date(+start_at); const endDate = new Date(+end_at); - if (type !== 'event' && !sessionColumns.includes(type) && !pageviewColumns.includes(type)) { + if ( + type !== 'event' && + !sessionColumns.includes(type) && + !pageviewColumns.includes(type) && + domain && + DOMAIN_REGEX.test(domain) + ) { return badRequest(res); } @@ -39,6 +46,7 @@ export default async (req, res) => { endDate, getColumn(type), getTable(type), + domain, ); return ok(res, rankings);