mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Refactored realtime.
This commit is contained in:
parent
cda3ba345b
commit
5108b91f80
@ -5,7 +5,7 @@ import styles from './RealtimeHeader.module.css';
|
|||||||
|
|
||||||
export function RealtimeHeader({ data }: { data: RealtimeData }) {
|
export function RealtimeHeader({ data }: { data: RealtimeData }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { pageviews, visitors, events, countries } = data || {};
|
const { totals }: any = data || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
@ -13,22 +13,22 @@ export function RealtimeHeader({ data }: { data: RealtimeData }) {
|
|||||||
<MetricCard
|
<MetricCard
|
||||||
className={styles.card}
|
className={styles.card}
|
||||||
label={formatMessage(labels.views)}
|
label={formatMessage(labels.views)}
|
||||||
value={pageviews?.length}
|
value={totals.views}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
className={styles.card}
|
className={styles.card}
|
||||||
label={formatMessage(labels.visitors)}
|
label={formatMessage(labels.visitors)}
|
||||||
value={visitors?.length}
|
value={totals.visitors}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
className={styles.card}
|
className={styles.card}
|
||||||
label={formatMessage(labels.events)}
|
label={formatMessage(labels.events)}
|
||||||
value={events?.length}
|
value={totals.events}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
className={styles.card}
|
className={styles.card}
|
||||||
label={formatMessage(labels.countries)}
|
label={formatMessage(labels.countries)}
|
||||||
value={countries?.length}
|
value={totals.countries}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,20 +54,20 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getTime = ({ timestamp }) => format(timestamp, 'h:mm:ss');
|
const getTime = ({ timestamp }) => format(timestamp * 1000, 'h:mm:ss');
|
||||||
|
|
||||||
const getColor = ({ id, sessionId }) => stringToColor(sessionId || id);
|
const getColor = ({ id, sessionId }) => stringToColor(sessionId || id);
|
||||||
|
|
||||||
const getIcon = ({ __type }) => icons[__type];
|
const getIcon = ({ __type }) => icons[__type];
|
||||||
|
|
||||||
const getDetail = (log: {
|
const getDetail = (log: {
|
||||||
__type: any;
|
__type: string;
|
||||||
eventName: any;
|
eventName: string;
|
||||||
urlPath: any;
|
urlPath: string;
|
||||||
browser: any;
|
browser: string;
|
||||||
os: any;
|
os: string;
|
||||||
country: any;
|
country: string;
|
||||||
device: any;
|
device: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { __type, eventName, urlPath: url, browser, os, country, device } = log;
|
const { __type, eventName, urlPath: url, browser, os, country, device } = log;
|
||||||
|
|
||||||
@ -141,8 +141,12 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pageviews, visitors, events } = data;
|
const { events, visitors } = data;
|
||||||
let logs = [...pageviews, ...visitors, ...events].sort(thenby.firstBy('createdAt', -1));
|
|
||||||
|
let logs = [
|
||||||
|
...events.map(e => ({ __type: e.eventName ? TYPE_EVENT : TYPE_PAGEVIEW, ...e })),
|
||||||
|
...visitors.map(v => ({ __type: TYPE_SESSION, ...v })),
|
||||||
|
].sort(thenby.firstBy('timestamp', -1));
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
logs = logs.filter(({ eventName, urlPath, browser, os, country, device }) => {
|
logs = logs.filter(({ eventName, urlPath, browser, os, country, device }) => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Key, useContext, useMemo, useState } from 'react';
|
import { Key, useContext, useState } from 'react';
|
||||||
import { ButtonGroup, Button, Flexbox } from 'react-basics';
|
import { ButtonGroup, Button, Flexbox } from 'react-basics';
|
||||||
import thenby from 'thenby';
|
import thenby from 'thenby';
|
||||||
import { percentFilter } from 'lib/filters';
|
import { percentFilter } from 'lib/filters';
|
||||||
@ -11,7 +11,7 @@ import { WebsiteContext } from '../WebsiteProvider';
|
|||||||
export function RealtimeUrls({ data }: { data: RealtimeData }) {
|
export function RealtimeUrls({ data }: { data: RealtimeData }) {
|
||||||
const website = useContext(WebsiteContext);
|
const website = useContext(WebsiteContext);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { pageviews } = data || {};
|
const { referrers, urls } = data || {};
|
||||||
const [filter, setFilter] = useState<Key>(FILTER_REFERRERS);
|
const [filter, setFilter] = useState<Key>(FILTER_REFERRERS);
|
||||||
const limit = 15;
|
const limit = 15;
|
||||||
|
|
||||||
@ -35,47 +35,29 @@ export function RealtimeUrls({ data }: { data: RealtimeData }) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [referrers = [], pages = []] = useMemo(() => {
|
const domains = percentFilter(
|
||||||
if (pageviews) {
|
Object.keys(referrers)
|
||||||
const referrers = percentFilter(
|
.map(key => {
|
||||||
pageviews
|
return {
|
||||||
.reduce((arr, { referrerDomain }) => {
|
x: key,
|
||||||
if (referrerDomain) {
|
y: referrers[key],
|
||||||
const row = arr.find(({ x }) => x === referrerDomain);
|
};
|
||||||
|
})
|
||||||
|
.sort(thenby.firstBy('y', -1))
|
||||||
|
.slice(0, limit),
|
||||||
|
);
|
||||||
|
|
||||||
if (!row) {
|
const pages = percentFilter(
|
||||||
arr.push({ x: referrerDomain, y: 1 });
|
Object.keys(urls)
|
||||||
} else {
|
.map(key => {
|
||||||
row.y += 1;
|
return {
|
||||||
}
|
x: key,
|
||||||
}
|
y: urls[key],
|
||||||
return arr;
|
};
|
||||||
}, [])
|
})
|
||||||
.sort(thenby.firstBy('y', -1))
|
.sort(thenby.firstBy('y', -1))
|
||||||
.slice(0, limit),
|
.slice(0, limit),
|
||||||
);
|
);
|
||||||
|
|
||||||
const pages = percentFilter(
|
|
||||||
pageviews
|
|
||||||
.reduce((arr, { urlPath }) => {
|
|
||||||
const row = arr.find(({ x }) => x === urlPath);
|
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
arr.push({ x: urlPath, y: 1 });
|
|
||||||
} else {
|
|
||||||
row.y += 1;
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}, [])
|
|
||||||
.sort(thenby.firstBy('y', -1))
|
|
||||||
.slice(0, limit),
|
|
||||||
);
|
|
||||||
|
|
||||||
return [referrers, pages];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}, [pageviews]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -89,7 +71,7 @@ export function RealtimeUrls({ data }: { data: RealtimeData }) {
|
|||||||
title={formatMessage(labels.referrers)}
|
title={formatMessage(labels.referrers)}
|
||||||
metric={formatMessage(labels.views)}
|
metric={formatMessage(labels.views)}
|
||||||
renderLabel={renderLink}
|
renderLabel={renderLink}
|
||||||
data={referrers}
|
data={domains}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{filter === FILTER_PAGES && (
|
{filter === FILTER_PAGES && (
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { firstBy } from 'thenby';
|
||||||
import { Grid, GridRow } from 'components/layout/Grid';
|
import { Grid, GridRow } from 'components/layout/Grid';
|
||||||
import Page from 'components/layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
import RealtimeChart from 'components/metrics/RealtimeChart';
|
import RealtimeChart from 'components/metrics/RealtimeChart';
|
||||||
@ -10,6 +11,7 @@ import RealtimeUrls from './RealtimeUrls';
|
|||||||
import RealtimeCountries from './RealtimeCountries';
|
import RealtimeCountries from './RealtimeCountries';
|
||||||
import WebsiteHeader from '../WebsiteHeader';
|
import WebsiteHeader from '../WebsiteHeader';
|
||||||
import WebsiteProvider from '../WebsiteProvider';
|
import WebsiteProvider from '../WebsiteProvider';
|
||||||
|
import { percentFilter } from 'lib/filters';
|
||||||
|
|
||||||
export function WebsiteRealtimePage({ websiteId }) {
|
export function WebsiteRealtimePage({ websiteId }) {
|
||||||
const { data, isLoading, error } = useRealtime(websiteId);
|
const { data, isLoading, error } = useRealtime(websiteId);
|
||||||
@ -18,6 +20,12 @@ export function WebsiteRealtimePage({ websiteId }) {
|
|||||||
return <Page isLoading={isLoading} error={error} />;
|
return <Page isLoading={isLoading} error={error} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const countries = percentFilter(
|
||||||
|
Object.keys(data.countries)
|
||||||
|
.map(key => ({ x: key, y: data.countries[key] }))
|
||||||
|
.sort(firstBy('y', -1)),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WebsiteProvider websiteId={websiteId}>
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
<WebsiteHeader websiteId={websiteId} />
|
<WebsiteHeader websiteId={websiteId} />
|
||||||
@ -29,8 +37,8 @@ export function WebsiteRealtimePage({ websiteId }) {
|
|||||||
<RealtimeLog data={data} />
|
<RealtimeLog data={data} />
|
||||||
</GridRow>
|
</GridRow>
|
||||||
<GridRow columns="one-two">
|
<GridRow columns="one-two">
|
||||||
<RealtimeCountries data={data?.countries} />
|
<RealtimeCountries data={countries} />
|
||||||
<WorldMap data={data?.countries} />
|
<WorldMap data={countries} />
|
||||||
</GridRow>
|
</GridRow>
|
||||||
</Grid>
|
</Grid>
|
||||||
</WebsiteProvider>
|
</WebsiteProvider>
|
||||||
|
@ -1,87 +1,21 @@
|
|||||||
import { useMemo, useRef } from 'react';
|
|
||||||
import { RealtimeData } from 'lib/types';
|
import { RealtimeData } from 'lib/types';
|
||||||
import { useApi } from './useApi';
|
import { useApi } from './useApi';
|
||||||
import { REALTIME_INTERVAL, REALTIME_RANGE } from 'lib/constants';
|
import { REALTIME_INTERVAL } from 'lib/constants';
|
||||||
import { startOfMinute, subMinutes } from 'date-fns';
|
import { useTimezone } from 'components/hooks';
|
||||||
import { percentFilter } from 'lib/filters';
|
|
||||||
import thenby from 'thenby';
|
|
||||||
|
|
||||||
function mergeData(state = [], data = [], time: number) {
|
|
||||||
const ids = state.map(({ id }) => id);
|
|
||||||
return state
|
|
||||||
.concat(data.filter(({ id }) => !ids.includes(id)))
|
|
||||||
.filter(({ timestamp }) => timestamp >= time);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRealtime(websiteId: string) {
|
export function useRealtime(websiteId: string) {
|
||||||
const currentData = useRef({
|
|
||||||
pageviews: [],
|
|
||||||
sessions: [],
|
|
||||||
events: [],
|
|
||||||
countries: [],
|
|
||||||
visitors: [],
|
|
||||||
timestamp: 0,
|
|
||||||
});
|
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
|
const { timezone } = useTimezone();
|
||||||
const { data, isLoading, error } = useQuery<RealtimeData>({
|
const { data, isLoading, error } = useQuery<RealtimeData>({
|
||||||
queryKey: ['realtime', websiteId],
|
queryKey: ['realtime', websiteId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const state = currentData.current;
|
return get(`/realtime/${websiteId}`, { timezone });
|
||||||
const data = await get(`/realtime/${websiteId}`, { startAt: state?.timestamp || 0 });
|
|
||||||
const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
|
|
||||||
const time = date.getTime();
|
|
||||||
const { pageviews, sessions, events, timestamp } = data;
|
|
||||||
|
|
||||||
return {
|
|
||||||
pageviews: mergeData(state?.pageviews, pageviews, time),
|
|
||||||
sessions: mergeData(state?.sessions, sessions, time),
|
|
||||||
events: mergeData(state?.events, events, time),
|
|
||||||
timestamp,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
enabled: !!websiteId,
|
enabled: !!websiteId,
|
||||||
refetchInterval: REALTIME_INTERVAL,
|
refetchInterval: REALTIME_INTERVAL,
|
||||||
});
|
});
|
||||||
|
|
||||||
const realtimeData: RealtimeData = useMemo(() => {
|
return { data, isLoading, error };
|
||||||
if (!data) {
|
|
||||||
return { pageviews: [], sessions: [], events: [], countries: [], visitors: [], timestamp: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
data.countries = percentFilter(
|
|
||||||
data.sessions
|
|
||||||
.reduce((arr, data) => {
|
|
||||||
if (!arr.find(({ id }) => id === data.id)) {
|
|
||||||
return arr.concat(data);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}, [])
|
|
||||||
.reduce((arr: { x: any; y: number }[], { country }: any) => {
|
|
||||||
if (country) {
|
|
||||||
const row = arr.find(({ x }) => x === country);
|
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
arr.push({ x: country, y: 1 });
|
|
||||||
} else {
|
|
||||||
row.y += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}, [])
|
|
||||||
.sort(thenby.firstBy('y', -1)),
|
|
||||||
);
|
|
||||||
|
|
||||||
data.visitors = data.sessions.reduce((arr, val) => {
|
|
||||||
if (!arr.find(({ id }) => id === val.id)) {
|
|
||||||
return arr.concat(val);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
return { data: realtimeData, isLoading, error };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useRealtime;
|
export default useRealtime;
|
||||||
|
@ -7,7 +7,6 @@ export function useFields() {
|
|||||||
{ name: 'url', type: 'string', label: formatMessage(labels.url) },
|
{ name: 'url', type: 'string', label: formatMessage(labels.url) },
|
||||||
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
|
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
|
||||||
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
|
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
|
||||||
{ name: 'host', type: 'string', label: formatMessage(labels.host) },
|
|
||||||
{ name: 'query', type: 'string', label: formatMessage(labels.query) },
|
{ name: 'query', type: 'string', label: formatMessage(labels.query) },
|
||||||
{ name: 'browser', type: 'string', label: formatMessage(labels.browser) },
|
{ name: 'browser', type: 'string', label: formatMessage(labels.browser) },
|
||||||
{ name: 'os', type: 'string', label: formatMessage(labels.os) },
|
{ name: 'os', type: 'string', label: formatMessage(labels.os) },
|
||||||
@ -15,6 +14,7 @@ export function useFields() {
|
|||||||
{ name: 'country', type: 'string', label: formatMessage(labels.country) },
|
{ name: 'country', type: 'string', label: formatMessage(labels.country) },
|
||||||
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
|
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
|
||||||
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
|
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
|
||||||
|
{ name: 'host', type: 'string', label: formatMessage(labels.host) },
|
||||||
];
|
];
|
||||||
|
|
||||||
return { fields };
|
return { fields };
|
||||||
|
@ -5,11 +5,11 @@ import { renderDateLabels } from 'lib/charts';
|
|||||||
|
|
||||||
export interface PageviewsChartProps extends BarChartProps {
|
export interface PageviewsChartProps extends BarChartProps {
|
||||||
data: {
|
data: {
|
||||||
pageviews: any[];
|
views: any[];
|
||||||
sessions: any[];
|
visitors: any[];
|
||||||
compare?: {
|
compare?: {
|
||||||
pageviews: any[];
|
views: any[];
|
||||||
sessions: any[];
|
visitors: any[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
unit: string;
|
unit: string;
|
||||||
@ -30,14 +30,14 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
|
|||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.visitors),
|
label: formatMessage(labels.visitors),
|
||||||
data: data.sessions,
|
data: data.visitors,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
...colors.chart.visitors,
|
...colors.chart.visitors,
|
||||||
order: 3,
|
order: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.views),
|
label: formatMessage(labels.views),
|
||||||
data: data.pageviews,
|
data: data.views,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
...colors.chart.views,
|
...colors.chart.views,
|
||||||
order: 4,
|
order: 4,
|
||||||
@ -47,7 +47,7 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
|
|||||||
{
|
{
|
||||||
type: 'line',
|
type: 'line',
|
||||||
label: `${formatMessage(labels.views)} (${formatMessage(labels.previous)})`,
|
label: `${formatMessage(labels.views)} (${formatMessage(labels.previous)})`,
|
||||||
data: data.compare.pageviews,
|
data: data.compare.views,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
backgroundColor: '#8601B0',
|
backgroundColor: '#8601B0',
|
||||||
borderColor: '#8601B0',
|
borderColor: '#8601B0',
|
||||||
@ -56,7 +56,7 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
|
|||||||
{
|
{
|
||||||
type: 'line',
|
type: 'line',
|
||||||
label: `${formatMessage(labels.visitors)} (${formatMessage(labels.previous)})`,
|
label: `${formatMessage(labels.visitors)} (${formatMessage(labels.previous)})`,
|
||||||
data: data.compare.sessions,
|
data: data.compare.visitors,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
backgroundColor: '#f15bb5',
|
backgroundColor: '#f15bb5',
|
||||||
borderColor: '#f15bb5',
|
borderColor: '#f15bb5',
|
||||||
|
@ -1,29 +1,10 @@
|
|||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { format, startOfMinute, subMinutes, isBefore } from 'date-fns';
|
import { startOfMinute, subMinutes, isBefore } from 'date-fns';
|
||||||
import PageviewsChart from './PageviewsChart';
|
import PageviewsChart from './PageviewsChart';
|
||||||
import { getDateArray } from 'lib/date';
|
import { getDateArray } from 'lib/date';
|
||||||
import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants';
|
import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants';
|
||||||
import { RealtimeData } from 'lib/types';
|
import { RealtimeData } from 'lib/types';
|
||||||
|
|
||||||
function mapData(data: any[]) {
|
|
||||||
let last = 0;
|
|
||||||
const arr = [];
|
|
||||||
|
|
||||||
data?.reduce((obj, { timestamp }) => {
|
|
||||||
const t = startOfMinute(new Date(timestamp));
|
|
||||||
if (t.getTime() > last) {
|
|
||||||
obj = { x: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 };
|
|
||||||
arr.push(obj);
|
|
||||||
last = t.getTime();
|
|
||||||
} else {
|
|
||||||
obj.y += 1;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RealtimeChartProps {
|
export interface RealtimeChartProps {
|
||||||
data: RealtimeData;
|
data: RealtimeData;
|
||||||
unit: string;
|
unit: string;
|
||||||
@ -37,12 +18,12 @@ export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) {
|
|||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return { pageviews: [], sessions: [] };
|
return { views: [], visitors: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit),
|
views: getDateArray(data.series.views, startDate, endDate, unit),
|
||||||
sessions: getDateArray(mapData(data.visitors), startDate, endDate, unit),
|
visitors: getDateArray(data.series.visitors, startDate, endDate, unit),
|
||||||
};
|
};
|
||||||
}, [data, startDate, endDate, unit]);
|
}, [data, startDate, endDate, unit]);
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import { maxDate } from './date';
|
|||||||
import { filtersToArray } from './params';
|
import { filtersToArray } from './params';
|
||||||
|
|
||||||
export const CLICKHOUSE_DATE_FORMATS = {
|
export const CLICKHOUSE_DATE_FORMATS = {
|
||||||
minute: '%Y-%m-%d %H:%M:00',
|
minute: '%Y-%m-%d %H:%i:00',
|
||||||
hour: '%Y-%m-%d %H:00:00',
|
hour: '%Y-%m-%d %H:00:00',
|
||||||
day: '%Y-%m-%d',
|
day: '%Y-%m-%d',
|
||||||
month: '%Y-%m-01',
|
month: '%Y-%m-01',
|
||||||
|
@ -23,7 +23,7 @@ export const DEFAULT_PAGE_SIZE = 10;
|
|||||||
export const DEFAULT_DATE_COMPARE = 'prev';
|
export const DEFAULT_DATE_COMPARE = 'prev';
|
||||||
|
|
||||||
export const REALTIME_RANGE = 30;
|
export const REALTIME_RANGE = 30;
|
||||||
export const REALTIME_INTERVAL = 5000;
|
export const REALTIME_INTERVAL = 10000;
|
||||||
|
|
||||||
export const FILTER_COMBINED = 'filter-combined';
|
export const FILTER_COMBINED = 'filter-combined';
|
||||||
export const FILTER_RAW = 'filter-raw';
|
export const FILTER_RAW = 'filter-raw';
|
||||||
@ -33,7 +33,16 @@ export const FILTER_REFERRERS = 'filter-referrers';
|
|||||||
export const FILTER_PAGES = 'filter-pages';
|
export const FILTER_PAGES = 'filter-pages';
|
||||||
|
|
||||||
export const UNIT_TYPES = ['year', 'month', 'hour', 'day', 'minute'];
|
export const UNIT_TYPES = ['year', 'month', 'hour', 'day', 'minute'];
|
||||||
export const EVENT_COLUMNS = ['url', 'entry', 'exit', 'referrer', 'title', 'query', 'event', 'host'];
|
export const EVENT_COLUMNS = [
|
||||||
|
'url',
|
||||||
|
'entry',
|
||||||
|
'exit',
|
||||||
|
'referrer',
|
||||||
|
'title',
|
||||||
|
'query',
|
||||||
|
'event',
|
||||||
|
'host',
|
||||||
|
];
|
||||||
|
|
||||||
export const SESSION_COLUMNS = [
|
export const SESSION_COLUMNS = [
|
||||||
'browser',
|
'browser',
|
||||||
|
@ -199,12 +199,23 @@ export interface QueryOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RealtimeData {
|
export interface RealtimeData {
|
||||||
pageviews: any[];
|
countries: { [key: string]: number };
|
||||||
sessions: any[];
|
|
||||||
events: any[];
|
events: any[];
|
||||||
|
pageviews: any[];
|
||||||
|
referrers: { [key: string]: number };
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
countries?: any[];
|
series: {
|
||||||
visitors?: any[];
|
views: any[];
|
||||||
|
visitors: any[];
|
||||||
|
};
|
||||||
|
totals: {
|
||||||
|
views: number;
|
||||||
|
visitors: number;
|
||||||
|
events: number;
|
||||||
|
countries: number;
|
||||||
|
};
|
||||||
|
urls: { [key: string]: number };
|
||||||
|
visitors: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionData {
|
export interface SessionData {
|
||||||
|
@ -10,13 +10,13 @@ import { REALTIME_RANGE } from 'lib/constants';
|
|||||||
|
|
||||||
export interface RealtimeRequestQuery {
|
export interface RealtimeRequestQuery {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
startAt: number;
|
timezone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
GET: yup.object().shape({
|
GET: yup.object().shape({
|
||||||
websiteId: yup.string().uuid().required(),
|
websiteId: yup.string().uuid().required(),
|
||||||
startAt: yup.number().integer().required(),
|
timezone: yup.string().required(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -28,19 +28,15 @@ export default async (
|
|||||||
await useValidate(schema, req, res);
|
await useValidate(schema, req, res);
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const { websiteId, startAt } = req.query;
|
const { websiteId, timezone } = req.query;
|
||||||
|
|
||||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
let startTime = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
|
const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
|
||||||
|
|
||||||
if (+startAt > startTime.getTime()) {
|
const data = await getRealtimeData(websiteId, { startDate, timezone });
|
||||||
startTime = new Date(+startAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await getRealtimeData(websiteId, startTime);
|
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, data);
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,34 @@
|
|||||||
import clickhouse from 'lib/clickhouse';
|
import clickhouse from 'lib/clickhouse';
|
||||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
|
import { QueryFilters } from 'lib/types';
|
||||||
|
|
||||||
export function getEvents(...args: [websiteId: string, startDate: Date, eventType: number]) {
|
export function getEvents(...args: [websiteId: string, filters: QueryFilters]) {
|
||||||
return runQuery({
|
return runQuery({
|
||||||
[PRISMA]: () => relationalQuery(...args),
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function relationalQuery(websiteId: string, startDate: Date, eventType: number) {
|
function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||||
|
const { startDate } = filters;
|
||||||
|
|
||||||
return prisma.client.websiteEvent.findMany({
|
return prisma.client.websiteEvent.findMany({
|
||||||
where: {
|
where: {
|
||||||
websiteId,
|
websiteId,
|
||||||
eventType,
|
|
||||||
createdAt: {
|
createdAt: {
|
||||||
gte: startDate,
|
gte: startDate,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'asc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function clickhouseQuery(websiteId: string, startDate: Date, eventType: number) {
|
function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||||
const { rawQuery } = clickhouse;
|
const { rawQuery } = clickhouse;
|
||||||
|
const { startDate } = filters;
|
||||||
|
|
||||||
return rawQuery(
|
return rawQuery(
|
||||||
`
|
`
|
||||||
@ -41,13 +44,11 @@ function clickhouseQuery(websiteId: string, startDate: Date, eventType: number)
|
|||||||
from website_event
|
from website_event
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
and created_at >= {startDate:DateTime64}
|
and created_at >= {startDate:DateTime64}
|
||||||
and event_type = {eventType:UInt32}
|
order by created_at desc
|
||||||
order by created_at asc
|
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
websiteId,
|
websiteId,
|
||||||
startDate,
|
startDate,
|
||||||
eventType,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,91 @@
|
|||||||
import { getSessions, getEvents } from 'queries/index';
|
import { getSessions, getEvents, getPageviewStats, getSessionStats } from 'queries/index';
|
||||||
import { EVENT_TYPE } from 'lib/constants';
|
|
||||||
|
|
||||||
export async function getRealtimeData(websiteId: string, startDate: Date) {
|
const MAX_SIZE = 50;
|
||||||
const [pageviews, sessions, events] = await Promise.all([
|
|
||||||
getEvents(websiteId, startDate, EVENT_TYPE.pageView),
|
function increment(data: object, key: string) {
|
||||||
getSessions(websiteId, startDate),
|
if (key) {
|
||||||
getEvents(websiteId, startDate, EVENT_TYPE.customEvent),
|
if (!data[key]) {
|
||||||
|
data[key] = 1;
|
||||||
|
} else {
|
||||||
|
data[key] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRealtimeData(
|
||||||
|
websiteId: string,
|
||||||
|
criteria: { startDate: Date; timezone: string },
|
||||||
|
) {
|
||||||
|
const { startDate, timezone } = criteria;
|
||||||
|
const filters = { startDate, endDate: new Date(), unit: 'minute', timezone };
|
||||||
|
const [events, sessions, pageviews, sessionviews] = await Promise.all([
|
||||||
|
getEvents(websiteId, { startDate }),
|
||||||
|
getSessions(websiteId, { startDate }),
|
||||||
|
getPageviewStats(websiteId, filters),
|
||||||
|
getSessionStats(websiteId, filters),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const decorate = (type: string, data: any[]) => {
|
const uniques = new Set();
|
||||||
return data.map((values: { [key: string]: any }) => ({
|
|
||||||
...values,
|
|
||||||
__type: type,
|
|
||||||
timestamp: values.timestamp ? values.timestamp * 1000 : new Date(values.createdAt).getTime(),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const set = new Set();
|
const sessionStats = sessions.reduce(
|
||||||
const uniques = (type: string, data: any[]) => {
|
(obj: { visitors: any; countries: any }, session: { id: any; country: any }) => {
|
||||||
return data.reduce((arr, values: { [key: string]: any }) => {
|
const { countries, visitors } = obj;
|
||||||
if (!set.has(values.id)) {
|
const { id, country } = session;
|
||||||
set.add(values.id);
|
|
||||||
|
|
||||||
return arr.concat({
|
if (!uniques.has(id)) {
|
||||||
...values,
|
uniques.add(id);
|
||||||
__type: type,
|
increment(countries, country);
|
||||||
timestamp: values.timestamp
|
|
||||||
? values.timestamp * 1000
|
if (visitors.length < MAX_SIZE) {
|
||||||
: new Date(values.createdAt).getTime(),
|
visitors.push(session);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
return arr;
|
|
||||||
}, []);
|
return obj;
|
||||||
};
|
},
|
||||||
|
{
|
||||||
|
countries: {},
|
||||||
|
visitors: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const eventStats = events.reduce(
|
||||||
|
(
|
||||||
|
obj: { urls: any; referrers: any; events: any },
|
||||||
|
event: { urlPath: any; referrerDomain: any },
|
||||||
|
) => {
|
||||||
|
const { urls, referrers, events } = obj;
|
||||||
|
const { urlPath, referrerDomain } = event;
|
||||||
|
|
||||||
|
increment(urls, urlPath);
|
||||||
|
increment(referrers, referrerDomain);
|
||||||
|
|
||||||
|
if (events.length < MAX_SIZE) {
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urls: {},
|
||||||
|
referrers: {},
|
||||||
|
events: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageviews: decorate('pageview', pageviews),
|
...sessionStats,
|
||||||
sessions: uniques('session', sessions),
|
...eventStats,
|
||||||
events: decorate('event', events),
|
series: {
|
||||||
|
views: pageviews,
|
||||||
|
visitors: sessionviews,
|
||||||
|
},
|
||||||
|
totals: {
|
||||||
|
views: events.filter(e => !e.eventName).length,
|
||||||
|
visitors: uniques.size,
|
||||||
|
events: events.filter(e => e.eventName).length,
|
||||||
|
countries: Object.keys(sessionStats.countries).length,
|
||||||
|
},
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -312,7 +312,7 @@ async function clickhouseQuery(
|
|||||||
const where = getWhere(urls, events, eventData);
|
const where = getWhere(urls, events, eventData);
|
||||||
|
|
||||||
const urlResults = hasUrl
|
const urlResults = hasUrl
|
||||||
? await rawQuery<any>(
|
? await rawQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
${columns.url}
|
${columns.url}
|
||||||
@ -332,7 +332,7 @@ async function clickhouseQuery(
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
const eventResults = hasEvent
|
const eventResults = hasEvent
|
||||||
? await rawQuery<any>(
|
? await rawQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
${columns.events}
|
${columns.events}
|
||||||
@ -352,7 +352,7 @@ async function clickhouseQuery(
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
const eventDataResults = hasEventData
|
const eventDataResults = hasEventData
|
||||||
? await rawQuery<any>(
|
? await rawQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
${columns.eventData}
|
${columns.eventData}
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
import clickhouse from 'lib/clickhouse';
|
import clickhouse from 'lib/clickhouse';
|
||||||
import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db';
|
import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db';
|
||||||
|
import { QueryFilters } from 'lib/types';
|
||||||
|
|
||||||
export async function getSessions(...args: [websiteId: string, startAt: Date]) {
|
export async function getSessions(...args: [websiteId: string, filters: QueryFilters]) {
|
||||||
return runQuery({
|
return runQuery({
|
||||||
[PRISMA]: () => relationalQuery(...args),
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function relationalQuery(websiteId: string, startDate: Date) {
|
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||||
|
const { startDate } = filters;
|
||||||
|
|
||||||
return prisma.client.session.findMany({
|
return prisma.client.session.findMany({
|
||||||
where: {
|
where: {
|
||||||
websiteId,
|
websiteId,
|
||||||
@ -18,13 +21,14 @@ async function relationalQuery(websiteId: string, startDate: Date) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'asc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clickhouseQuery(websiteId: string, startDate: Date) {
|
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||||
const { rawQuery } = clickhouse;
|
const { rawQuery } = clickhouse;
|
||||||
|
const { startDate } = filters;
|
||||||
|
|
||||||
return rawQuery(
|
return rawQuery(
|
||||||
`
|
`
|
||||||
@ -46,7 +50,7 @@ async function clickhouseQuery(websiteId: string, startDate: Date) {
|
|||||||
from website_event
|
from website_event
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
and created_at >= {startDate:DateTime64}
|
and created_at >= {startDate:DateTime64}
|
||||||
order by created_at asc
|
order by created_at desc
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
websiteId,
|
websiteId,
|
||||||
|
Loading…
Reference in New Issue
Block a user