mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-06 01:15:42 +01:00
Updated realtime data fetch.
This commit is contained in:
parent
04e0b33622
commit
e35c11c3d6
@ -10,7 +10,6 @@ import { safeDecodeURI } from 'next-basics';
|
|||||||
import { useContext, useMemo, useState } from 'react';
|
import { useContext, useMemo, useState } from 'react';
|
||||||
import { Icon, SearchField, StatusLight, Text } from 'react-basics';
|
import { Icon, SearchField, StatusLight, Text } from 'react-basics';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { FixedSizeList } from 'react-window';
|
||||||
import thenby from 'thenby';
|
|
||||||
import { WebsiteContext } from '../WebsiteProvider';
|
import { WebsiteContext } from '../WebsiteProvider';
|
||||||
import styles from './RealtimeLog.module.css';
|
import styles from './RealtimeLog.module.css';
|
||||||
|
|
||||||
@ -141,15 +140,7 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { events, visitors } = data;
|
let logs = data.events;
|
||||||
|
|
||||||
let logs = [
|
|
||||||
...events.map(e => ({
|
|
||||||
__type: e.eventName ? TYPE_EVENT : TYPE_PAGEVIEW,
|
|
||||||
...e,
|
|
||||||
})),
|
|
||||||
...visitors.map(v => ({ __type: TYPE_SESSION, ...v })),
|
|
||||||
].sort(thenby.firstBy('createdAt', -1));
|
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
logs = logs.filter(({ eventName, urlPath, browser, os, country, device }) => {
|
logs = logs.filter(({ eventName, urlPath, browser, os, country, device }) => {
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import { RealtimeData } from 'lib/types';
|
import { RealtimeData } from 'lib/types';
|
||||||
import { useApi } from './useApi';
|
import { useApi } from './useApi';
|
||||||
import { REALTIME_INTERVAL } from 'lib/constants';
|
import { REALTIME_INTERVAL } from 'lib/constants';
|
||||||
import { useTimezone } from '../useTimezone';
|
|
||||||
|
|
||||||
export function useRealtime(websiteId: string) {
|
export function useRealtime(websiteId: string) {
|
||||||
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 () => {
|
||||||
return get(`/realtime/${websiteId}`, { timezone });
|
return get(`/realtime/${websiteId}`);
|
||||||
},
|
},
|
||||||
enabled: !!websiteId,
|
enabled: !!websiteId,
|
||||||
refetchInterval: REALTIME_INTERVAL,
|
refetchInterval: REALTIME_INTERVAL,
|
||||||
|
@ -187,7 +187,10 @@ async function rawQuery<T = unknown>(
|
|||||||
query: query,
|
query: query,
|
||||||
query_params: params,
|
query_params: params,
|
||||||
format: 'JSONEachRow',
|
format: 'JSONEachRow',
|
||||||
clickhouse_settings: { output_format_json_quote_64bit_integers: 0 },
|
clickhouse_settings: {
|
||||||
|
date_time_output_format: 'iso',
|
||||||
|
output_format_json_quote_64bit_integers: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (await resultSet.json()) as T;
|
return (await resultSet.json()) as T;
|
||||||
|
@ -10,13 +10,11 @@ import { REALTIME_RANGE } from 'lib/constants';
|
|||||||
|
|
||||||
export interface RealtimeRequestQuery {
|
export interface RealtimeRequestQuery {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
timezone: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
GET: yup.object().shape({
|
GET: yup.object().shape({
|
||||||
websiteId: yup.string().uuid().required(),
|
websiteId: yup.string().uuid().required(),
|
||||||
timezone: yup.string().required(),
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,7 +23,7 @@ export default async (req: NextApiRequestQueryBody<RealtimeRequestQuery>, res: N
|
|||||||
await useValidate(schema, req, res);
|
await useValidate(schema, req, res);
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const { websiteId, timezone } = req.query;
|
const { websiteId } = req.query;
|
||||||
|
|
||||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
@ -33,7 +31,7 @@ export default async (req: NextApiRequestQueryBody<RealtimeRequestQuery>, res: N
|
|||||||
|
|
||||||
const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
|
const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
|
||||||
|
|
||||||
const data = await getRealtimeData(websiteId, { startDate, timezone });
|
const data = await getRealtimeData(websiteId, { startDate });
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, data);
|
||||||
}
|
}
|
||||||
|
67
src/queries/analytics/getRealtimeActivity.ts
Normal file
67
src/queries/analytics/getRealtimeActivity.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import prisma from 'lib/prisma';
|
||||||
|
import clickhouse from 'lib/clickhouse';
|
||||||
|
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
|
||||||
|
import { QueryFilters } from 'lib/types';
|
||||||
|
|
||||||
|
export async function getRealtimeActivity(...args: [websiteId: string, filters: QueryFilters]) {
|
||||||
|
return runQuery({
|
||||||
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||||
|
const { rawQuery, parseFilters } = prisma;
|
||||||
|
const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
|
||||||
|
|
||||||
|
return rawQuery(
|
||||||
|
`
|
||||||
|
select
|
||||||
|
website_event.session_id as sessionId,
|
||||||
|
website_event.event_name as eventName,
|
||||||
|
website_event.created_at as createdAt,
|
||||||
|
session.browser,
|
||||||
|
session.os,
|
||||||
|
session.device,
|
||||||
|
session.country,
|
||||||
|
session.url_path as urlPath,
|
||||||
|
session.referrer_domain as referrerDomain
|
||||||
|
from website_event
|
||||||
|
inner join session
|
||||||
|
on session.session_id = website_event.session_id
|
||||||
|
where website_id = {{websiteId::uuid}}
|
||||||
|
${filterQuery}
|
||||||
|
${dateQuery}
|
||||||
|
order by created_at asc
|
||||||
|
limit 100
|
||||||
|
`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promise<{ x: number }> {
|
||||||
|
const { rawQuery, parseFilters } = clickhouse;
|
||||||
|
const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
|
||||||
|
|
||||||
|
return rawQuery(
|
||||||
|
`
|
||||||
|
select
|
||||||
|
session_id as sessionId,
|
||||||
|
event_name as eventName,
|
||||||
|
created_at as createdAt,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
country,
|
||||||
|
url_path as urlPath,
|
||||||
|
referrer_domain as referrerDomain
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
${filterQuery}
|
||||||
|
${dateQuery}
|
||||||
|
order by createdAt asc
|
||||||
|
limit 100
|
||||||
|
`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
}
|
@ -1,11 +1,4 @@
|
|||||||
import {
|
import { getRealtimeActivity, getPageviewStats, getSessionStats } from 'queries/index';
|
||||||
getWebsiteSessions,
|
|
||||||
getWebsiteEvents,
|
|
||||||
getPageviewStats,
|
|
||||||
getSessionStats,
|
|
||||||
} from 'queries/index';
|
|
||||||
|
|
||||||
const MAX_SIZE = 50;
|
|
||||||
|
|
||||||
function increment(data: object, key: string) {
|
function increment(data: object, key: string) {
|
||||||
if (key) {
|
if (key) {
|
||||||
@ -17,61 +10,47 @@ function increment(data: object, key: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRealtimeData(
|
export async function getRealtimeData(websiteId: string, criteria: { startDate: Date }) {
|
||||||
websiteId: string,
|
const { startDate } = criteria;
|
||||||
criteria: { startDate: Date; timezone: string },
|
const filters = { startDate, endDate: new Date(), unit: 'minute' };
|
||||||
) {
|
const [activity, pageviews, sessions] = await Promise.all([
|
||||||
const { startDate, timezone } = criteria;
|
getRealtimeActivity(websiteId, filters),
|
||||||
const filters = { startDate, endDate: new Date(), unit: 'minute', timezone };
|
|
||||||
const [events, sessions, pageviews, sessionviews] = await Promise.all([
|
|
||||||
getWebsiteEvents(websiteId, { startDate, timezone }, { pageSize: 10000 }),
|
|
||||||
getWebsiteSessions(websiteId, { startDate, timezone }, { pageSize: 10000 }),
|
|
||||||
getPageviewStats(websiteId, filters),
|
getPageviewStats(websiteId, filters),
|
||||||
getSessionStats(websiteId, filters),
|
getSessionStats(websiteId, filters),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const uniques = new Set();
|
const uniques = new Set();
|
||||||
|
|
||||||
const sessionStats = sessions.data.reduce(
|
const { countries, urls, referrers, events } = activity.reduce(
|
||||||
(obj: { visitors: any; countries: any }, session: { id: any; country: any }) => {
|
(
|
||||||
const { countries, visitors } = obj;
|
obj: { countries: any; urls: any; referrers: any; events: any },
|
||||||
const { id, country } = session;
|
event: {
|
||||||
|
sessionId: string;
|
||||||
|
urlPath: string;
|
||||||
|
referrerDomain: string;
|
||||||
|
country: string;
|
||||||
|
eventName: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const { countries, urls, referrers, events } = obj;
|
||||||
|
const { sessionId, urlPath, referrerDomain, country, eventName } = event;
|
||||||
|
|
||||||
if (!uniques.has(id)) {
|
if (!uniques.has(sessionId)) {
|
||||||
uniques.add(id);
|
uniques.add(sessionId);
|
||||||
increment(countries, country);
|
increment(countries, country);
|
||||||
|
|
||||||
if (visitors.length < MAX_SIZE) {
|
events.push({ __type: 'session', ...event });
|
||||||
visitors.push(session);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
increment(urls, urlPath);
|
||||||
|
increment(referrers, referrerDomain);
|
||||||
|
|
||||||
|
events.push({ __type: eventName ? 'event' : 'pageview', ...event });
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
countries: {},
|
countries: {},
|
||||||
visitors: [],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const eventStats = events.data.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: {},
|
urls: {},
|
||||||
referrers: {},
|
referrers: {},
|
||||||
events: [],
|
events: [],
|
||||||
@ -79,17 +58,19 @@ export async function getRealtimeData(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...sessionStats,
|
countries,
|
||||||
...eventStats,
|
urls,
|
||||||
|
referrers,
|
||||||
|
events: events.reverse(),
|
||||||
series: {
|
series: {
|
||||||
views: pageviews,
|
views: pageviews,
|
||||||
visitors: sessionviews,
|
visitors: sessions,
|
||||||
},
|
},
|
||||||
totals: {
|
totals: {
|
||||||
views: events.data.filter(e => !e.eventName).length,
|
views: pageviews.reduce((sum: number, { y }: { y: number }) => sum + y, 0),
|
||||||
visitors: uniques.size,
|
visitors: sessions.reduce((sum: number, { y }: { y: number }) => sum + y, 0),
|
||||||
events: events.data.filter(e => e.eventName).length,
|
events: activity.filter(e => e.eventName).length,
|
||||||
countries: Object.keys(sessionStats.countries).length,
|
countries: Object.keys(countries).length,
|
||||||
},
|
},
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
|
@ -40,8 +40,8 @@ async function clickhouseQuery(
|
|||||||
websiteId: string,
|
websiteId: string,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
): Promise<{ x: string; y: number }[]> {
|
): Promise<{ x: string; y: number }[]> {
|
||||||
const { timezone = 'UTC', unit = 'day' } = filters;
|
const { unit = 'day' } = filters;
|
||||||
const { parseFilters, rawQuery, getDateStringSQL, getDateSQL } = clickhouse;
|
const { parseFilters, rawQuery } = clickhouse;
|
||||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||||
...filters,
|
...filters,
|
||||||
eventType: EVENT_TYPE.pageView,
|
eventType: EVENT_TYPE.pageView,
|
||||||
@ -52,11 +52,11 @@ async function clickhouseQuery(
|
|||||||
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item)) || unit === 'minute') {
|
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item)) || unit === 'minute') {
|
||||||
sql = `
|
sql = `
|
||||||
select
|
select
|
||||||
${getDateStringSQL('g.t', unit)} as x,
|
g.t as x,
|
||||||
g.y as y
|
g.y as y
|
||||||
from (
|
from (
|
||||||
select
|
select
|
||||||
${getDateSQL('created_at', unit, timezone)} as t,
|
date_trunc('${unit}', created_at) as t,
|
||||||
count(*) as y
|
count(*) as y
|
||||||
from website_event
|
from website_event
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
@ -70,11 +70,11 @@ async function clickhouseQuery(
|
|||||||
} else {
|
} else {
|
||||||
sql = `
|
sql = `
|
||||||
select
|
select
|
||||||
${getDateStringSQL('g.t', unit)} as x,
|
g.t as x,
|
||||||
g.y as y
|
g.y as y
|
||||||
from (
|
from (
|
||||||
select
|
select
|
||||||
${getDateSQL('created_at', unit, timezone)} as t,
|
date_trunc('${unit}', created_at) as t,
|
||||||
sum(views)as y
|
sum(views)as y
|
||||||
from website_event_stats_hourly website_event
|
from website_event_stats_hourly website_event
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
|
@ -40,8 +40,8 @@ async function clickhouseQuery(
|
|||||||
websiteId: string,
|
websiteId: string,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
): Promise<{ x: string; y: number }[]> {
|
): Promise<{ x: string; y: number }[]> {
|
||||||
const { timezone = 'UTC', unit = 'day' } = filters;
|
const { unit = 'day' } = filters;
|
||||||
const { parseFilters, rawQuery, getDateStringSQL, getDateSQL } = clickhouse;
|
const { parseFilters, rawQuery } = clickhouse;
|
||||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||||
...filters,
|
...filters,
|
||||||
eventType: EVENT_TYPE.pageView,
|
eventType: EVENT_TYPE.pageView,
|
||||||
@ -52,11 +52,11 @@ async function clickhouseQuery(
|
|||||||
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item)) || unit === 'minute') {
|
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item)) || unit === 'minute') {
|
||||||
sql = `
|
sql = `
|
||||||
select
|
select
|
||||||
${getDateStringSQL('g.t', unit)} as x,
|
g.t as x,
|
||||||
g.y as y
|
g.y as y
|
||||||
from (
|
from (
|
||||||
select
|
select
|
||||||
${getDateSQL('created_at', unit, timezone)} as t,
|
date_trunc('${unit}', created_at) as t,
|
||||||
count(distinct session_id) as y
|
count(distinct session_id) as y
|
||||||
from website_event
|
from website_event
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
@ -70,11 +70,11 @@ async function clickhouseQuery(
|
|||||||
} else {
|
} else {
|
||||||
sql = `
|
sql = `
|
||||||
select
|
select
|
||||||
${getDateStringSQL('g.t', unit)} as x,
|
g.t as x,
|
||||||
g.y as y
|
g.y as y
|
||||||
from (
|
from (
|
||||||
select
|
select
|
||||||
${getDateSQL('created_at', unit, timezone)} as t,
|
date_trunc('${unit}', created_at) as t,
|
||||||
uniq(session_id) as y
|
uniq(session_id) as y
|
||||||
from website_event_stats_hourly website_event
|
from website_event_stats_hourly website_event
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
|
@ -31,6 +31,7 @@ export * from './analytics/sessions/getSessionActivity';
|
|||||||
export * from './analytics/sessions/getSessionStats';
|
export * from './analytics/sessions/getSessionStats';
|
||||||
export * from './analytics/sessions/saveSessionData';
|
export * from './analytics/sessions/saveSessionData';
|
||||||
export * from './analytics/getActiveVisitors';
|
export * from './analytics/getActiveVisitors';
|
||||||
|
export * from './analytics/getRealtimeActivity';
|
||||||
export * from './analytics/getRealtimeData';
|
export * from './analytics/getRealtimeData';
|
||||||
export * from './analytics/getValues';
|
export * from './analytics/getValues';
|
||||||
export * from './analytics/getWebsiteDateRange';
|
export * from './analytics/getWebsiteDateRange';
|
||||||
|
Loading…
Reference in New Issue
Block a user