From 439377f5c526f84916740bc6b2b25a1151c9fe38 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 15 Aug 2024 12:04:48 -0700 Subject: [PATCH 1/3] add final keyword to session data queries --- src/queries/analytics/sessions/getSessionData.ts | 2 +- src/queries/analytics/sessions/getSessionDataProperties.ts | 2 +- src/queries/analytics/sessions/getSessionDataValues.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/queries/analytics/sessions/getSessionData.ts b/src/queries/analytics/sessions/getSessionData.ts index 8b78fc86..509fcdbf 100644 --- a/src/queries/analytics/sessions/getSessionData.ts +++ b/src/queries/analytics/sessions/getSessionData.ts @@ -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 diff --git a/src/queries/analytics/sessions/getSessionDataProperties.ts b/src/queries/analytics/sessions/getSessionDataProperties.ts index bfbd9246..b33fecfb 100644 --- a/src/queries/analytics/sessions/getSessionDataProperties.ts +++ b/src/queries/analytics/sessions/getSessionDataProperties.ts @@ -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} diff --git a/src/queries/analytics/sessions/getSessionDataValues.ts b/src/queries/analytics/sessions/getSessionDataValues.ts index fd67e7a9..c2fa5f66 100644 --- a/src/queries/analytics/sessions/getSessionDataValues.ts +++ b/src/queries/analytics/sessions/getSessionDataValues.ts @@ -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} From a058552d3963203908e78a609366d4ada98721c0 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 19 Aug 2024 09:35:22 -0700 Subject: [PATCH 2/3] add sessionstats query for performance, countries, events values --- .../[websiteId]/events/EventsMetricsBar.tsx | 17 ++-- .../sessions/SessionsMetricsBar.tsx | 12 +-- .../hooks/queries/useWebsiteSessionStats.ts | 16 ++++ .../websites/[websiteId]/sessions/stats.ts | 84 +++++++++++++++++++ src/queries/analytics/getWebsiteStats.ts | 45 +++++----- .../sessions/getWebsiteSessionStats.ts | 82 ++++++++++++++++++ 6 files changed, 220 insertions(+), 36 deletions(-) create mode 100644 src/components/hooks/queries/useWebsiteSessionStats.ts create mode 100644 src/pages/api/websites/[websiteId]/sessions/stats.ts create mode 100644 src/queries/analytics/sessions/getWebsiteSessionStats.ts diff --git a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx index 0a054e8d..d039b67f 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx @@ -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 ( @@ -28,6 +28,11 @@ export function EventsMetricsBar({ websiteId }: { websiteId: string }) { label={formatMessage(labels.views)} formatValue={formatLongNumber} /> + diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx index 9133ca71..803e7a06 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx @@ -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 ( diff --git a/src/components/hooks/queries/useWebsiteSessionStats.ts b/src/components/hooks/queries/useWebsiteSessionStats.ts new file mode 100644 index 00000000..7671b2eb --- /dev/null +++ b/src/components/hooks/queries/useWebsiteSessionStats.ts @@ -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; diff --git a/src/pages/api/websites/[websiteId]/sessions/stats.ts b/src/pages/api/websites/[websiteId]/sessions/stats.ts new file mode 100644 index 00000000..a522bd6b --- /dev/null +++ b/src/pages/api/websites/[websiteId]/sessions/stats.ts @@ -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, + res: NextApiResponse, +) => { + 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); +}; diff --git a/src/queries/analytics/getWebsiteStats.ts b/src/queries/analytics/getWebsiteStats.ts index c35aea06..c5141d3b 100644 --- a/src/queries/analytics/getWebsiteStats.ts +++ b/src/queries/analytics/getWebsiteStats.ts @@ -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'; @@ -24,6 +25,7 @@ async function relationalQuery( const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma; const { filterQuery, joinSession, params } = await parseFilters(websiteId, { ...filters, + eventType: EVENT_TYPE.pageView, }); return rawQuery( @@ -32,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", + 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" @@ -47,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, @@ -64,6 +65,7 @@ async function clickhouseQuery( const { rawQuery, parseFilters } = clickhouse; const { filterQuery, params } = await parseFilters(websiteId, { ...filters, + eventType: EVENT_TYPE.pageView, }); let sql = ''; @@ -74,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 { @@ -98,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; `; } @@ -126,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), }; }); }); diff --git a/src/queries/analytics/sessions/getWebsiteSessionStats.ts b/src/queries/analytics/sessions/getWebsiteSessionStats.ts new file mode 100644 index 00000000..a3130c7c --- /dev/null +++ b/src/queries/analytics/sessions/getWebsiteSessionStats.ts @@ -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 t.session_id) as "visitors", + count(distinct t.visit_id) as "visits", + count(distinct t.country) as "countries", + sum(case when 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), + }; + }); + }); +} From 7ae3c790cd290b377238a66e68a3f58afc4df154 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 19 Aug 2024 10:28:10 -0700 Subject: [PATCH 3/3] fix relational queries for new screens --- .../analytics/events/getEventDataProperties.ts | 13 +++++++------ .../analytics/sessions/getWebsiteSessionStats.ts | 8 ++++---- .../analytics/sessions/getWebsiteSessionsWeekly.ts | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/queries/analytics/events/getEventDataProperties.ts b/src/queries/analytics/events/getEventDataProperties.ts index 8d1eaf9d..9d55f896 100644 --- a/src/queries/analytics/events/getEventDataProperties.ts +++ b/src/queries/analytics/events/getEventDataProperties.ts @@ -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 `, diff --git a/src/queries/analytics/sessions/getWebsiteSessionStats.ts b/src/queries/analytics/sessions/getWebsiteSessionStats.ts index a3130c7c..5660a5fa 100644 --- a/src/queries/analytics/sessions/getWebsiteSessionStats.ts +++ b/src/queries/analytics/sessions/getWebsiteSessionStats.ts @@ -29,10 +29,10 @@ async function relationalQuery( ` select count(*) 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 event_type = 2 then 1 else 0 end) as "events" + 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}} diff --git a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts b/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts index 9031edf5..c92c2929 100644 --- a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts +++ b/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts @@ -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