diff --git a/db/clickhouse/migrations/09_add_performance.sql b/db/clickhouse/migrations/09_add_performance.sql index 4b5249541..12f631eb6 100644 --- a/db/clickhouse/migrations/09_add_performance.sql +++ b/db/clickhouse/migrations/09_add_performance.sql @@ -1,72 +1,105 @@ --- Create Performance -CREATE TABLE umami.website_performance -( - website_id UUID, - session_id UUID, - visit_id UUID, - url_path String, - lcp Nullable(Decimal(10, 1)), - inp Nullable(Decimal(10, 1)), - cls Nullable(Decimal(10, 4)), - fcp Nullable(Decimal(10, 1)), - ttfb Nullable(Decimal(10, 1)), - created_at DateTime('UTC') -) -ENGINE = MergeTree - PARTITION BY toYYYYMM(created_at) - ORDER BY (website_id, toStartOfHour(created_at), session_id) - SETTINGS index_granularity = 8192; +-- Add performance columns to website_event +ALTER TABLE umami.website_event ADD COLUMN lcp Nullable(Decimal(10, 1)) AFTER twclid; +ALTER TABLE umami.website_event ADD COLUMN inp Nullable(Decimal(10, 1)) AFTER lcp; +ALTER TABLE umami.website_event ADD COLUMN cls Nullable(Decimal(10, 4)) AFTER inp; +ALTER TABLE umami.website_event ADD COLUMN fcp Nullable(Decimal(10, 1)) AFTER cls; +ALTER TABLE umami.website_event ADD COLUMN ttfb Nullable(Decimal(10, 1)) AFTER fcp; --- Performance hourly aggregation -CREATE TABLE umami.website_performance_hourly -( - website_id UUID, - url_path String, - lcp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))), - lcp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))), - lcp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))), - inp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))), - inp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))), - inp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))), - cls_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 4))), - cls_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 4))), - cls_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 4))), - fcp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))), - fcp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))), - fcp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))), - ttfb_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))), - ttfb_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))), - ttfb_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))), - sample_count SimpleAggregateFunction(sum, UInt64), - created_at DateTime('UTC') -) -ENGINE = AggregatingMergeTree - PARTITION BY toYYYYMM(created_at) - ORDER BY (website_id, toStartOfHour(created_at), url_path) - SETTINGS index_granularity = 8192; +-- Update materialized view to exclude performance events from view counts +DROP TABLE umami.website_event_stats_hourly_mv; -CREATE MATERIALIZED VIEW umami.website_performance_hourly_mv -TO umami.website_performance_hourly +CREATE MATERIALIZED VIEW umami.website_event_stats_hourly_mv +TO umami.website_event_stats_hourly AS SELECT website_id, - url_path, - quantileState(0.5)(lcp) as lcp_p50, - quantileState(0.75)(lcp) as lcp_p75, - quantileState(0.95)(lcp) as lcp_p95, - quantileState(0.5)(inp) as inp_p50, - quantileState(0.75)(inp) as inp_p75, - quantileState(0.95)(inp) as inp_p95, - quantileState(0.5)(cls) as cls_p50, - quantileState(0.75)(cls) as cls_p75, - quantileState(0.95)(cls) as cls_p95, - quantileState(0.5)(fcp) as fcp_p50, - quantileState(0.75)(fcp) as fcp_p75, - quantileState(0.95)(fcp) as fcp_p95, - quantileState(0.5)(ttfb) as ttfb_p50, - quantileState(0.75)(ttfb) as ttfb_p75, - quantileState(0.95)(ttfb) as ttfb_p95, - count() as sample_count, - toStartOfHour(created_at) as created_at -FROM umami.website_performance -GROUP BY website_id, url_path, created_at; + session_id, + visit_id, + hostnames as hostname, + browser, + os, + device, + screen, + language, + country, + region, + city, + entry_url, + exit_url, + url_paths as url_path, + url_query, + utm_source, + utm_medium, + utm_campaign, + utm_content, + utm_term, + referrer_domain, + page_title, + gclid, + fbclid, + msclkid, + ttclid, + li_fat_id, + twclid, + event_type, + event_name, + views, + min_time, + max_time, + tag, + distinct_id, + timestamp as created_at +FROM (SELECT + website_id, + session_id, + visit_id, + arrayFilter(x -> x != '', groupArray(hostname)) hostnames, + browser, + os, + device, + screen, + language, + country, + region, + city, + argMinState(url_path, created_at) entry_url, + argMaxState(url_path, created_at) exit_url, + arrayFilter(x -> x != '', groupArray(url_path)) as url_paths, + arrayFilter(x -> x != '', groupArray(url_query)) url_query, + arrayFilter(x -> x != '', groupArray(utm_source)) utm_source, + arrayFilter(x -> x != '', groupArray(utm_medium)) utm_medium, + arrayFilter(x -> x != '', groupArray(utm_campaign)) utm_campaign, + arrayFilter(x -> x != '', groupArray(utm_content)) utm_content, + arrayFilter(x -> x != '', groupArray(utm_term)) utm_term, + arrayFilter(x -> x != '' and x != hostname, groupArray(referrer_domain)) referrer_domain, + arrayFilter(x -> x != '', groupArray(page_title)) page_title, + arrayFilter(x -> x != '', groupArray(gclid)) gclid, + arrayFilter(x -> x != '', groupArray(fbclid)) fbclid, + arrayFilter(x -> x != '', groupArray(msclkid)) msclkid, + arrayFilter(x -> x != '', groupArray(ttclid)) ttclid, + arrayFilter(x -> x != '', groupArray(li_fat_id)) li_fat_id, + arrayFilter(x -> x != '', groupArray(twclid)) twclid, + event_type, + if(event_type = 2, groupArray(event_name), []) event_name, + sumIf(1, event_type NOT IN (2, 5)) views, + min(created_at) min_time, + max(created_at) max_time, + arrayFilter(x -> x != '', groupArray(tag)) tag, + distinct_id, + toStartOfHour(created_at) timestamp +FROM umami.website_event +GROUP BY website_id, + session_id, + visit_id, + hostname, + browser, + os, + device, + screen, + language, + country, + region, + city, + event_type, + distinct_id, + timestamp); diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index ae08bb7b3..f8add3569 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -34,6 +34,12 @@ CREATE TABLE umami.website_event ttclid String, li_fat_id String, twclid String, + --performance + lcp Nullable(Decimal(10, 1)), + inp Nullable(Decimal(10, 1)), + cls Nullable(Decimal(10, 4)), + fcp Nullable(Decimal(10, 1)), + ttfb Nullable(Decimal(10, 1)), --events event_type UInt32, event_name String, @@ -209,7 +215,7 @@ FROM (SELECT arrayFilter(x -> x != '', groupArray(twclid)) twclid, event_type, if(event_type = 2, groupArray(event_name), []) event_name, - sumIf(1, event_type != 2) views, + sumIf(1, event_type NOT IN (2, 5)) views, min(created_at) min_time, max(created_at) max_time, arrayFilter(x -> x != '', groupArray(tag)) tag, @@ -282,79 +288,6 @@ JOIN (SELECT event_id, string_value as currency ON c.event_id = ed.event_id WHERE positionCaseInsensitive(data_key, 'revenue') > 0; --- Create Performance -CREATE TABLE umami.website_performance -( - website_id UUID, - session_id UUID, - visit_id UUID, - url_path String, - lcp Nullable(Decimal(10, 1)), - inp Nullable(Decimal(10, 1)), - cls Nullable(Decimal(10, 4)), - fcp Nullable(Decimal(10, 1)), - ttfb Nullable(Decimal(10, 1)), - created_at DateTime('UTC') -) -ENGINE = MergeTree - PARTITION BY toYYYYMM(created_at) - ORDER BY (website_id, toStartOfHour(created_at), session_id) - SETTINGS index_granularity = 8192; - --- Performance hourly aggregation -CREATE TABLE umami.website_performance_hourly -( - website_id UUID, - url_path String, - lcp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))), - lcp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))), - lcp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))), - inp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))), - inp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))), - inp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))), - cls_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 4))), - cls_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 4))), - cls_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 4))), - fcp_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))), - fcp_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))), - fcp_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))), - ttfb_p50 AggregateFunction(quantile(0.5), Nullable(Decimal(10, 1))), - ttfb_p75 AggregateFunction(quantile(0.75), Nullable(Decimal(10, 1))), - ttfb_p95 AggregateFunction(quantile(0.95), Nullable(Decimal(10, 1))), - sample_count SimpleAggregateFunction(sum, UInt64), - created_at DateTime('UTC') -) -ENGINE = AggregatingMergeTree - PARTITION BY toYYYYMM(created_at) - ORDER BY (website_id, toStartOfHour(created_at), url_path) - SETTINGS index_granularity = 8192; - -CREATE MATERIALIZED VIEW umami.website_performance_hourly_mv -TO umami.website_performance_hourly -AS -SELECT - website_id, - url_path, - quantileState(0.5)(lcp) as lcp_p50, - quantileState(0.75)(lcp) as lcp_p75, - quantileState(0.95)(lcp) as lcp_p95, - quantileState(0.5)(inp) as inp_p50, - quantileState(0.75)(inp) as inp_p75, - quantileState(0.95)(inp) as inp_p95, - quantileState(0.5)(cls) as cls_p50, - quantileState(0.75)(cls) as cls_p75, - quantileState(0.95)(cls) as cls_p95, - quantileState(0.5)(fcp) as fcp_p50, - quantileState(0.75)(fcp) as fcp_p75, - quantileState(0.95)(fcp) as fcp_p95, - quantileState(0.5)(ttfb) as ttfb_p50, - quantileState(0.75)(ttfb) as ttfb_p75, - quantileState(0.95)(ttfb) as ttfb_p95, - count() as sample_count, - toStartOfHour(created_at) as created_at -FROM umami.website_performance -GROUP BY website_id, url_path, created_at; - -- Create session_replay CREATE TABLE umami.session_replay ( diff --git a/src/app/(main)/websites/[websiteId]/(reports)/performance/Performance.tsx b/src/app/(main)/websites/[websiteId]/(reports)/performance/Performance.tsx index 90821f377..f35090e48 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/performance/Performance.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/performance/Performance.tsx @@ -1,13 +1,26 @@ 'use client'; -import { Column, Grid, Row, Text } from '@umami/react-zen'; +import { + Column, + Grid, + Heading, + ListItem, + Row, + Select, + Tab, + TabList, + TabPanel, + Tabs, + Text, +} from '@umami/react-zen'; import { colord } from 'colord'; import { useCallback, useMemo, useState } from 'react'; import { BarChart } from '@/components/charts/BarChart'; +import { GridRow } from '@/components/common/GridRow'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { Panel } from '@/components/common/Panel'; +import { TypeIcon } from '@/components/common/TypeIcon'; import { useLocale, useMessages, useResultQuery } from '@/components/hooks'; import { ListTable } from '@/components/metrics/ListTable'; -import { MetricsBar } from '@/components/metrics/MetricsBar'; import { PerformanceCard } from '@/components/metrics/PerformanceCard'; import { renderDateLabels } from '@/lib/charts'; import { CHART_COLORS, WEB_VITALS_THRESHOLDS } from '@/lib/constants'; @@ -39,8 +52,15 @@ function formatMetricValue(metric: string, value: number): string { return `${Math.round(value)} ms`; } +const PERCENTILES = [ + { id: 'p50', label: 'p50 — Median' }, + { id: 'p75', label: 'p75 — 75th Percentile' }, + { id: 'p95', label: 'p95 — 95th Percentile' }, +] as const; + export function Performance({ websiteId, startDate, endDate, unit }: PerformanceProps) { const [selectedMetric, setSelectedMetric] = useState('lcp'); + const [selectedPercentile, setSelectedPercentile] = useState<'p50' | 'p75' | 'p95'>('p75'); const { t, labels } = useMessages(); const { locale, dateLocale } = useLocale(); @@ -121,6 +141,19 @@ export function Performance({ websiteId, startDate, endDate, unit }: Performance return ( + + + {data && ( @@ -129,7 +162,7 @@ export function Performance({ websiteId, startDate, endDate, unit }: Performance formatMetricValue(metric, n)} onClick={() => setSelectedMetric(metric)} @@ -162,20 +195,84 @@ export function Performance({ websiteId, startDate, endDate, unit }: Performance /> - - ({ - label: urlPath, - count: Number(p75), - percent: 0, - }), - )} - renderLabel={({ label }: { label: string }) => {label}} - /> - + + + + {t(labels.pages)} + + {t(labels.path)} + {t(labels.pageTitle)} + + + ({ + label: name, + count: Number({ p50, p75, p95 }[selectedPercentile]), + percent: 0, + }))} + renderLabel={({ label }: { label: string }) => {label}} + /> + + + ({ + label: name, + count: Number({ p50, p75, p95 }[selectedPercentile]), + percent: 0, + }))} + renderLabel={({ label }: { label: string }) => {label}} + /> + + + + + + {t(labels.environment)} + + {t(labels.device)} + {t(labels.browser)} + + + ({ + label: name, + count: Number({ p50, p75, p95 }[selectedPercentile]), + percent: 0, + }))} + renderLabel={({ label }: { label: string }) => ( + + + {label} + + )} + /> + + + ({ + label: name, + count: Number({ p50, p75, p95 }[selectedPercentile]), + percent: 0, + }))} + renderLabel={({ label }: { label: string }) => ( + + + {label} + + )} + /> + + + + )} diff --git a/src/app/api/reports/performance/route.ts b/src/app/api/reports/performance/route.ts index e6a085574..d32ffbdb6 100644 --- a/src/app/api/reports/performance/route.ts +++ b/src/app/api/reports/performance/route.ts @@ -3,6 +3,7 @@ import { json, unauthorized } from '@/lib/response'; import { reportResultSchema } from '@/lib/schema'; import { canViewWebsite } from '@/permissions'; import { getPerformance, type PerformanceParameters } from '@/queries/sql/reports/getPerformance'; +import { getPerformanceMetrics } from '@/queries/sql/reports/getPerformanceMetrics'; export async function POST(request: Request) { const { auth, body, error } = await parseRequest(request, reportResultSchema); @@ -20,7 +21,19 @@ export async function POST(request: Request) { const parameters = await setWebsiteDate(websiteId, body.parameters); const filters = await getQueryFilters(body.filters, websiteId); - const data = await getPerformance(websiteId, parameters as PerformanceParameters, filters); + const [{ chart, summary }, pages, pageTitles, devices, browsers] = await Promise.all([ + getPerformance(websiteId, parameters as PerformanceParameters, filters), + getPerformanceMetrics(websiteId, parameters as PerformanceParameters, filters, 'url_path', 500), + getPerformanceMetrics( + websiteId, + parameters as PerformanceParameters, + filters, + 'page_title', + 500, + ), + getPerformanceMetrics(websiteId, parameters as PerformanceParameters, filters, 'device'), + getPerformanceMetrics(websiteId, parameters as PerformanceParameters, filters, 'browser', 500), + ]); - return json(data); + return json({ chart, summary, pages, pageTitles, devices, browsers }); } diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index 1eadafee1..be0220bfe 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -12,7 +12,7 @@ import { parseRequest } from '@/lib/request'; import { badRequest, forbidden, json, serverError } from '@/lib/response'; import { anyObjectParam, urlOrPathParam } from '@/lib/schema'; import { safeDecodeURI, safeDecodeURIComponent } from '@/lib/url'; -import { createSession, saveEvent, savePerformance, saveSessionData } from '@/queries/sql'; +import { createSession, saveEvent, saveSessionData } from '@/queries/sql'; interface Cache { websiteId: string; @@ -284,11 +284,21 @@ export async function POST(request: Request) { const currentUrl = new URL(url, base); const urlPath = currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname; - await savePerformance({ + await saveEvent({ websiteId: sourceId, sessionId, visitId, urlPath, + pageTitle: safeDecodeURIComponent(title), + eventType: EVENT_TYPE.performance, + browser, + os, + device, + screen, + language, + country, + region, + city, lcp, inp, cls, diff --git a/src/components/metrics/ListTable.tsx b/src/components/metrics/ListTable.tsx index b2924b374..9c2fb2360 100644 --- a/src/components/metrics/ListTable.tsx +++ b/src/components/metrics/ListTable.tsx @@ -68,7 +68,12 @@ export function ListTable({ return ( - + {title} {metric} diff --git a/src/components/metrics/PerformanceCard.module.css b/src/components/metrics/PerformanceCard.module.css index 4c7390216..c0de4ae2d 100644 --- a/src/components/metrics/PerformanceCard.module.css +++ b/src/components/metrics/PerformanceCard.module.css @@ -1,9 +1,15 @@ .card { - transition: box-shadow 0.2s; + transition: box-shadow 0.2s ease; +} + +.card:hover { + box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 15%, transparent); } .card.selected { - box-shadow: 0 0 0 2px var(--base-color-primary); + box-shadow: + 0 0 0 1px color-mix(in srgb, var(--primary) 40%, transparent), + 0 0 6px color-mix(in srgb, var(--primary) 15%, transparent); } .good .rating { diff --git a/src/components/metrics/PerformanceCard.tsx b/src/components/metrics/PerformanceCard.tsx index 1c2a1eb65..96f909459 100644 --- a/src/components/metrics/PerformanceCard.tsx +++ b/src/components/metrics/PerformanceCard.tsx @@ -1,6 +1,4 @@ -import { useSpring } from '@react-spring/web'; import { Column, Text } from '@umami/react-zen'; -import { AnimatedDiv } from '@/components/common/AnimatedDiv'; import { useMessages } from '@/components/hooks'; import { WEB_VITALS_THRESHOLDS } from '@/lib/constants'; import { formatNumber } from '@/lib/format'; @@ -33,7 +31,6 @@ export const PerformanceCard = ({ }: PerformanceCardProps) => { const { t, labels } = useMessages(); const rating = getRating(metric, value); - const props = useSpring({ x: Number(value) || 0, from: { x: 0 } }); return ( - {props?.x?.to(x => formatValue(x))} + {formatValue(value)} {t(labels[rating === 'needs-improvement' ? 'needsImprovement' : rating])} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index aa5dbc437..9044897a7 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -114,6 +114,7 @@ export const EVENT_TYPE = { customEvent: 2, linkEvent: 3, pixelEvent: 4, + performance: 5, } as const; export const ENTITY_TYPE = { diff --git a/src/queries/sql/events/saveEvent.ts b/src/queries/sql/events/saveEvent.ts index 7313fe472..b668b7ea7 100644 --- a/src/queries/sql/events/saveEvent.ts +++ b/src/queries/sql/events/saveEvent.ts @@ -53,6 +53,13 @@ export interface SaveEventArgs { ttclid?: string; lifatid?: string; twclid?: string; + + // Performance + lcp?: number; + inp?: number; + cls?: number; + fcp?: number; + ttfb?: number; } export async function saveEvent(args: SaveEventArgs) { @@ -89,6 +96,11 @@ async function relationalQuery({ ttclid, lifatid, twclid, + lcp, + inp, + cls, + fcp, + ttfb, }: SaveEventArgs) { const websiteEventId = uuid(); @@ -119,6 +131,11 @@ async function relationalQuery({ eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, tag, hostname, + lcp, + inp, + cls, + fcp, + ttfb, createdAt, }, }); @@ -186,6 +203,11 @@ async function clickhouseQuery({ ttclid, lifatid, twclid, + lcp, + inp, + cls, + fcp, + ttfb, }: SaveEventArgs) { const { insert, getUTCString } = clickhouse; const { sendMessage } = kafka; @@ -221,12 +243,17 @@ async function clickhouseQuery({ tag: tag, distinct_id: distinctId, created_at: getUTCString(createdAt), - browser, - os, - device, - screen, - language, - hostname, + browser: browser, + os: os, + device: device, + screen: screen, + language: language, + hostname: hostname, + lcp: lcp, + inp: inp, + cls: cls, + fcp: fcp, + ttfb: ttfb, }; if (kafka.enabled) { diff --git a/src/queries/sql/getChannelExpandedMetrics.ts b/src/queries/sql/getChannelExpandedMetrics.ts index 1c6197064..c3201dfa6 100644 --- a/src/queries/sql/getChannelExpandedMetrics.ts +++ b/src/queries/sql/getChannelExpandedMetrics.ts @@ -68,7 +68,7 @@ async function relationalQuery( ${excludeBounceQuery} ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) ${dateQuery} ${filterQuery} group by prefix, @@ -163,7 +163,7 @@ async function clickhouseQuery( VIDEO_DOMAINS, )}]) != 0 or position(lower(utm_medium), 'video') > 0 then concat(prefix, 'Video') when referrer_domain != hostname and referrer_domain != '' then 'referral' - else '' end AS name, + else '' end AS "name", session_id, visit_id, count(*) c, @@ -174,7 +174,7 @@ async function clickhouseQuery( ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) and name != '' ${filterQuery} group by prefix, name, session_id, visit_id diff --git a/src/queries/sql/getChannelMetrics.ts b/src/queries/sql/getChannelMetrics.ts index 524d42ac4..487c5dd74 100644 --- a/src/queries/sql/getChannelMetrics.ts +++ b/src/queries/sql/getChannelMetrics.ts @@ -45,7 +45,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { ${excludeBounceQuery} ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) ${dateQuery} ${filterQuery}), @@ -123,7 +123,7 @@ async function clickhouseQuery( ${cohortQuery} ${excludeBounceQuery} where website_id = {websiteId:UUID} - and event_type != 2 + and event_type NOT IN (2, 5) ${dateQuery} ${filterQuery} group by 1, 2 diff --git a/src/queries/sql/getWebsiteStats.ts b/src/queries/sql/getWebsiteStats.ts index 833673e77..527b9c152 100644 --- a/src/queries/sql/getWebsiteStats.ts +++ b/src/queries/sql/getWebsiteStats.ts @@ -58,7 +58,7 @@ async function relationalQuery( ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) ${filterQuery} group by 1, 2 @@ -103,7 +103,7 @@ async function clickhouseQuery( ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${filterQuery} group by session_id, visit_id ) as t; @@ -127,7 +127,7 @@ async function clickhouseQuery( ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${filterQuery} group by session_id, visit_id ) as t; diff --git a/src/queries/sql/getWeeklyTraffic.ts b/src/queries/sql/getWeeklyTraffic.ts index b6da2f1b6..bd439841f 100644 --- a/src/queries/sql/getWeeklyTraffic.ts +++ b/src/queries/sql/getWeeklyTraffic.ts @@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) ${filterQuery} group by time order by 1 @@ -63,7 +63,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) { ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${filterQuery} group by time order by time @@ -78,7 +78,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) { ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${filterQuery} group by time order by time diff --git a/src/queries/sql/index.ts b/src/queries/sql/index.ts index 45a6149d5..0b164a458 100644 --- a/src/queries/sql/index.ts +++ b/src/queries/sql/index.ts @@ -24,7 +24,6 @@ export * from './pageviews/getPageviewExpandedMetrics'; export * from './pageviews/getPageviewMetrics'; export * from './pageviews/getPageviewStats'; export * from './performance/getPerformanceStats'; -export * from './performance/savePerformance'; export * from './replays/getReplayChunks'; export * from './replays/getSessionReplays'; export * from './replays/saveRecording'; diff --git a/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts b/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts index 29576a478..a0c489ac8 100644 --- a/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts +++ b/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts @@ -68,7 +68,7 @@ async function relationalQuery( from website_event where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) group by visit_id ) x on x.visit_id = website_event.visit_id @@ -100,7 +100,7 @@ async function relationalQuery( ${entryExitQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) ${excludeDomain} ${filterQuery} group by ${column}, website_event.session_id, website_event.visit_id @@ -149,7 +149,7 @@ async function clickhouseQuery( from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) group by visit_id) x ON x.visit_id = website_event.visit_id`; } @@ -177,7 +177,7 @@ async function clickhouseQuery( ${entryExitQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) and name != '' ${excludeDomain} ${filterQuery} diff --git a/src/queries/sql/pageviews/getPageviewMetrics.ts b/src/queries/sql/pageviews/getPageviewMetrics.ts index 748d13665..3edb25282 100644 --- a/src/queries/sql/pageviews/getPageviewMetrics.ts +++ b/src/queries/sql/pageviews/getPageviewMetrics.ts @@ -63,7 +63,7 @@ async function relationalQuery( from website_event where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) order by visit_id, created_at ${order} ) x on x.visit_id = website_event.visit_id @@ -81,7 +81,7 @@ async function relationalQuery( ${entryExitQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) ${excludeDomain} ${filterQuery} group by 1 @@ -127,7 +127,7 @@ async function clickhouseQuery( from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) group by visit_id) x ON x.visit_id = website_event.visit_id`; } @@ -141,7 +141,7 @@ async function clickhouseQuery( ${entryExitQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${excludeDomain} ${filterQuery} group by x @@ -180,7 +180,7 @@ async function clickhouseQuery( ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${excludeDomain} ${filterQuery} ${groupByQuery}) as g diff --git a/src/queries/sql/pageviews/getPageviewStats.ts b/src/queries/sql/pageviews/getPageviewStats.ts index a0aa27689..86d438b36 100644 --- a/src/queries/sql/pageviews/getPageviewStats.ts +++ b/src/queries/sql/pageviews/getPageviewStats.ts @@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) ${filterQuery} group by 1 order by 1 @@ -70,7 +70,7 @@ async function clickhouseQuery( ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${filterQuery} group by t ) as g @@ -90,7 +90,7 @@ async function clickhouseQuery( ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${filterQuery} group by t ) as g diff --git a/src/queries/sql/performance/getPerformanceStats.ts b/src/queries/sql/performance/getPerformanceStats.ts index f727e2111..0c2b49506 100644 --- a/src/queries/sql/performance/getPerformanceStats.ts +++ b/src/queries/sql/performance/getPerformanceStats.ts @@ -23,8 +23,11 @@ async function relationalQuery( websiteId: string, filters: QueryFilters, ): Promise { - const { rawQuery } = prisma; - const { startDate, endDate } = filters; + const { rawQuery, parseFilters } = prisma; + const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + }); const result = await rawQuery( ` @@ -35,11 +38,15 @@ async function relationalQuery( percentile_cont(0.75) within group (order by fcp) as fcp, percentile_cont(0.75) within group (order by ttfb) as ttfb, count(*) as count - from performance - where website_id = {{websiteId::uuid}} - and created_at between {{startDate}} and {{endDate}} + from website_event + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.event_type = 5 + and website_event.created_at between {{startDate}} and {{endDate}} + ${filterQuery} `, - { websiteId, startDate, endDate }, + queryParams, ); return result?.[0] || { lcp: 0, inp: 0, cls: 0, fcp: 0, ttfb: 0, count: 0 }; @@ -49,8 +56,8 @@ async function clickhouseQuery( websiteId: string, filters: QueryFilters, ): Promise { - const { rawQuery } = clickhouse; - const { startDate, endDate } = filters; + const { rawQuery, parseFilters } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); const result = await rawQuery( ` @@ -61,11 +68,14 @@ async function clickhouseQuery( quantile(0.75)(fcp) as fcp, quantile(0.75)(ttfb) as ttfb, count() as count - from website_performance - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} + from website_event + ${cohortQuery} + where website_event.website_id = {websiteId:UUID} + and website_event.event_type = 5 + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} `, - { websiteId, startDate, endDate }, + queryParams, ); return result?.[0] || { lcp: 0, inp: 0, cls: 0, fcp: 0, ttfb: 0, count: 0 }; diff --git a/src/queries/sql/performance/savePerformance.ts b/src/queries/sql/performance/savePerformance.ts deleted file mode 100644 index b50856185..000000000 --- a/src/queries/sql/performance/savePerformance.ts +++ /dev/null @@ -1,70 +0,0 @@ -import clickhouse from '@/lib/clickhouse'; -import { uuid } from '@/lib/crypto'; -import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; -import kafka from '@/lib/kafka'; -import prisma from '@/lib/prisma'; - -export interface SavePerformanceArgs { - websiteId: string; - sessionId: string; - visitId: string; - urlPath: string; - lcp?: number; - inp?: number; - cls?: number; - fcp?: number; - ttfb?: number; - createdAt: Date; -} - -export async function savePerformance(args: SavePerformanceArgs) { - return runQuery({ - [PRISMA]: () => relationalQuery(args), - [CLICKHOUSE]: () => clickhouseQuery(args), - }); -} - -async function relationalQuery(data: SavePerformanceArgs) { - const { websiteId, sessionId, visitId, urlPath, lcp, inp, cls, fcp, ttfb, createdAt } = data; - - await prisma.client.performance.create({ - data: { - id: uuid(), - websiteId, - sessionId, - visitId, - urlPath: urlPath?.substring(0, 500), - lcp, - inp, - cls, - fcp, - ttfb, - createdAt, - }, - }); -} - -async function clickhouseQuery(data: SavePerformanceArgs) { - const { websiteId, sessionId, visitId, urlPath, lcp, inp, cls, fcp, ttfb, createdAt } = data; - const { insert, getUTCString } = clickhouse; - const { sendMessage } = kafka; - - const message = { - website_id: websiteId, - session_id: sessionId, - visit_id: visitId, - url_path: urlPath?.substring(0, 500), - lcp: lcp ?? null, - inp: inp ?? null, - cls: cls ?? null, - fcp: fcp ?? null, - ttfb: ttfb ?? null, - created_at: getUTCString(createdAt), - }; - - if (kafka.enabled) { - await sendMessage('performance', message); - } else { - await insert('website_performance', [message]); - } -} diff --git a/src/queries/sql/reports/getPerformance.ts b/src/queries/sql/reports/getPerformance.ts index b73bf100a..6a1c7a898 100644 --- a/src/queries/sql/reports/getPerformance.ts +++ b/src/queries/sql/reports/getPerformance.ts @@ -13,7 +13,6 @@ export interface PerformanceParameters { export interface PerformanceResult { chart: { t: string; p50: number; p75: number; p95: number }[]; - pages: { urlPath: string; p75: number; count: number }[]; summary: { lcp: { p50: number; p75: number; p95: number }; inp: { p50: number; p75: number; p95: number }; @@ -39,7 +38,11 @@ async function relationalQuery( filters: QueryFilters, ): Promise { const { startDate, endDate, unit = 'day', timezone = 'utc', metric = 'lcp' } = parameters; - const { getDateSQL, rawQuery } = prisma; + const { getDateSQL, rawQuery, parseFilters } = prisma; + const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + }); const chart = await rawQuery( ` @@ -48,29 +51,17 @@ async function relationalQuery( percentile_cont(0.5) within group (order by ${metric}) as p50, percentile_cont(0.75) within group (order by ${metric}) as p75, percentile_cont(0.95) within group (order by ${metric}) as p95 - from performance - where website_id = {{websiteId::uuid}} - and created_at between {{startDate}} and {{endDate}} + from website_event + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.event_type = 5 + and website_event.created_at between {{startDate}} and {{endDate}} + ${filterQuery} group by t order by t `, - { websiteId, startDate, endDate }, - ); - - const pages = await rawQuery( - ` - select - url_path as "urlPath", - percentile_cont(0.75) within group (order by ${metric}) as p75, - count(*) as count - from performance - where website_id = {{websiteId::uuid}} - and created_at between {{startDate}} and {{endDate}} - group by url_path - order by p75 desc - limit 100 - `, - { websiteId, startDate, endDate }, + { ...queryParams, startDate, endDate }, ); const summaryResult = await rawQuery( @@ -92,11 +83,15 @@ async function relationalQuery( percentile_cont(0.75) within group (order by ttfb) as ttfb_p75, percentile_cont(0.95) within group (order by ttfb) as ttfb_p95, count(*) as count - from performance - where website_id = {{websiteId::uuid}} - and created_at between {{startDate}} and {{endDate}} + from website_event + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.event_type = 5 + and website_event.created_at between {{startDate}} and {{endDate}} + ${filterQuery} `, - { websiteId, startDate, endDate }, + { ...queryParams, startDate, endDate }, ).then(result => result?.[0]); const summary = { @@ -128,7 +123,7 @@ async function relationalQuery( count: Number(summaryResult?.count || 0), }; - return { chart, pages, summary }; + return { chart, summary }; } async function clickhouseQuery( @@ -137,38 +132,26 @@ async function clickhouseQuery( filters: QueryFilters, ): Promise { const { startDate, endDate, unit = 'day', timezone = 'utc', metric = 'lcp' } = parameters; - const { getDateSQL, rawQuery } = clickhouse; + const { getDateSQL, rawQuery, parseFilters } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); const chart = await rawQuery<{ t: string; p50: number; p75: number; p95: number }[]>( ` select - ${getDateSQL('created_at', unit, timezone)} t, + ${getDateSQL('created_at', 'minute', timezone)} t, quantile(0.5)(${metric}) as p50, quantile(0.75)(${metric}) as p75, quantile(0.95)(${metric}) as p95 - from website_performance - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} + from website_event + ${cohortQuery} + where website_event.website_id = {websiteId:UUID} + and website_event.event_type = 5 + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} group by t order by t `, - { websiteId, startDate, endDate }, - ); - - const pages = await rawQuery<{ urlPath: string; p75: number; count: number }[]>( - ` - select - url_path as "urlPath", - quantile(0.75)(${metric}) as p75, - count() as count - from website_performance - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} - group by url_path - order by p75 desc - limit 100 - `, - { websiteId, startDate, endDate }, + { ...queryParams, startDate, endDate }, ); const summaryResult = await rawQuery( @@ -190,11 +173,14 @@ async function clickhouseQuery( quantile(0.75)(ttfb) as ttfb_p75, quantile(0.95)(ttfb) as ttfb_p95, count() as count - from website_performance - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} + from website_event + ${cohortQuery} + where website_event.website_id = {websiteId:UUID} + and website_event.event_type = 5 + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} `, - { websiteId, startDate, endDate }, + { ...queryParams, startDate, endDate }, ).then(result => result?.[0]); const summary = { @@ -226,5 +212,5 @@ async function clickhouseQuery( count: Number(summaryResult?.count || 0), }; - return { chart, pages, summary }; + return { chart, summary }; } diff --git a/src/queries/sql/reports/getPerformanceMetrics.ts b/src/queries/sql/reports/getPerformanceMetrics.ts new file mode 100644 index 000000000..888b27abe --- /dev/null +++ b/src/queries/sql/reports/getPerformanceMetrics.ts @@ -0,0 +1,98 @@ +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import type { QueryFilters } from '@/lib/types'; +import type { PerformanceParameters } from './getPerformance'; + +export interface PerformanceMetricsData { + name: string; + p50: number; + p75: number; + p95: number; + count: number; +} + +export async function getPerformanceMetrics( + ...args: [ + websiteId: string, + parameters: PerformanceParameters, + filters: QueryFilters, + column: string, + limit?: number, + ] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + parameters: PerformanceParameters, + filters: QueryFilters, + column: string, + limit?: number, +): Promise { + const { startDate, endDate, metric = 'lcp' } = parameters; + const { rawQuery, parseFilters } = prisma; + const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({ + ...filters, + websiteId, + }); + + return rawQuery( + ` + select + ${column} as "name", + percentile_cont(0.5) within group (order by ${metric}) as p50, + percentile_cont(0.75) within group (order by ${metric}) as p75, + percentile_cont(0.95) within group (order by ${metric}) as p95, + count(*) as count + from website_event + ${cohortQuery} + ${joinSessionQuery} + where website_event.website_id = {{websiteId::uuid}} + and website_event.event_type = 5 + and website_event.created_at between {{startDate}} and {{endDate}} + ${filterQuery} + group by ${column} + order by p75 desc + ${limit ? `limit ${limit}` : ''} + `, + { ...queryParams, startDate, endDate }, + ); +} + +async function clickhouseQuery( + websiteId: string, + parameters: PerformanceParameters, + filters: QueryFilters, + column: string, + limit?: number, +): Promise { + const { startDate, endDate, metric = 'lcp' } = parameters; + const { rawQuery, parseFilters } = clickhouse; + const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId }); + + return rawQuery( + ` + select + ${column} as "name", + quantile(0.5)(${metric}) as p50, + quantile(0.75)(${metric}) as p75, + quantile(0.95)(${metric}) as p95, + count() as count + from website_event + ${cohortQuery} + where website_event.website_id = {websiteId:UUID} + and website_event.event_type = 5 + and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} + ${filterQuery} + group by ${column} + order by p75 desc + ${limit ? `limit ${limit}` : ''} + `, + { ...queryParams, startDate, endDate }, + ); +} diff --git a/src/queries/sql/sessions/getSessionExpandedMetrics.ts b/src/queries/sql/sessions/getSessionExpandedMetrics.ts index 24cad9433..f81acc769 100644 --- a/src/queries/sql/sessions/getSessionExpandedMetrics.ts +++ b/src/queries/sql/sessions/getSessionExpandedMetrics.ts @@ -79,7 +79,7 @@ async function relationalQuery( ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) ${filterQuery} group by name, website_event.session_id, website_event.visit_id ${includeCountry ? ', country' : ''} @@ -138,7 +138,7 @@ async function clickhouseQuery( ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) and name != '' ${filterQuery} group by name, session_id, visit_id diff --git a/src/queries/sql/sessions/getSessionMetrics.ts b/src/queries/sql/sessions/getSessionMetrics.ts index 32a915c4c..59b2ebf82 100644 --- a/src/queries/sql/sessions/getSessionMetrics.ts +++ b/src/queries/sql/sessions/getSessionMetrics.ts @@ -57,7 +57,7 @@ async function relationalQuery( ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) ${filterQuery} group by 1 ${includeCountry ? ', 3' : ''} @@ -101,7 +101,7 @@ async function clickhouseQuery( ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${filterQuery} group by x ${includeCountry ? ', country' : ''} @@ -120,7 +120,7 @@ async function clickhouseQuery( ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${filterQuery} group by x ${includeCountry ? ', country' : ''} diff --git a/src/queries/sql/sessions/getSessionStats.ts b/src/queries/sql/sessions/getSessionStats.ts index 150346810..644dcf658 100644 --- a/src/queries/sql/sessions/getSessionStats.ts +++ b/src/queries/sql/sessions/getSessionStats.ts @@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { ${joinSessionQuery} where website_event.website_id = {{websiteId::uuid}} and website_event.created_at between {{startDate}} and {{endDate}} - and website_event.event_type != 2 + and website_event.event_type NOT IN (2, 5) ${filterQuery} group by 1 order by 1 @@ -70,7 +70,7 @@ async function clickhouseQuery( ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${filterQuery} group by t ) as g @@ -90,7 +90,7 @@ async function clickhouseQuery( ${excludeBounceQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type != 2 + and event_type NOT IN (2, 5) ${filterQuery} group by t ) as g