mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-05 08:37:30 +01:00
Merge remote-tracking branch 'origin/dev' into dev
# Conflicts: # src/queries/analytics/getWebsiteStats.ts
This commit is contained in:
commit
07576bef61
@ -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>
|
||||
|
@ -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 }}>
|
||||
|
16
src/components/hooks/queries/useWebsiteSessionStats.ts
Normal file
16
src/components/hooks/queries/useWebsiteSessionStats.ts
Normal 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;
|
84
src/pages/api/websites/[websiteId]/sessions/stats.ts
Normal file
84
src/pages/api/websites/[websiteId]/sessions/stats.ts
Normal 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);
|
||||
};
|
@ -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
|
||||
`,
|
||||
|
@ -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,
|
||||
{
|
||||
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
},
|
||||
{ joinSession: true },
|
||||
);
|
||||
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
|
||||
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}
|
||||
and event_type = {eventType:UInt32}
|
||||
${filterQuery}
|
||||
group by session_id, visit_id, country
|
||||
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),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
82
src/queries/analytics/sessions/getWebsiteSessionStats.ts
Normal file
82
src/queries/analytics/sessions/getWebsiteSessionStats.ts
Normal 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),
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user