Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	src/queries/analytics/getWebsiteStats.ts
This commit is contained in:
Mike Cao 2024-08-19 10:55:39 -07:00
commit 07576bef61
11 changed files with 233 additions and 52 deletions

View File

@ -1,14 +1,14 @@
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
import { Flexbox } from 'react-basics';
import MetricsBar from 'components/metrics/MetricsBar';
import MetricCard from 'components/metrics/MetricCard';
import { useMessages } from 'components/hooks';
import useWebsiteStats from 'components/hooks/queries/useWebsiteStats';
import useWebsiteSessionStats from 'components/hooks/queries/useWebsiteSessionStats';
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
import MetricCard from 'components/metrics/MetricCard';
import MetricsBar from 'components/metrics/MetricsBar';
import { formatLongNumber } from 'lib/format';
import { Flexbox } from 'react-basics';
export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
const { data, isLoading, isFetched, error } = useWebsiteStats(websiteId);
const { data, isLoading, isFetched, error } = useWebsiteSessionStats(websiteId);
return (
<Flexbox direction="row" justifyContent="space-between" style={{ minHeight: 120 }}>
@ -28,6 +28,11 @@ export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
label={formatMessage(labels.views)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.events?.value}
label={formatMessage(labels.events)}
formatValue={formatLongNumber}
/>
</MetricsBar>
<WebsiteDateFilter websiteId={websiteId} />
</Flexbox>

View File

@ -1,14 +1,14 @@
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
import { Flexbox } from 'react-basics';
import MetricsBar from 'components/metrics/MetricsBar';
import MetricCard from 'components/metrics/MetricCard';
import { useMessages } from 'components/hooks';
import useWebsiteStats from 'components/hooks/queries/useWebsiteStats';
import useWebsiteSessionStats from 'components/hooks/queries/useWebsiteSessionStats';
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
import MetricCard from 'components/metrics/MetricCard';
import MetricsBar from 'components/metrics/MetricsBar';
import { formatLongNumber } from 'lib/format';
import { Flexbox } from 'react-basics';
export function SessionsMetricsBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
const { data, isLoading, isFetched, error } = useWebsiteStats(websiteId);
const { data, isLoading, isFetched, error } = useWebsiteSessionStats(websiteId);
return (
<Flexbox direction="row" justifyContent="space-between" style={{ minHeight: 120 }}>

View File

@ -0,0 +1,16 @@
import { useApi } from './useApi';
import { useFilterParams } from '../useFilterParams';
export function useWebsiteSessionStats(websiteId: string, options?: { [key: string]: string }) {
const { get, useQuery } = useApi();
const params = useFilterParams(websiteId);
return useQuery({
queryKey: ['sessions:stats', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/sessions/stats`, { ...params }),
enabled: !!websiteId,
...options,
});
}
export default useWebsiteSessionStats;

View File

@ -0,0 +1,84 @@
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { getRequestDateRange, getRequestFilters } from 'lib/request';
import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getWebsiteSessionStats } from 'queries/analytics/sessions/getWebsiteSessionStats';
import * as yup from 'yup';
export interface WebsiteSessionStatsRequestQuery {
websiteId: string;
startAt: number;
endAt: number;
url?: string;
referrer?: string;
title?: string;
query?: string;
event?: string;
host?: string;
os?: string;
browser?: string;
device?: string;
country?: string;
region?: string;
city?: string;
}
const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().required(),
endAt: yup.number().required(),
url: yup.string(),
referrer: yup.string(),
title: yup.string(),
query: yup.string(),
event: yup.string(),
host: yup.string(),
os: yup.string(),
browser: yup.string(),
device: yup.string(),
country: yup.string(),
region: yup.string(),
city: yup.string(),
}),
};
export default async (
req: NextApiRequestQueryBody<WebsiteSessionStatsRequestQuery>,
res: NextApiResponse<WebsiteStats>,
) => {
await useCors(req, res);
await useAuth(req, res);
await useValidate(schema, req, res);
const { websiteId } = req.query;
if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) {
return unauthorized(res);
}
const { startDate, endDate } = await getRequestDateRange(req);
const filters = getRequestFilters(req);
const metrics = await getWebsiteSessionStats(websiteId, {
...filters,
startDate,
endDate,
});
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
obj[key] = {
value: Number(metrics[0][key]) || 0,
};
return obj;
}, {});
return ok(res, stats);
}
return methodNotAllowed(res);
};

View File

@ -24,14 +24,15 @@ async function relationalQuery(
return rawQuery(
`
select
event_name as "eventName",
data_key as "propertyName",
we.event_name as "eventName",
ed.data_key as "propertyName",
count(*) as "total"
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
from event_data ed
join website_event we on we.event_id = ed.website_event_id
where ed.website_id = {{websiteId::uuid}}
and ed.created_at between {{startDate}} and {{endDate}}
${filterQuery}
group by event_name, data_key
group by we.event_name, ed.data_key
order by 3 desc
limit 500
`,

View File

@ -1,4 +1,5 @@
import clickhouse from 'lib/clickhouse';
import { EVENT_TYPE } from 'lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma';
import { QueryFilters } from 'lib/types';
@ -22,13 +23,10 @@ async function relationalQuery(
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> {
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(
websiteId,
{
...filters,
},
{ joinSession: true },
);
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
return rawQuery(
`
@ -36,14 +34,12 @@ async function relationalQuery(
sum(t.c) as "pageviews",
count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits",
count(distinct t.country) as "countries",
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from (
select
website_event.session_id,
website_event.visit_id,
session.country,
count(*) as "c",
min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time"
@ -51,8 +47,9 @@ async function relationalQuery(
${joinSession}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
${filterQuery}
group by 1, 2, 3
group by 1, 2
) as t
`,
params,
@ -68,6 +65,7 @@ async function clickhouseQuery(
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.pageView,
});
let sql = '';
@ -78,22 +76,21 @@ async function clickhouseQuery(
sum(t.c) as "pageviews",
uniq(t.session_id) as "visitors",
uniq(t.visit_id) as "visits",
uniq(t.country) as "countries",
sumIf(1, t.c = 1) as "bounces",
sum(if(t.c = 1, 1, 0)) as "bounces",
sum(max_time-min_time) as "totaltime"
from (
select
session_id,
visit_id,
country,
count(*) c,
min(created_at) min_time,
max(created_at) max_time
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
${filterQuery}
group by session_id, visit_id, country
group by session_id, visit_id
) as t;
`;
} else {
@ -102,22 +99,20 @@ async function clickhouseQuery(
sum(t.c) as "pageviews",
uniq(session_id) as "visitors",
uniq(visit_id) as "visits",
uniq(country) as "countries",
sumIf(1, t.c = 1) as "bounces",
sum(max_time-min_time) as "totaltime"
from (
select
session_id,
visit_id,
country,
sum(views) c,
min(min_time) min_time,
max(max_time) max_time
from umami.website_event_stats_hourly "website_event"
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
group by session_id, visit_id, country
from (select
session_id,
visit_id,
sum(views) c,
min(min_time) min_time,
max(max_time) max_time
from umami.website_event_stats_hourly "website_event"
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
${filterQuery}
group by session_id, visit_id
) as t;
`;
}
@ -130,8 +125,6 @@ async function clickhouseQuery(
visits: Number(a.visits),
bounces: Number(a.bounces),
totaltime: Number(a.totaltime),
countries: Number(a.countries),
events: Number(a.events),
};
});
});

View File

@ -32,7 +32,7 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
number_value as numberValue,
date_value as dateValue,
created_at as createdAt
from session_data
from session_data final
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
order by data_key asc

View File

@ -52,7 +52,7 @@ async function clickhouseQuery(
select
data_key as propertyName,
count(*) as total
from session_data
from session_data final
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}

View File

@ -51,7 +51,7 @@ async function clickhouseQuery(
data_type = 4, toString(date_trunc('hour', date_value)),
string_value) as "value",
count(*) as "total"
from session_data
from session_data final
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and data_key = {propertyName:String}

View File

@ -0,0 +1,82 @@
import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma';
import { QueryFilters } from 'lib/types';
export async function getWebsiteSessionStats(
...args: [websiteId: string, filters: QueryFilters]
): Promise<
{ pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
filters: QueryFilters,
): Promise<
{ pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
> {
const { parseFilters, rawQuery } = prisma;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
});
return rawQuery(
`
select
count(*) as "pageviews",
count(distinct website_event.session_id) as "visitors",
count(distinct website_event.visit_id) as "visits",
count(distinct session.country) as "countries",
sum(case when website_event.event_type = 2 then 1 else 0 end) as "events"
from website_event
join session on website_event.session_id = session.session_id
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${filterQuery}
`,
params,
);
}
async function clickhouseQuery(
websiteId: string,
filters: QueryFilters,
): Promise<
{ pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
});
return rawQuery(
`
select
sum(views) as "pageviews",
uniq(session_id) as "visitors",
uniq(visit_id) as "visits",
uniq(country) as "countries",
sum(length(event_name)) as "events"
from umami.website_event_stats_hourly "website_event"
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
`,
params,
).then(result => {
return Object.values(result).map((a: any) => {
return {
pageviews: Number(a.pageviews),
visitors: Number(a.visitors),
visits: Number(a.visits),
countries: Number(a.countries),
events: Number(a.events),
};
});
});
}

View File

@ -21,7 +21,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
select
${getDateWeeklySQL('created_at')} as time,
count(distinct session_id) as value
from website_event_stats_hourly
from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
group by time