mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-15 09:45:04 +01:00
Fixed realtime chart display.
This commit is contained in:
parent
93b77672f3
commit
802c262cd9
@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { format, parseISO, startOfMinute, subMinutes, isBefore } from 'date-fns';
|
import { format, 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';
|
||||||
@ -8,13 +8,12 @@ function mapData(data) {
|
|||||||
let last = 0;
|
let last = 0;
|
||||||
const arr = [];
|
const arr = [];
|
||||||
|
|
||||||
data.reduce((obj, val) => {
|
data.reduce((obj, { timestamp }) => {
|
||||||
const { createdAt } = val;
|
const t = startOfMinute(new Date(timestamp));
|
||||||
const t = startOfMinute(parseISO(createdAt));
|
|
||||||
if (t.getTime() > last) {
|
if (t.getTime() > last) {
|
||||||
obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 };
|
obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 };
|
||||||
arr.push(obj);
|
arr.push(obj);
|
||||||
last = t;
|
last = t.getTime();
|
||||||
} else {
|
} else {
|
||||||
obj.y += 1;
|
obj.y += 1;
|
||||||
}
|
}
|
||||||
@ -30,14 +29,15 @@ export default function RealtimeChart({ data, unit, ...props }) {
|
|||||||
const prevEndDate = useRef(endDate);
|
const prevEndDate = useRef(endDate);
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
if (data) {
|
if (!data) {
|
||||||
return {
|
return { pageviews: [], sessions: [] };
|
||||||
pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit),
|
|
||||||
sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return { pageviews: [], sessions: [] };
|
|
||||||
}, [data]);
|
return {
|
||||||
|
pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit),
|
||||||
|
sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit),
|
||||||
|
};
|
||||||
|
}, [data, startDate, endDate, unit]);
|
||||||
|
|
||||||
// Don't animate the bars shifting over because it looks weird
|
// Don't animate the bars shifting over because it looks weird
|
||||||
const animationDuration = useMemo(() => {
|
const animationDuration = useMemo(() => {
|
||||||
@ -46,7 +46,7 @@ export default function RealtimeChart({ data, unit, ...props }) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return DEFAULT_ANIMATION_DURATION;
|
return DEFAULT_ANIMATION_DURATION;
|
||||||
}, [data]);
|
}, [data, endDate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageviewsChart
|
<PageviewsChart
|
||||||
|
26
components/pages/realtime/RealtimeCountries.js
Normal file
26
components/pages/realtime/RealtimeCountries.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
|
import DataTable from 'components/metrics/DataTable';
|
||||||
|
import useLocale from 'hooks/useLocale';
|
||||||
|
import useCountryNames from 'hooks/useCountryNames';
|
||||||
|
|
||||||
|
export default function RealtimeCountries({ data }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { locale } = useLocale();
|
||||||
|
const countryNames = useCountryNames(locale);
|
||||||
|
|
||||||
|
const renderCountryName = useCallback(
|
||||||
|
({ x }) => <span className={locale}>{countryNames[x]}</span>,
|
||||||
|
[countryNames, locale],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
title={formatMessage(labels.countries)}
|
||||||
|
metric={formatMessage(labels.visitors)}
|
||||||
|
data={data}
|
||||||
|
renderLabel={renderCountryName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,66 +1,57 @@
|
|||||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { subMinutes, startOfMinute, differenceInMinutes } from 'date-fns';
|
import { subMinutes, startOfMinute } from 'date-fns';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import firstBy from 'thenby';
|
import firstBy from 'thenby';
|
||||||
import { GridRow, GridColumn } from 'components/layout/Grid';
|
import { GridRow, GridColumn } 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';
|
||||||
import RealtimeLog from 'components/pages/realtime/RealtimeLog';
|
|
||||||
import RealtimeHeader from 'components/pages/realtime/RealtimeHeader';
|
|
||||||
import StickyHeader from 'components/helpers/StickyHeader';
|
import StickyHeader from 'components/helpers/StickyHeader';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import WorldMap from 'components/common/WorldMap';
|
import WorldMap from 'components/common/WorldMap';
|
||||||
import DataTable from 'components/metrics/DataTable';
|
import RealtimeLog from 'components/pages/realtime/RealtimeLog';
|
||||||
|
import RealtimeHeader from 'components/pages/realtime/RealtimeHeader';
|
||||||
import RealtimeUrls from 'components/pages/realtime/RealtimeUrls';
|
import RealtimeUrls from 'components/pages/realtime/RealtimeUrls';
|
||||||
|
import RealtimeCountries from 'components/pages/realtime/RealtimeCountries';
|
||||||
|
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import useLocale from 'hooks/useLocale';
|
|
||||||
import useCountryNames from 'hooks/useCountryNames';
|
|
||||||
import { percentFilter } from 'lib/filters';
|
import { percentFilter } from 'lib/filters';
|
||||||
import { labels } from 'components/messages';
|
import { labels } from 'components/messages';
|
||||||
import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
|
import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
|
||||||
import styles from './RealtimeDashboard.module.css';
|
import styles from './RealtimeDashboard.module.css';
|
||||||
import WebsiteSelect from '../../input/WebsiteSelect';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
|
|
||||||
function mergeData(state = [], data, time) {
|
function mergeData(state = [], data = [], time) {
|
||||||
const ids = state.map(({ __id }) => __id);
|
const ids = state.map(({ __id }) => __id);
|
||||||
return state
|
return state
|
||||||
.concat(data.filter(({ __id }) => !ids.includes(__id)))
|
.concat(data.filter(({ __id }) => !ids.includes(__id)))
|
||||||
.filter(({ createdAt }) => new Date(createdAt).getTime() >= time);
|
.filter(({ timestamp }) => timestamp >= time);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RealtimeDashboard({ websiteId }) {
|
export default function RealtimeDashboard({ websiteId }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { locale } = useLocale();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const countryNames = useCountryNames(locale);
|
|
||||||
const [currentData, setCurrentData] = useState();
|
const [currentData, setCurrentData] = useState();
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data, isLoading, error } = useQuery(
|
const { data, isLoading, error } = useQuery(
|
||||||
['realtime', websiteId],
|
['realtime', websiteId],
|
||||||
() => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp }),
|
() => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }),
|
||||||
{
|
{
|
||||||
enabled: !!websiteId,
|
enabled: !!websiteId,
|
||||||
retryInterval: REALTIME_INTERVAL,
|
refetchInterval: REALTIME_INTERVAL,
|
||||||
|
cache: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderCountryName = useCallback(
|
|
||||||
({ x }) => <span className={locale}>{countryNames[x]}</span>,
|
|
||||||
[countryNames],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const { pageviews, sessions, events, timestamp } = data;
|
const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
|
||||||
const time = subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime();
|
const time = date.getTime();
|
||||||
|
|
||||||
setCurrentData(state => ({
|
setCurrentData(state => ({
|
||||||
...state,
|
pageviews: mergeData(state?.pageviews, data.pageviews, time),
|
||||||
pageviews: mergeData(state?.pageviews, pageviews, time),
|
sessions: mergeData(state?.sessions, data.sessions, time),
|
||||||
sessions: mergeData(state?.sessions, sessions, time),
|
events: mergeData(state?.events, data.events, time),
|
||||||
events: mergeData(state?.events, events, time),
|
timestamp: data.timestamp,
|
||||||
timestamp,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@ -72,6 +63,12 @@ export default function RealtimeDashboard({ websiteId }) {
|
|||||||
|
|
||||||
currentData.countries = percentFilter(
|
currentData.countries = percentFilter(
|
||||||
currentData.sessions
|
currentData.sessions
|
||||||
|
.reduce((arr, data) => {
|
||||||
|
if (!arr.find(({ sessionId }) => sessionId === data.sessionId)) {
|
||||||
|
return arr.concat(data);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [])
|
||||||
.reduce((arr, { country }) => {
|
.reduce((arr, { country }) => {
|
||||||
if (country) {
|
if (country) {
|
||||||
const row = arr.find(({ x }) => x === country);
|
const row = arr.find(({ x }) => x === country);
|
||||||
@ -115,12 +112,7 @@ export default function RealtimeDashboard({ websiteId }) {
|
|||||||
</GridRow>
|
</GridRow>
|
||||||
<GridRow>
|
<GridRow>
|
||||||
<GridColumn xs={12} lg={4}>
|
<GridColumn xs={12} lg={4}>
|
||||||
<DataTable
|
<RealtimeCountries data={realtimeData?.countries} />
|
||||||
title={formatMessage(labels.countries)}
|
|
||||||
metric={formatMessage(labels.visitors)}
|
|
||||||
data={realtimeData?.countries}
|
|
||||||
renderLabel={renderCountryName}
|
|
||||||
/>
|
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
<GridColumn xs={12} lg={8}>
|
<GridColumn xs={12} lg={8}>
|
||||||
<WorldMap data={realtimeData?.countries} />
|
<WorldMap data={realtimeData?.countries} />
|
||||||
|
@ -7,13 +7,20 @@ export default function RealtimeHeader({ data = {} }) {
|
|||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { pageviews, sessions, events, countries } = data;
|
const { pageviews, sessions, events, countries } = data;
|
||||||
|
|
||||||
|
const visitors = sessions?.reduce((arr, { sessionId }) => {
|
||||||
|
if (sessionId && !arr.includes(sessionId)) {
|
||||||
|
return arr.concat(sessionId);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className={styles.metrics}>
|
<div className={styles.metrics}>
|
||||||
<MetricCard label={formatMessage(labels.views)} value={pageviews?.length} hideComparison />
|
<MetricCard label={formatMessage(labels.views)} value={pageviews?.length} hideComparison />
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label={formatMessage(labels.visitors)}
|
label={formatMessage(labels.visitors)}
|
||||||
value={sessions?.length}
|
value={visitors?.length}
|
||||||
hideComparison
|
hideComparison
|
||||||
/>
|
/>
|
||||||
<MetricCard label={formatMessage(labels.events)} value={events?.length} hideComparison />
|
<MetricCard label={formatMessage(labels.events)} value={events?.length} hideComparison />
|
||||||
|
@ -21,7 +21,7 @@ export const DEFAULT_DATE_RANGE = '24hour';
|
|||||||
export const DEFAULT_WEBSITE_LIMIT = 10;
|
export const DEFAULT_WEBSITE_LIMIT = 10;
|
||||||
|
|
||||||
export const REALTIME_RANGE = 30;
|
export const REALTIME_RANGE = 30;
|
||||||
export const REALTIME_INTERVAL = 3000;
|
export const REALTIME_INTERVAL = 5000;
|
||||||
|
|
||||||
export const UI_LAYOUT_BODY = 'ui-layout-body';
|
export const UI_LAYOUT_BODY = 'ui-layout-body';
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
import { v4, v5 } from 'uuid';
|
import { v4, v5 } from 'uuid';
|
||||||
import { startOfMonth } from 'date-fns';
|
import { startOfMonth } from 'date-fns';
|
||||||
import { hash } from 'next-basics';
|
import { hash } from 'next-basics';
|
||||||
@ -17,3 +18,7 @@ export function uuid(...args) {
|
|||||||
|
|
||||||
return v5(hash(...args, salt()), v5.DNS);
|
return v5(hash(...args, salt()), v5.DNS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function md5(...args) {
|
||||||
|
return crypto.createHash('md5').update(args.join('')).digest('hex');
|
||||||
|
}
|
||||||
|
@ -14,6 +14,7 @@ import '@fontsource/inter/600.css';
|
|||||||
const client = new QueryClient({
|
const client = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
|
retry: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -9,9 +9,14 @@ export default async (req: NextApiRequestAuth, res: NextApiResponse<RealtimeInit
|
|||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const { id } = req.query;
|
const { id, startAt } = req.query;
|
||||||
|
let startTime = subMinutes(new Date(), 30);
|
||||||
|
|
||||||
const data = await getRealtimeData(id, subMinutes(new Date(), 30));
|
if (+startAt > startTime.getTime()) {
|
||||||
|
startTime = new Date(+startAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getRealtimeData(id, startTime);
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, data);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ export default function RealtimeDetailsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<RealtimeDashboard websiteId={websiteId} />
|
<RealtimeDashboard key={websiteId} websiteId={websiteId} />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -26,12 +26,13 @@ function clickhouseQuery(websiteId: string, startAt: Date) {
|
|||||||
|
|
||||||
return rawQuery(
|
return rawQuery(
|
||||||
`select
|
`select
|
||||||
event_id,
|
event_id as id,
|
||||||
website_id,
|
website_id as websiteId,
|
||||||
session_id,
|
session_id as sessionId,
|
||||||
created_at,
|
created_at as createdAt,
|
||||||
|
toUnixTimestamp(created_at) as timestamp,
|
||||||
url,
|
url,
|
||||||
event_name
|
event_name as eventName
|
||||||
from event
|
from event
|
||||||
where event_type = ${EVENT_TYPE.customEvent}
|
where event_type = ${EVENT_TYPE.customEvent}
|
||||||
and website_id = {websiteId:UUID}
|
and website_id = {websiteId:UUID}
|
||||||
|
@ -29,6 +29,7 @@ async function clickhouseQuery(websiteId: string, startAt: Date) {
|
|||||||
website_id as websiteId,
|
website_id as websiteId,
|
||||||
session_id as sessionId,
|
session_id as sessionId,
|
||||||
created_at as createdAt,
|
created_at as createdAt,
|
||||||
|
toUnixTimestamp(created_at) as timestamp,
|
||||||
url
|
url
|
||||||
from event
|
from event
|
||||||
where event_type = ${EVENT_TYPE.pageView}
|
where event_type = ${EVENT_TYPE.pageView}
|
||||||
|
@ -25,9 +25,10 @@ async function clickhouseQuery(websiteId: string, startAt: Date) {
|
|||||||
|
|
||||||
return rawQuery(
|
return rawQuery(
|
||||||
`select distinct
|
`select distinct
|
||||||
session_id,
|
session_id as sessionId,
|
||||||
website_id,
|
website_id as websiteId,
|
||||||
created_at,
|
created_at as createdAt,
|
||||||
|
toUnixTimestamp(created_at) as timestamp,
|
||||||
hostname,
|
hostname,
|
||||||
browser,
|
browser,
|
||||||
os,
|
os,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { md5 } from 'lib/crypto';
|
||||||
import { getPageviews } from '../pageview/getPageviews';
|
import { getPageviews } from '../pageview/getPageviews';
|
||||||
import { getSessions } from '../session/getSessions';
|
import { getSessions } from '../session/getSessions';
|
||||||
import { getEvents } from '../event/getEvents';
|
import { getEvents } from '../event/getEvents';
|
||||||
@ -9,22 +10,19 @@ export async function getRealtimeData(websiteId, time) {
|
|||||||
getEvents(websiteId, time),
|
getEvents(websiteId, time),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const decorate = (id, data) => {
|
||||||
|
return data.map(props => ({
|
||||||
|
...props,
|
||||||
|
__id: md5(id, ...Object.values(props)),
|
||||||
|
timestamp: props.timestamp * 1000,
|
||||||
|
timestampCompare: new Date(props.createdAt).getTime(),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageviews: pageviews.map(({ id, ...props }) => ({
|
pageviews: decorate('pageviews', pageviews),
|
||||||
__id: `p${id}`,
|
sessions: decorate('sessions', sessions),
|
||||||
pageviewId: id,
|
events: decorate('events', events),
|
||||||
...props,
|
|
||||||
})),
|
|
||||||
sessions: sessions.map(({ id, ...props }) => ({
|
|
||||||
__id: `s${id}`,
|
|
||||||
sessionId: id,
|
|
||||||
...props,
|
|
||||||
})),
|
|
||||||
events: events.map(({ id, ...props }) => ({
|
|
||||||
__id: `e${id}`,
|
|
||||||
eventId: id,
|
|
||||||
...props,
|
|
||||||
})),
|
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console, @typescript-eslint/no-var-requires */
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
|
Loading…
Reference in New Issue
Block a user