Allow filtering on session fields.

This commit is contained in:
Mike Cao 2022-04-10 03:51:43 -07:00
parent edd1645bab
commit fb2dc9f5ab
19 changed files with 275 additions and 211 deletions

View File

@ -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 (
<div className={styles.row}>
<Link href={resolve({ [id]: value })} replace>
<a
className={classNames(styles.label, {
[styles.inactive]: active && !selected,
[styles.active]: active && selected,
})}
>
{safeDecodeURI(label || value)}
</a>
</Link>
{externalUrl && (
<a href={externalUrl} target="_blank" rel="noreferrer noopener" className={styles.link}>
<Icon icon={<External />} className={styles.icon} />
</a>
)}
</div>
);
}

View File

@ -1,19 +1,20 @@
body .inactive { .row {
display: flex;
align-items: center;
}
.row .inactive {
color: var(--gray500); color: var(--gray500);
} }
body .active { .row .active {
color: var(--gray900); color: var(--gray900);
font-weight: 600; font-weight: 600;
} }
.row {
display: flex;
justify-content: space-between;
}
.row .link { .row .link {
display: none; display: none;
margin-left: 20px;
} }
.row .label { .row .label {

View File

@ -2,8 +2,13 @@ import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { browserFilter } from 'lib/filters'; import { browserFilter } from 'lib/filters';
import FilterLink from '../common/FilterLink';
export default function BrowsersTable({ websiteId, ...props }) { export default function BrowsersTable({ websiteId, ...props }) {
function renderLink({ x: browser }) {
return <FilterLink id="browser" value={browser} />;
}
return ( return (
<MetricsTable <MetricsTable
{...props} {...props}
@ -12,6 +17,7 @@ export default function BrowsersTable({ websiteId, ...props }) {
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />} metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId} websiteId={websiteId}
dataFilter={browserFilter} dataFilter={browserFilter}
renderLabel={renderLink}
/> />
); );
} }

View File

@ -2,6 +2,7 @@ import React from 'react';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import FilterLink from 'components/common/FilterLink';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
@ -9,10 +10,16 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
const { locale } = useLocale(); const { locale } = useLocale();
const countryNames = useCountryNames(locale); const countryNames = useCountryNames(locale);
function renderLabel({ x }) { function renderLink({ x: code }) {
return ( return (
<div className={locale}> <div className={locale}>
{countryNames[x] ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />} <FilterLink
id="country"
value={code}
label={
countryNames[code] ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />
}
/>
</div> </div>
); );
} }
@ -25,7 +32,7 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />} metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId} websiteId={websiteId}
onDataLoad={data => onDataLoad?.(percentFilter(data))} onDataLoad={data => onDataLoad?.(percentFilter(data))}
renderLabel={renderLabel} renderLabel={renderLink}
/> />
); );
} }

View File

@ -1,9 +1,18 @@
import React from 'react'; import React from 'react';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import { getDeviceMessage } from 'components/messages'; import { getDeviceMessage } from 'components/messages';
import FilterLink from 'components/common/FilterLink';
export default function DevicesTable({ websiteId, ...props }) { export default function DevicesTable({ websiteId, ...props }) {
const { formatMessage } = useIntl();
function renderLink({ x: device }) {
return (
<FilterLink id="device" value={device} label={formatMessage(getDeviceMessage(device))} />
);
}
return ( return (
<MetricsTable <MetricsTable
{...props} {...props}
@ -11,7 +20,7 @@ export default function DevicesTable({ websiteId, ...props }) {
type="device" type="device"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />} metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId} websiteId={websiteId}
renderLabel={({ x }) => <FormattedMessage {...getDeviceMessage(x)} />} renderLabel={renderLink}
/> />
); );
} }

View File

