From fb2dc9f5ab304c67bf59c38dc624cc9835fdad81 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 10 Apr 2022 03:51:43 -0700 Subject: [PATCH] Allow filtering on session fields. --- components/common/FilterLink.js | 34 +++ .../FilterLink.module.css} | 15 +- components/metrics/BrowsersTable.js | 6 + components/metrics/CountriesTable.js | 13 +- components/metrics/DevicesTable.js | 13 +- components/metrics/MetricsBar.js | 8 +- components/metrics/MetricsTable.js | 8 +- components/metrics/OSTable.js | 6 + components/metrics/PagesTable.js | 23 +- components/metrics/PagesTable.module.css | 8 - components/metrics/ReferrersTable.js | 32 +-- components/metrics/WebsiteChart.js | 13 +- lib/queries.js | 239 +++++++++--------- package.json | 4 +- pages/_app.js | 2 +- pages/api/collect.js | 5 +- pages/api/website/[id]/metrics.js | 25 +- pages/api/website/[id]/pageviews.js | 17 +- pages/api/website/[id]/stats.js | 15 +- 19 files changed, 275 insertions(+), 211 deletions(-) create mode 100644 components/common/FilterLink.js rename components/{metrics/ReferrersTable.module.css => common/FilterLink.module.css} (79%) delete mode 100644 components/metrics/PagesTable.module.css diff --git a/components/common/FilterLink.js b/components/common/FilterLink.js new file mode 100644 index 00000000..459a8ae1 --- /dev/null +++ b/components/common/FilterLink.js @@ -0,0 +1,34 @@ +import React from 'react'; +import Link from 'next/link'; +import classNames from 'classnames'; +import usePageQuery from 'hooks/usePageQuery'; +import { safeDecodeURI } from 'lib/url'; +import Icon from './Icon'; +import External from 'assets/arrow-up-right-from-square.svg'; +import styles from './FilterLink.module.css'; + +export default function FilterLink({ id, value, label, externalUrl }) { + const { resolve, query } = usePageQuery(); + const active = query[id] !== undefined; + const selected = query[id] === value; + + return ( +
+ + + {safeDecodeURI(label || value)} + + + {externalUrl && ( + + } className={styles.icon} /> + + )} +
+ ); +} diff --git a/components/metrics/ReferrersTable.module.css b/components/common/FilterLink.module.css similarity index 79% rename from components/metrics/ReferrersTable.module.css rename to components/common/FilterLink.module.css index 238667f3..45b049da 100644 --- a/components/metrics/ReferrersTable.module.css +++ b/components/common/FilterLink.module.css @@ -1,19 +1,20 @@ -body .inactive { +.row { + display: flex; + align-items: center; +} + +.row .inactive { color: var(--gray500); } -body .active { +.row .active { color: var(--gray900); font-weight: 600; } -.row { - display: flex; - justify-content: space-between; -} - .row .link { display: none; + margin-left: 20px; } .row .label { diff --git a/components/metrics/BrowsersTable.js b/components/metrics/BrowsersTable.js index 12c1087b..60cb57d7 100644 --- a/components/metrics/BrowsersTable.js +++ b/components/metrics/BrowsersTable.js @@ -2,8 +2,13 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import { browserFilter } from 'lib/filters'; +import FilterLink from '../common/FilterLink'; export default function BrowsersTable({ websiteId, ...props }) { + function renderLink({ x: browser }) { + return ; + } + return ( } websiteId={websiteId} dataFilter={browserFilter} + renderLabel={renderLink} /> ); } diff --git a/components/metrics/CountriesTable.js b/components/metrics/CountriesTable.js index 01e7c7c7..17d9127a 100644 --- a/components/metrics/CountriesTable.js +++ b/components/metrics/CountriesTable.js @@ -2,6 +2,7 @@ import React from 'react'; import MetricsTable from './MetricsTable'; import { percentFilter } from 'lib/filters'; import { FormattedMessage } from 'react-intl'; +import FilterLink from 'components/common/FilterLink'; import useCountryNames from 'hooks/useCountryNames'; import useLocale from 'hooks/useLocale'; @@ -9,10 +10,16 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) { const { locale } = useLocale(); const countryNames = useCountryNames(locale); - function renderLabel({ x }) { + function renderLink({ x: code }) { return (
- {countryNames[x] ?? } + + } + />
); } @@ -25,7 +32,7 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) { metric={} websiteId={websiteId} onDataLoad={data => onDataLoad?.(percentFilter(data))} - renderLabel={renderLabel} + renderLabel={renderLink} /> ); } diff --git a/components/metrics/DevicesTable.js b/components/metrics/DevicesTable.js index d09774b9..c704e08d 100644 --- a/components/metrics/DevicesTable.js +++ b/components/metrics/DevicesTable.js @@ -1,9 +1,18 @@ import React from 'react'; import MetricsTable from './MetricsTable'; -import { FormattedMessage } from 'react-intl'; +import { useIntl, FormattedMessage } from 'react-intl'; import { getDeviceMessage } from 'components/messages'; +import FilterLink from 'components/common/FilterLink'; export default function DevicesTable({ websiteId, ...props }) { + const { formatMessage } = useIntl(); + + function renderLink({ x: device }) { + return ( + + ); + } + return ( } websiteId={websiteId} - renderLabel={({ x }) => } + renderLabel={renderLink} /> ); } diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js index f28aeec7..290cde81 100644 --- a/components/metrics/MetricsBar.js +++ b/components/metrics/MetricsBar.js @@ -18,7 +18,7 @@ export default function MetricsBar({ websiteId, className }) { const { startDate, endDate, modified } = dateRange; const [format, setFormat] = useState(true); const { - query: { url, referrer }, + query: { url, referrer, os, browser, device, country }, } = usePageQuery(); const { data, error, loading } = useFetch( @@ -29,10 +29,14 @@ export default function MetricsBar({ websiteId, className }) { end_at: +endDate, url, referrer, + os, + browser, + device, + country, }, headers: { [TOKEN_HEADER]: shareToken?.token }, }, - [modified, url, referrer], + [modified, url, referrer, os, browser, device, country], ); const formatFunc = format diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index e1fa6891..48e004cb 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -30,7 +30,7 @@ export default function MetricsTable({ const { resolve, router, - query: { url, referrer }, + query: { url, referrer, os, browser, device, country }, } = usePageQuery(); const { data, loading, error } = useFetch( @@ -42,12 +42,16 @@ export default function MetricsTable({ end_at: +endDate, url, referrer, + os, + browser, + device, + country, }, onDataLoad, delay: DEFAULT_ANIMATION_DURATION, headers: { [TOKEN_HEADER]: shareToken?.token }, }, - [modified, url, referrer], + [modified, url, referrer, os, browser, device, country], ); const filteredData = useMemo(() => { diff --git a/components/metrics/OSTable.js b/components/metrics/OSTable.js index c77ae074..18bc2499 100644 --- a/components/metrics/OSTable.js +++ b/components/metrics/OSTable.js @@ -1,14 +1,20 @@ import React from 'react'; import MetricsTable from './MetricsTable'; import { FormattedMessage } from 'react-intl'; +import FilterLink from 'components/common/FilterLink'; export default function OSTable({ websiteId, ...props }) { + function renderLink({ x: os }) { + return ; + } + return ( } type="os" metric={} + renderLabel={renderLink} websiteId={websiteId} /> ); diff --git a/components/metrics/PagesTable.js b/components/metrics/PagesTable.js index 6fe8c139..98a0fd72 100644 --- a/components/metrics/PagesTable.js +++ b/components/metrics/PagesTable.js @@ -1,23 +1,15 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; -import Link from 'next/link'; +import FilterLink from 'components/common/FilterLink'; import FilterButtons from 'components/common/FilterButtons'; import { urlFilter } from 'lib/filters'; -import { safeDecodeURI } from 'lib/url'; -import usePageQuery from 'hooks/usePageQuery'; import MetricsTable from './MetricsTable'; -import styles from './PagesTable.module.css'; export const FILTER_COMBINED = 0; export const FILTER_RAW = 1; export default function PagesTable({ websiteId, websiteDomain, showFilters, ...props }) { const [filter, setFilter] = useState(FILTER_COMBINED); - const { - resolve, - query: { url: currentUrl }, - } = usePageQuery(); const buttons = [ { @@ -28,18 +20,7 @@ export default function PagesTable({ websiteId, websiteDomain, showFilters, ...p ]; const renderLink = ({ x: url }) => { - return ( - - - {safeDecodeURI(url)} - - - ); + return ; }; return ( diff --git a/components/metrics/PagesTable.module.css b/components/metrics/PagesTable.module.css deleted file mode 100644 index 3c592a74..00000000 --- a/components/metrics/PagesTable.module.css +++ /dev/null @@ -1,8 +0,0 @@ -body .inactive { - color: var(--gray500); -} - -body .active { - color: var(--gray900); - font-weight: 600; -} diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js index 93cdb956..1a9e3f2d 100644 --- a/components/metrics/ReferrersTable.js +++ b/components/metrics/ReferrersTable.js @@ -2,14 +2,8 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import FilterButtons from 'components/common/FilterButtons'; +import FilterLink from 'components/common/FilterLink'; import { refFilter } from 'lib/filters'; -import { safeDecodeURI } from 'lib/url'; -import Link from 'next/link'; -import classNames from 'classnames'; -import usePageQuery from 'hooks/usePageQuery'; -import External from 'assets/arrow-up-right-from-square.svg'; -import Icon from '../common/Icon'; -import styles from './ReferrersTable.module.css'; export const FILTER_DOMAIN_ONLY = 0; export const FILTER_COMBINED = 1; @@ -17,10 +11,6 @@ export const FILTER_RAW = 2; export default function ReferrersTable({ websiteId, websiteDomain, showFilters, ...props }) { const [filter, setFilter] = useState(FILTER_COMBINED); - const { - resolve, - query: { referrer: currentRef }, - } = usePageQuery(); const buttons = [ { @@ -34,24 +24,8 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters, { label: , value: FILTER_RAW }, ]; - const renderLink = ({ w: link, x: label }) => { - return ( - - ); + const renderLink = ({ w: link, x: referrer }) => { + return ; }; return ( diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index 817addc1..03cc7b58 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -33,7 +33,7 @@ export default function WebsiteChart({ const { router, resolve, - query: { url, referrer }, + query: { url, referrer, os, browser, device, country }, } = usePageQuery(); const { get } = useApi(); @@ -47,11 +47,15 @@ export default function WebsiteChart({ tz: timezone, url, referrer, + os, + browser, + device, + country, }, onDataLoad, headers: { [TOKEN_HEADER]: shareToken?.token }, }, - [modified, url, referrer], + [modified, url, referrer, os, browser, device, country], ); const chartData = useMemo(() => { @@ -88,7 +92,10 @@ export default function WebsiteChart({ stickyClassName={styles.sticky} enabled={stickyHeader} > - +
diff --git a/lib/queries.js b/lib/queries.js index 1b89ddea..25284428 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -21,24 +21,6 @@ export function getDatabase() { return type; } -export async function runQuery(query) { - return query.catch(e => { - throw e; - }); -} - -export async function rawQuery(query, params = []) { - const db = getDatabase(); - - if (db !== POSTGRESQL && db !== MYSQL) { - return Promise.reject(new Error('Unknown database.')); - } - - const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query; - - return prisma.$queryRawUnsafe.apply(prisma, [sql, ...params]); -} - export function getDateQuery(field, unit, timezone) { const db = getDatabase(); @@ -72,6 +54,79 @@ export function getTimestampInterval(field) { } } +export function getFilterQuery(table, filters = {}, params = []) { + const query = Object.keys(filters).reduce((arr, key) => { + const value = filters[key]; + + if (value === undefined) { + return arr; + } + + switch (key) { + case 'url': + if (table === 'session' || table === 'pageview') { + arr.push(`and ${table}.${key}=$${params.length + 1}`); + params.push(decodeURIComponent(value)); + } + break; + + case 'os': + case 'browser': + case 'device': + case 'country': + if (table === 'session') { + arr.push(`and ${table}.${key}=$${params.length + 1}`); + params.push(decodeURIComponent(value)); + } + break; + + case 'event_type': + if (table === 'event') { + arr.push(`and ${table}.${key}=$${params.length + 1}`); + params.push(decodeURIComponent(value)); + } + break; + + case 'referrer': + if (table === 'pageview') { + arr.push(`and ${table}.referrer like $${params.length + 1}`); + params.push(`%${decodeURIComponent(value)}%`); + } + break; + + case 'domain': + if (table === 'pageview') { + arr.push(`and ${table}.referrer not like $${params.length + 1}`); + arr.push(`and ${table}.referrer not like '/%'`); + params.push(`%://${value}/%`); + } + break; + } + + return arr; + }, []); + + return query.join('\n'); +} + +export async function runQuery(query) { + return query.catch(e => { + throw e; + }); +} + +export async function rawQuery(query, params = []) { + const db = getDatabase(); + + if (db !== POSTGRESQL && db !== MYSQL) { + return Promise.reject(new Error('Unknown database.')); + } + + const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query; + + return runQuery(prisma.$queryRawUnsafe.apply(prisma, [sql, ...params])); +} + export async function getWebsiteById(website_id) { return runQuery( prisma.website.findUnique({ @@ -344,19 +399,12 @@ 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, referrer } = filters; - let urlFilter = ''; - let refFilter = ''; + const { url, referrer, os, browser, device, country } = filters; - if (url) { - urlFilter = `and url=$${params.length + 1}`; - params.push(decodeURIComponent(url)); - } - - if (referrer) { - refFilter = `and referrer like $${params.length + 1}`; - params.push(`%${decodeURIComponent(referrer)}%`); - } + const joinSession = + os || browser || device || country + ? 'inner join session on session.session_id = pageview.session_id' + : ''; return rawQuery( ` @@ -365,15 +413,16 @@ export function getWebsiteStats(website_id, start_at, end_at, filters = {}) { sum(case when t.c = 1 then 1 else 0 end) as "bounces", sum(t.time) as "totaltime" from ( - select session_id, - ${getDateQuery('created_at', 'hour')}, + select pageview.session_id, + ${getDateQuery('pageview.created_at', 'hour')}, count(*) c, - ${getTimestampInterval('created_at')} as "time" + ${getTimestampInterval('pageview.created_at')} as "time" from pageview - where website_id=$1 - and created_at between $2 and $3 - ${urlFilter} - ${refFilter} + ${joinSession} + where pageview.website_id=$1 + and pageview.created_at between $2 and $3 + ${getFilterQuery('pageview', { url, referrer }, params)} + ${getFilterQuery('session', { os, browser, device, country }, params)} group by 1, 2 ) t `, @@ -391,30 +440,22 @@ export function getPageviewStats( filters = {}, ) { const params = [website_id, start_at, end_at]; - const { url, referrer } = filters; - - let urlFilter = ''; - let refFilter = ''; - - if (url) { - urlFilter = `and url=$${params.length + 1}`; - params.push(decodeURIComponent(url)); - } - - if (referrer) { - refFilter = `and referrer like $${params.length + 1}`; - params.push(`%${decodeURIComponent(referrer)}%`); - } + const { url, referrer, os, browser, device, country } = filters; + const joinSession = + os || browser || device || country + ? 'inner join session on session.session_id = pageview.session_id' + : ''; return rawQuery( ` - select ${getDateQuery('created_at', unit, timezone)} t, + select ${getDateQuery('pageview.created_at', unit, timezone)} t, count(${count}) y from pageview - where website_id=$1 - and created_at between $2 and $3 - ${urlFilter} - ${refFilter} + ${joinSession} + where pageview.website_id=$1 + and pageview.created_at between $2 and $3 + ${getFilterQuery('pageview', { url, referrer }, params)} + ${getFilterQuery('session', { os, browser, device, country }, params)} group by 1 order by 1 `, @@ -424,32 +465,24 @@ export function getPageviewStats( export function getSessionMetrics(website_id, start_at, end_at, field, filters = {}) { const params = [website_id, start_at, end_at]; - const { url, referrer } = filters; - - let urlFilter = ''; - let refFilter = ''; - - if (url) { - urlFilter = `and url=$${params.length + 1}`; - params.push(decodeURIComponent(url)); - } - - if (referrer) { - refFilter = `and referrer like $${params.length + 1}`; - params.push(`%${decodeURIComponent(referrer)}%`); - } + const { url, referrer, os, browser, device, country } = filters; + const joinSession = + os || browser || device || country + ? 'inner join session on session.session_id = pageview.session_id' + : ''; return rawQuery( ` select ${field} x, count(*) y - from session - where session_id in ( - select session_id + from session as x + where x.session_id in ( + select pageview.session_id from pageview - where website_id=$1 - and created_at between $2 and $3 - ${urlFilter} - ${refFilter} + ${joinSession} + where pageview.website_id=$1 + and pageview.created_at between $2 and $3 + ${getFilterQuery('pageview', { url, referrer }, params)} + ${getFilterQuery('session', { os, browser, device, country }, params)} ) group by 1 order by 2 desc @@ -460,36 +493,21 @@ 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, referrer } = filters; - - let domainFilter = ''; - let urlFilter = ''; - let refFilter = ''; - - if (domain) { - domainFilter = `and referrer not like $${params.length + 1} and referrer not like '/%'`; - params.push(`%://${domain}/%`); - } - - if (url) { - urlFilter = `and url=$${params.length + 1}`; - params.push(decodeURIComponent(url)); - } - - if (referrer && table !== 'event') { - refFilter = `and referrer like $${params.length + 1}`; - params.push(`%${decodeURIComponent(referrer)}%`); - } + const { domain, url, referrer, os, browser, device, country } = filters; + const joinSession = + (os || browser || device || country) && table === 'pageview' + ? 'inner join session on session.session_id = pageview.session_id' + : ''; return rawQuery( ` select ${field} x, count(*) y from ${table} - where website_id=$1 - and created_at between $2 and $3 - ${domainFilter} - ${urlFilter} - ${refFilter} + ${joinSession} + where ${table}.website_id=$1 + and ${table}.created_at between $2 and $3 + ${getFilterQuery(table, { domain, url, referrer }, params)} + ${joinSession && getFilterQuery('session', { os, browser, device, country }, params)} group by 1 order by 2 desc `, @@ -521,20 +539,6 @@ export function getEventMetrics( filters = {}, ) { const params = [website_id, start_at, end_at]; - const { url, event_type } = filters; - - let urlFilter = ''; - let eventTypeFilter = ''; - - if (url) { - urlFilter = `and url=$${params.length + 1}`; - params.push(decodeURIComponent(url)); - } - - if (event_type) { - eventTypeFilter = `and event_type=$${params.length + 1}`; - params.push(event_type); - } return rawQuery( ` @@ -545,8 +549,7 @@ export function getEventMetrics( from event where website_id=$1 and created_at between $2 and $3 - ${urlFilter} - ${eventTypeFilter} + ${getFilterQuery('event', filters, params)} group by 1, 2 order by 2 `, diff --git a/package.json b/package.json index b93499d2..c7b40c23 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "umami", - "version": "1.29.0", - "description": "A simple, fast, website analytics alternative to Google Analytics.", + "version": "1.30.0", + "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Mike Cao ", "license": "MIT", "homepage": "https://umami.is", diff --git a/pages/_app.js b/pages/_app.js index 86edfcdc..1e03db15 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -37,7 +37,7 @@ export default function App({ Component, pageProps }) { diff --git a/pages/api/collect.js b/pages/api/collect.js index 683a1c93..c9159858 100644 --- a/pages/api/collect.js +++ b/pages/api/collect.js @@ -14,8 +14,9 @@ export default async (req, res) => { return ok(res); } - if (process.env.IGNORE_IP) { - const ips = process.env.IGNORE_IP.split(',').map(n => n.trim()); + const ignoreIps = process.env.IGNORE_IP; + if (ignoreIps) { + const ips = ignoreIps.split(',').map(n => n.trim()); const ip = getIpAddress(req); const blocked = ips.find(i => { if (i === ip) return true; diff --git a/pages/api/website/[id]/metrics.js b/pages/api/website/[id]/metrics.js index aa285a55..44001cfc 100644 --- a/pages/api/website/[id]/metrics.js +++ b/pages/api/website/[id]/metrics.js @@ -33,22 +33,31 @@ export default async (req, res) => { return unauthorized(res); } - const { id, type, start_at, end_at, url, referrer } = req.query; + const { id, type, start_at, end_at, url, referrer, os, browser, device, country } = req.query; const websiteId = +id; const startDate = new Date(+start_at); const endDate = new Date(+end_at); if (sessionColumns.includes(type)) { - let data = await getSessionMetrics(websiteId, startDate, endDate, type, { url, referrer }); + let data = await getSessionMetrics(websiteId, startDate, endDate, type, { + os, + browser, + device, + country, + }); if (type === 'language') { let combined = {}; for (let { x, y } of data) { x = String(x).toLowerCase().split('-')[0]; - if (!combined[x]) combined[x] = { x, y }; - else combined[x].y += y; + + if (!combined[x]) { + combined[x] = { x, y }; + } else { + combined[x].y += y; + } } data = Object.values(combined); @@ -77,8 +86,12 @@ export default async (req, res) => { getTable(type), { domain, - url: type !== 'url' && url, - referrer, + url: type !== 'url' ? url : undefined, + referrer: type !== 'referrer' ? referrer : undefined, + os: type !== 'os' ? os : undefined, + browser: type !== 'browser' ? browser : undefined, + device: type !== 'device' ? device : undefined, + country: type !== 'country' ? country : undefined, }, ); diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js index 41ea06eb..bc663ce1 100644 --- a/pages/api/website/[id]/pageviews.js +++ b/pages/api/website/[id]/pageviews.js @@ -14,7 +14,8 @@ export default async (req, res) => { return unauthorized(res); } - const { id, start_at, end_at, unit, tz, url, referrer } = req.query; + const { id, start_at, end_at, unit, tz, url, referrer, os, browser, device, country } = + req.query; const websiteId = +id; const startDate = new Date(+start_at); @@ -25,10 +26,20 @@ export default async (req, res) => { } const [pageviews, sessions] = await Promise.all([ - getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', { url, referrer }), - getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', { + getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', { url, referrer, + os, + browser, + device, + country, + }), + getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct pageview.session_id', { + url, + os, + browser, + device, + country, }), ]); diff --git a/pages/api/website/[id]/stats.js b/pages/api/website/[id]/stats.js index 7b1c5cf0..15cc45ad 100644 --- a/pages/api/website/[id]/stats.js +++ b/pages/api/website/[id]/stats.js @@ -11,7 +11,7 @@ export default async (req, res) => { return unauthorized(res); } - const { id, start_at, end_at, url, referrer } = req.query; + const { id, start_at, end_at, url, referrer, os, browser, device, country } = req.query; const websiteId = +id; const startDate = new Date(+start_at); @@ -21,10 +21,21 @@ 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, referrer }); + const metrics = await getWebsiteStats(websiteId, startDate, endDate, { + url, + referrer, + os, + browser, + device, + country, + }); const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, { url, referrer, + os, + browser, + device, + country, }); const stats = Object.keys(metrics[0]).reduce((obj, key) => {