From 4497951000065e9cc57ac8556bcc5a1bf04087ea Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 2 Aug 2023 14:21:13 -0700 Subject: [PATCH] Split out session query. --- components/layout/AppLayout.js | 6 +- lib/clickhouse.ts | 7 +- lib/prisma.ts | 22 ++--- pages/api/websites/[id]/metrics.ts | 4 +- pages/api/websites/[id]/pageviews.ts | 6 +- .../analytics/pageviews/getPageviewMetrics.ts | 3 +- .../analytics/pageviews/getPageviewStats.ts | 41 ++++---- .../analytics/sessions/getSessionMetrics.ts | 8 +- queries/analytics/sessions/getSessionStats.ts | 98 +++++++++++++++++++ 9 files changed, 139 insertions(+), 56 deletions(-) create mode 100644 queries/analytics/sessions/getSessionStats.ts diff --git a/components/layout/AppLayout.js b/components/layout/AppLayout.js index 989128f9..7ab74351 100644 --- a/components/layout/AppLayout.js +++ b/components/layout/AppLayout.js @@ -2,9 +2,7 @@ import { Container } from 'react-basics'; import Head from 'next/head'; import NavBar from 'components/layout/NavBar'; import UpdateNotice from 'components/common/UpdateNotice'; -import useRequireLogin from 'hooks/useRequireLogin'; -import useConfig from 'hooks/useConfig'; -import { CURRENT_VERSION } from 'lib/constants'; +import { useRequireLogin, useConfig } from 'hooks'; import styles from './AppLayout.module.css'; export function AppLayout({ title, children }) { @@ -16,7 +14,7 @@ export function AppLayout({ title, children }) { } return ( -
+
{title ? `${title} | umami` : 'umami'} diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index b3dc2c48..d294110c 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -61,14 +61,13 @@ function getDateFormat(date) { return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`; } -function getFilterQuery(filters = {}, params = {}) { +function getFilterQuery(filters = {}) { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; if (filter !== undefined) { const column = FILTER_COLUMNS[key] || key; arr.push(`and ${column} = {${key}:String}`); - params[key] = decodeURIComponent(filter); } return arr; @@ -77,9 +76,9 @@ function getFilterQuery(filters = {}, params = {}) { return query.join('\n'); } -function parseFilters(filters: WebsiteMetricFilter = {}, params: any = {}) { +function parseFilters(filters: WebsiteMetricFilter = {}) { return { - filterQuery: getFilterQuery(filters, params), + filterQuery: getFilterQuery(filters), }; } diff --git a/lib/prisma.ts b/lib/prisma.ts index 08309e31..0a52dd7e 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -1,7 +1,7 @@ import prisma from '@umami/prisma-client'; import moment from 'moment-timezone'; import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; -import { FILTER_COLUMNS } from './constants'; +import { FILTER_COLUMNS, SESSION_COLUMNS } from './constants'; const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%d %H:%i:00', @@ -64,14 +64,13 @@ function getTimestampIntervalQuery(field: string): string { } } -function getFilterQuery(filters = {}, params = []): string { +function getFilterQuery(filters = {}): string { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; if (filter !== undefined) { const column = FILTER_COLUMNS[key] || key; arr.push(`and ${column}={{${key}}}`); - params.push(decodeURIComponent(filter)); } return arr; @@ -80,19 +79,12 @@ function getFilterQuery(filters = {}, params = []): string { return query.join('\n'); } -function parseFilters( - filters: { [key: string]: any } = {}, - params = [], - sessionKey = 'session_id', -) { - const { os, browser, device, country, region, city } = filters; - +function parseFilters(filters: { [key: string]: any } = {}) { return { - joinSession: - os || browser || device || country || region || city - ? `inner join session on website_event.${sessionKey} = session.${sessionKey}` - : '', - filterQuery: getFilterQuery(filters, params), + joinSession: Object.keys(filters).find(key => SESSION_COLUMNS[key]) + ? `inner join session on website_event.session_id = session.session_id` + : '', + filterQuery: getFilterQuery(filters), }; } diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index 37a04691..fa0b7554 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -68,7 +68,7 @@ export default async ( filters[type] = undefined; - let data = await getSessionMetrics(websiteId, { + const data = await getSessionMetrics(websiteId, { startDate, endDate, column, @@ -88,7 +88,7 @@ export default async ( } } - data = Object.values(combined); + return ok(res, Object.values(combined)); } return ok(res, data); diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index 453c6733..810854a7 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -6,6 +6,7 @@ import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { getPageviewStats } from 'queries'; import { parseDateRangeQuery } from 'lib/query'; +import { getSessionStats } from '../../../../queries/analytics/sessions/getSessionStats'; export interface WebsitePageviewRequestQuery { id: string; @@ -62,7 +63,6 @@ export default async ( endDate, timezone, unit, - count: '*', filters: { url, referrer, @@ -75,14 +75,14 @@ export default async ( city, }, }), - getPageviewStats(websiteId, { + getSessionStats(websiteId, { startDate, endDate, timezone, unit, - count: 'distinct website_event.', filters: { url, + referrer, title, os, browser, diff --git a/queries/analytics/pageviews/getPageviewMetrics.ts b/queries/analytics/pageviews/getPageviewMetrics.ts index 1032540b..a5da178a 100644 --- a/queries/analytics/pageviews/getPageviewMetrics.ts +++ b/queries/analytics/pageviews/getPageviewMetrics.ts @@ -84,6 +84,7 @@ async function clickhouseQuery( const { rawQuery, parseFilters } = clickhouse; const website = await loadWebsite(websiteId); const params = { + ...filters, websiteId, startDate: maxDate(startDate, website.resetAt), endDate, @@ -98,7 +99,7 @@ async function clickhouseQuery( params.domain = website.domain; } - const { filterQuery } = parseFilters(filters, params); + const { filterQuery } = parseFilters(filters); return rawQuery( ` diff --git a/queries/analytics/pageviews/getPageviewStats.ts b/queries/analytics/pageviews/getPageviewStats.ts index f6d4158c..6d702993 100644 --- a/queries/analytics/pageviews/getPageviewStats.ts +++ b/queries/analytics/pageviews/getPageviewStats.ts @@ -10,9 +10,19 @@ export interface PageviewStatsCriteria { endDate: Date; timezone?: string; unit?: string; - count?: string; - filters: object; - sessionKey?: string; + filters: { + url?: string; + referrer?: string; + title?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; + region?: string; + city?: string; + }; } export async function getPageviewStats( @@ -25,15 +35,7 @@ export async function getPageviewStats( } async function relationalQuery(websiteId: string, criteria: PageviewStatsCriteria) { - const { - startDate, - endDate, - timezone = 'utc', - unit = 'day', - count = '*', - filters = {}, - sessionKey = 'session_id', - } = criteria; + const { startDate, endDate, timezone = 'utc', unit = 'day', filters = {} } = criteria; const { getDateQuery, parseFilters, rawQuery } = prisma; const website = await loadWebsite(websiteId); const { filterQuery, joinSession } = parseFilters(filters); @@ -42,7 +44,7 @@ async function relationalQuery(websiteId: string, criteria: PageviewStatsCriteri ` select ${getDateQuery('website_event.created_at', unit, timezone)} x, - count(${count !== '*' ? `${count}${sessionKey}` : count}) y + count(*) y from website_event ${joinSession} where website_event.website_id = {{websiteId::uuid}} @@ -52,24 +54,17 @@ async function relationalQuery(websiteId: string, criteria: PageviewStatsCriteri group by 1 `, { + ...filters, websiteId, startDate: maxDate(startDate, website.resetAt), endDate, eventType: EVENT_TYPE.pageView, - ...filters, }, ); } async function clickhouseQuery(websiteId: string, criteria: PageviewStatsCriteria) { - const { - startDate, - endDate, - timezone = 'UTC', - unit = 'day', - count = '*', - filters = {}, - } = criteria; + const { startDate, endDate, timezone = 'UTC', unit = 'day', filters = {} } = criteria; const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse; const website = await loadWebsite(websiteId); const { filterQuery } = parseFilters(filters); @@ -82,7 +77,7 @@ async function clickhouseQuery(websiteId: string, criteria: PageviewStatsCriteri from ( select ${getDateQuery('created_at', unit, timezone)} as t, - count(${count !== '*' ? 'distinct session_id' : count}) as y + count(*) as y from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} diff --git a/queries/analytics/sessions/getSessionMetrics.ts b/queries/analytics/sessions/getSessionMetrics.ts index aec2d8f1..2512b06c 100644 --- a/queries/analytics/sessions/getSessionMetrics.ts +++ b/queries/analytics/sessions/getSessionMetrics.ts @@ -28,8 +28,8 @@ async function relationalQuery( return rawQuery( `select ${column} x, count(*) y - from session as x - where x.session_id in ( + from session as s + where s.session_id in ( select website_event.session_id from website_event join website @@ -38,7 +38,7 @@ async function relationalQuery( where website.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} ${filterQuery} - ) + ) as t group by 1 order by 2 desc limit 100`, @@ -64,7 +64,7 @@ async function clickhouseQuery( ` select ${column} x, count(distinct session_id) y - from website_event as x + from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime} and {endDate:DateTime} and event_type = {eventType:UInt32} diff --git a/queries/analytics/sessions/getSessionStats.ts b/queries/analytics/sessions/getSessionStats.ts new file mode 100644 index 00000000..966fd91f --- /dev/null +++ b/queries/analytics/sessions/getSessionStats.ts @@ -0,0 +1,98 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import prisma from 'lib/prisma'; +import { EVENT_TYPE } from 'lib/constants'; +import { loadWebsite } from 'lib/load'; +import { maxDate } from 'lib/date'; + +export interface SessionStatsCriteria { + startDate: Date; + endDate: Date; + timezone?: string; + unit?: string; + filters: { + url?: string; + referrer?: string; + title?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; + region?: string; + city?: string; + }; +} + +export async function getSessionStats( + ...args: [websiteId: string, criteria: SessionStatsCriteria] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websiteId: string, criteria: SessionStatsCriteria) { + const { startDate, endDate, timezone = 'utc', unit = 'day', filters = {} } = criteria; + const { getDateQuery, parseFilters, rawQuery } = prisma; + const website = await loadWebsite(websiteId); + const { filterQuery, joinSession } = parseFilters(filters); + + return rawQuery( + ` + select + ${getDateQuery('website_event.created_at', unit, timezone)} x, + count(distinct website_event.session_id) y + from website_event + ${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 + `, + { + ...filters, + websiteId, + startDate: maxDate(startDate, website.resetAt), + endDate, + eventType: EVENT_TYPE.pageView, + }, + ); +} + +async function clickhouseQuery(websiteId: string, criteria: SessionStatsCriteria) { + const { startDate, endDate, timezone = 'UTC', unit = 'day', filters = {} } = criteria; + const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse; + const website = await loadWebsite(websiteId); + const { filterQuery } = parseFilters(filters); + + return rawQuery( + ` + select + ${getDateStringQuery('g.t', unit)} as x, + g.y as y + from ( + select + ${getDateQuery('created_at', unit, timezone)} as t, + count(distinct session_id) as y + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime} and {endDate:DateTime} + and event_type = {eventType:UInt32} + ${filterQuery} + group by t + ) as g + order by t + `, + { + ...filters, + websiteId, + startDate: maxDate(startDate, website.resetAt), + endDate, + eventType: EVENT_TYPE.pageView, + }, + ); +}