@ -18,7 +18,7 @@ export default function MetricsBar({ websiteId, className }) {
const { startDate, endDate, modified } = dateRange; const { startDate, endDate, modified } = dateRange;
const [format, setFormat] = useState(true); const [format, setFormat] = useState(true);
const { const {
query: { url, referrer }, query: { url, referrer, os, browser, device, country },
} = usePageQuery(); } = usePageQuery();
const { data, error, loading } = useFetch( const { data, error, loading } = useFetch(
@ -29,10 +29,14 @@ export default function MetricsBar({ websiteId, className }) {
end_at: +endDate, end_at: +endDate,
url, url,
referrer, referrer,
os,
browser,
device,
country,
}, },
headers: { [TOKEN_HEADER]: shareToken?.token }, headers: { [TOKEN_HEADER]: shareToken?.token },
}, },
[modified, url, referrer], [modified, url, referrer, os, browser, device, country],
); );
const formatFunc = format const formatFunc = format

View File

@ -30,7 +30,7 @@ export default function MetricsTable({
const { const {
resolve, resolve,
router, router,
query: { url, referrer }, query: { url, referrer, os, browser, device, country },
} = usePageQuery(); } = usePageQuery();
const { data, loading, error } = useFetch( const { data, loading, error } = useFetch(
@ -42,12 +42,16 @@ export default function MetricsTable({
end_at: +endDate, end_at: +endDate,
url, url,
referrer, referrer,
os,
browser,
device,
country,
}, },
onDataLoad, onDataLoad,
delay: DEFAULT_ANIMATION_DURATION, delay: DEFAULT_ANIMATION_DURATION,
headers: { [TOKEN_HEADER]: shareToken?.token }, headers: { [TOKEN_HEADER]: shareToken?.token },
}, },
[modified, url, referrer], [modified, url, referrer, os, browser, device, country],
); );
const filteredData = useMemo(() => { const filteredData = useMemo(() => {

View File

@ -1,14 +1,20 @@
import React from 'react'; import React from 'react';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import FilterLink from 'components/common/FilterLink';
export default function OSTable({ websiteId, ...props }) { export default function OSTable({ websiteId, ...props }) {
function renderLink({ x: os }) {
return <FilterLink id="os" value={os} />;
}
return ( return (
<MetricsTable <MetricsTable
{...props} {...props}
title={<FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />} title={<FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />}
type="os" type="os"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />} metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
renderLabel={renderLink}
websiteId={websiteId} websiteId={websiteId}
/> />
); );

View File

@ -1,23 +1,15 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import FilterLink from 'components/common/FilterLink';
import Link from 'next/link';
import FilterButtons from 'components/common/FilterButtons'; import FilterButtons from 'components/common/FilterButtons';
import { urlFilter } from 'lib/filters'; import { urlFilter } from 'lib/filters';
import { safeDecodeURI } from 'lib/url';
import usePageQuery from 'hooks/usePageQuery';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import styles from './PagesTable.module.css';
export const FILTER_COMBINED = 0; export const FILTER_COMBINED = 0;
export const FILTER_RAW = 1; export const FILTER_RAW = 1;
export default function PagesTable({ websiteId, websiteDomain, showFilters, ...props }) { export default function PagesTable({ websiteId, websiteDomain, showFilters, ...props }) {
const [filter, setFilter] = useState(FILTER_COMBINED); const [filter, setFilter] = useState(FILTER_COMBINED);
const {
resolve,
query: { url: currentUrl },
} = usePageQuery();
const buttons = [ const buttons = [
{ {
@ -28,18 +20,7 @@ export default function PagesTable({ websiteId, websiteDomain, showFilters, ...p
]; ];
const renderLink = ({ x: url }) => { const renderLink = ({ x: url }) => {
return ( return <FilterLink id="url" value={url} />;
<Link href={resolve({ url })} replace={true}>
<a
className={classNames({
[styles.inactive]: currentUrl && url !== currentUrl,
[styles.active]: url === currentUrl,
})}
>
{safeDecodeURI(url)}
</a>
</Link>
);
}; };
return ( return (

View File

@ -1,8 +0,0 @@
body .inactive {
color: var(--gray500);
}
body .active {
color: var(--gray900);
font-weight: 600;
}

View File

@ -2,14 +2,8 @@ import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import FilterButtons from 'components/common/FilterButtons'; import FilterButtons from 'components/common/FilterButtons';
import FilterLink from 'components/common/FilterLink';
import { refFilter } from 'lib/filters'; 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_DOMAIN_ONLY = 0;
export const FILTER_COMBINED = 1; export const FILTER_COMBINED = 1;
@ -17,10 +11,6 @@ export const FILTER_RAW = 2;
export default function ReferrersTable({ websiteId, websiteDomain, showFilters, ...props }) { export default function ReferrersTable({ websiteId, websiteDomain, showFilters, ...props }) {
const [filter, setFilter] = useState(FILTER_COMBINED); const [filter, setFilter] = useState(FILTER_COMBINED);
const {
resolve,
query: { referrer: currentRef },
} = usePageQuery();
const buttons = [ const buttons = [
{ {
@ -34,24 +24,8 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters,
{ label: <FormattedMessage id="metrics.filter.raw" defaultMessage="Raw" />, value: FILTER_RAW }, { label: <FormattedMessage id="metrics.filter.raw" defaultMessage="Raw" />, value: FILTER_RAW },
]; ];
const renderLink = ({ w: link, x: label }) => { const renderLink = ({ w: link, x: referrer }) => {
return ( return <FilterLink id="referrer" value={referrer} externalUrl={link} />;
<div className={styles.row}>
<Link href={resolve({ referrer: label })} replace={true}>
<a
className={classNames(styles.label, {
[styles.inactive]: currentRef && label !== currentRef,
[styles.active]: label === currentRef,
})}
>
{safeDecodeURI(label)}
</a>
</Link>
<a href={link || label} target="_blank" rel="noreferrer noopener" className={styles.link}>
<Icon icon={<External />} className={styles.icon} />
</a>
</div>
);
}; };
return ( return (

View File

@ -33,7 +33,7 @@ export default function WebsiteChart({
const { const {
router, router,
resolve, resolve,
query: { url, referrer }, query: { url, referrer, os, browser, device, country },
} = usePageQuery(); } = usePageQuery();
const { get } = useApi(); const { get } = useApi();
@ -47,11 +47,15 @@ export default function WebsiteChart({
tz: timezone, tz: timezone,
url, url,
referrer, referrer,
os,
browser,
device,
country,
}, },
onDataLoad, onDataLoad,
headers: { [TOKEN_HEADER]: shareToken?.token }, headers: { [TOKEN_HEADER]: shareToken?.token },
}, },
[modified, url, referrer], [modified, url, referrer, os, browser, device, country],
); );
const chartData = useMemo(() => { const chartData = useMemo(() => {
@ -88,7 +92,10 @@ export default function WebsiteChart({
stickyClassName={styles.sticky} stickyClassName={styles.sticky}
enabled={stickyHeader} enabled={stickyHeader}
> >
<FilterTags params={{ url, referrer }} onClick={handleCloseFilter} /> <FilterTags
params={{ url, referrer, os, browser, device, country }}
onClick={handleCloseFilter}
/>
<div className="col-12 col-lg-9"> <div className="col-12 col-lg-9">
<MetricsBar websiteId={websiteId} /> <MetricsBar websiteId={websiteId} />
</div> </div>

View File

@ -21,24 +21,6 @@ export function getDatabase() {
return type; 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) { export function getDateQuery(field, unit, timezone) {
const db = getDatabase(); 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) { export async function getWebsiteById(website_id) {
return runQuery( return runQuery(
prisma.website.findUnique({ prisma.website.findUnique({
@ -344,19 +399,12 @@ export async function getEvents(websites, start_at) {
export function getWebsiteStats(website_id, start_at, end_at, filters = {}) { export function getWebsiteStats(website_id, start_at, end_at, filters = {}) {
const params = [website_id, start_at, end_at]; const params = [website_id, start_at, end_at];
const { url, referrer } = filters; const { url, referrer, os, browser, device, country } = filters;
let urlFilter = '';
let refFilter = '';
if (url) { const joinSession =
urlFilter = `and url=$${params.length + 1}`; os || browser || device || country
params.push(decodeURIComponent(url)); ? 'inner join session on session.session_id = pageview.session_id'
} : '';
if (referrer) {
refFilter = `and referrer like $${params.length + 1}`;
params.push(`%${decodeURIComponent(referrer)}%`);
}
return rawQuery( 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(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(t.time) as "totaltime" sum(t.time) as "totaltime"
from ( from (
select session_id, select pageview.session_id,
${getDateQuery('created_at', 'hour')}, ${getDateQuery('pageview.created_at', 'hour')},
count(*) c, count(*) c,
${getTimestampInterval('created_at')} as "time" ${getTimestampInterval('pageview.created_at')} as "time"
from pageview from pageview
where website_id=$1 ${joinSession}
and created_at between $2 and $3 where pageview.website_id=$1
${urlFilter} and pageview.created_at between $2 and $3
${refFilter} ${getFilterQuery('pageview', { url, referrer }, params)}
${getFilterQuery('session', { os, browser, device, country }, params)}
group by 1, 2 group by 1, 2
) t ) t
`, `,
@ -391,30 +440,22 @@ export function getPageviewStats(
filters = {}, filters = {},
) { ) {
const params = [website_id, start_at, end_at]; const params = [website_id, start_at, end_at];
const { url, referrer } = filters; const { url, referrer, os, browser, device, country } = filters;
const joinSession =
let urlFilter = ''; os || browser || device || country
let refFilter = ''; ? 'inner join session on session.session_id = pageview.session_id'
: '';
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)}%`);
}
return rawQuery( return rawQuery(
` `
select ${getDateQuery('created_at', unit, timezone)} t, select ${getDateQuery('pageview.created_at', unit, timezone)} t,
count(${count}) y count(${count}) y
from pageview from pageview
where website_id=$1 ${joinSession}
and created_at between $2 and $3 where pageview.website_id=$1
${urlFilter} and pageview.created_at between $2 and $3
${refFilter} ${getFilterQuery('pageview', { url, referrer }, params)}
${getFilterQuery('session', { os, browser, device, country }, params)}
group by 1 group by 1
order by 1 order by 1
`, `,
@ -424,32 +465,24 @@ export function getPageviewStats(
export function getSessionMetrics(website_id, start_at, end_at, field, filters = {}) { export function getSessionMetrics(website_id, start_at, end_at, field, filters = {}) {
const params = [website_id, start_at, end_at]; const params = [website_id, start_at, end_at];
const { url, referrer } = filters; const { url, referrer, os, browser, device, country } = filters;
const joinSession =
let urlFilter = ''; os || browser || device || country
let refFilter = ''; ? 'inner join session on session.session_id = pageview.session_id'
: '';
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)}%`);
}
return rawQuery( return rawQuery(
` `
select ${field} x, count(*) y select ${field} x, count(*) y
from session from session as x
where session_id in ( where x.session_id in (
select session_id select pageview.session_id
from pageview from pageview
where website_id=$1 ${joinSession}
and created_at between $2 and $3 where pageview.website_id=$1
${urlFilter} and pageview.created_at between $2 and $3
${refFilter} ${getFilterQuery('pageview', { url, referrer }, params)}
${getFilterQuery('session', { os, browser, device, country }, params)}
) )
group by 1 group by 1
order by 2 desc 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 = {}) { export function getPageviewMetrics(website_id, start_at, end_at, field, table, filters = {}) {
const params = [website_id, start_at, end_at]; const params = [website_id, start_at, end_at];
const { domain, url, referrer } = filters; const { domain, url, referrer, os, browser, device, country } = filters;
const joinSession =
let domainFilter = ''; (os || browser || device || country) && table === 'pageview'
let urlFilter = ''; ? 'inner join session on session.session_id = pageview.session_id'
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)}%`);
}
return rawQuery( return rawQuery(
` `
select ${field} x, count(*) y select ${field} x, count(*) y
from ${table} from ${table}
where website_id=$1 ${joinSession}
and created_at between $2 and $3 where ${table}.website_id=$1
${domainFilter} and ${table}.created_at between $2 and $3
${urlFilter} ${getFilterQuery(table, { domain, url, referrer }, params)}
${refFilter} ${joinSession && getFilterQuery('session', { os, browser, device, country }, params)}
group by 1 group by 1
order by 2 desc order by 2 desc
`, `,
@ -521,20 +539,6 @@ export function getEventMetrics(
filters = {}, filters = {},
) { ) {
const params = [website_id, start_at, end_at]; 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( return rawQuery(
` `
@ -545,8 +549,7 @@ export function getEventMetrics(
from event from event
where website_id=$1 where website_id=$1
and created_at between $2 and $3 and created_at between $2 and $3
${urlFilter} ${getFilterQuery('event', filters, params)}
${eventTypeFilter}
group by 1, 2 group by 1, 2
order by 2 order by 2
`, `,

View File

@ -1,7 +1,7 @@
{ {
"name": "umami", "name": "umami",
"version": "1.29.0", "version": "1.30.0",
"description": "A simple, fast, website analytics alternative to Google Analytics.", "description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Mike Cao <mike@mikecao.com>", "author": "Mike Cao <mike@mikecao.com>",
"license": "MIT", "license": "MIT",
"homepage": "https://umami.is", "homepage": "https://umami.is",

View File

@ -37,7 +37,7 @@ export default function App({ Component, pageProps }) {
<link rel="mask-icon" href={`${basePath}/safari-pinned-tab.svg`} color="#5bbad5" /> <link rel="mask-icon" href={`${basePath}/safari-pinned-tab.svg`} color="#5bbad5" />
<link <link
rel="preload" rel="preload"
href={`https://i.umami.is/umami.png?v=${version}`} href={`https://i.umami.is/icon.png?v=${version}`}
as="image" as="image"
type="image/png" type="image/png"
/> />

View File

@ -14,8 +14,9 @@ export default async (req, res) => {
return ok(res); return ok(res);
} }
if (process.env.IGNORE_IP) { const ignoreIps = process.env.IGNORE_IP;
const ips = process.env.IGNORE_IP.split(',').map(n => n.trim()); if (ignoreIps) {
const ips = ignoreIps.split(',').map(n => n.trim());
const ip = getIpAddress(req); const ip = getIpAddress(req);
const blocked = ips.find(i => { const blocked = ips.find(i => {
if (i === ip) return true; if (i === ip) return true;

View File

@ -33,22 +33,31 @@ export default async (req, res) => {
return unauthorized(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 websiteId = +id;
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
const endDate = new Date(+end_at); const endDate = new Date(+end_at);
if (sessionColumns.includes(type)) { 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') { if (type === 'language') {
let combined = {}; let combined = {};
for (let { x, y } of data) { for (let { x, y } of data) {
x = String(x).toLowerCase().split('-')[0]; 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); data = Object.values(combined);
@ -77,8 +86,12 @@ export default async (req, res) => {
getTable(type), getTable(type),
{ {
domain, domain,
url: type !== 'url' && url, url: type !== 'url' ? url : undefined,
referrer, 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,
}, },
); );

View File

@ -14,7 +14,8 @@ export default async (req, res) => {
return unauthorized(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 websiteId = +id;
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
@ -25,10 +26,20 @@ export default async (req, res) => {
} }
const [pageviews, sessions] = await Promise.all([ const [pageviews, sessions] = await Promise.all([
getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', { url, referrer }), getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', {
getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', {
url, url,
referrer, referrer,
os,
browser,
device,
country,
}),
getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct pageview.session_id', {
url,
os,
browser,
device,
country,
}), }),
]); ]);

View File

@ -11,7 +11,7 @@ export default async (req, res) => {
return unauthorized(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 websiteId = +id;
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
@ -21,10 +21,21 @@ export default async (req, res) => {
const prevStartDate = new Date(+start_at - distance); const prevStartDate = new Date(+start_at - distance);
const prevEndDate = new Date(+end_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, { const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, {
url, url,
referrer, referrer,
os,
browser,
device,
country,
}); });
const stats = Object.keys(metrics[0]).reduce((obj, key) => { const stats = Object.keys(metrics[0]).reduce((obj, key) => {