mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Allow filtering on session fields.
This commit is contained in:
parent
edd1645bab
commit
fb2dc9f5ab
34
components/common/FilterLink.js
Normal file
34
components/common/FilterLink.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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 {
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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 (
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
body .inactive {
|
|
||||||
color: var(--gray500);
|
|
||||||
}
|
|
||||||
|
|
||||||
body .active {
|
|
||||||
color: var(--gray900);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
|
239
lib/queries.js
239
lib/queries.js
@ -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
|
||||||
`,
|
`,
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -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) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user