Merge branch 'dev' of https://github.com/umami-software/umami into analytics

This commit is contained in:
Francis Cao 2025-07-21 10:56:08 -07:00
commit 4dd4d66a3b
12 changed files with 316 additions and 99 deletions

View file

@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE `segment` (
`segment_id` VARCHAR(36) NOT NULL,
`website_id` VARCHAR(36) NOT NULL,
`type` VARCHAR(200) NOT NULL,
`name` VARCHAR(200) NOT NULL,
`parameters` JSON NOT NULL,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`updated_at` TIMESTAMP(0) NULL,
UNIQUE INDEX `segment_segment_id_key`(`segment_id`),
INDEX `segment_website_id_idx`(`website_id`),
PRIMARY KEY (`segment_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `report` MODIFY `parameters` JSON NOT NULL;

View file

@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE `revenue` (
`revenue_id` VARCHAR(36) NOT NULL,
`website_id` VARCHAR(36) NOT NULL,
`session_id` VARCHAR(36) NOT NULL,
`event_id` VARCHAR(36) NOT NULL,
`event_name` VARCHAR(50) NOT NULL,
`currency` VARCHAR(100) NOT NULL,
`revenue` DECIMAL(19, 4) NULL,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
UNIQUE INDEX `revenue_revenue_id_key`(`revenue_id`),
INDEX `revenue_website_id_idx`(`website_id`),
INDEX `revenue_session_id_idx`(`session_id`),
INDEX `revenue_website_id_created_at_idx`(`website_id`, `created_at`),
INDEX `revenue_website_id_session_id_created_at_idx`(`website_id`, `session_id`, `created_at`),
PRIMARY KEY (`revenue_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View file

@ -43,6 +43,7 @@ model Session {
websiteEvent WebsiteEvent[]
sessionData SessionData[]
revenue Revenue[]
@@index([createdAt])
@@index([websiteId])
@ -76,7 +77,9 @@ model Website {
team Team? @relation(fields: [teamId], references: [id])
eventData EventData[]
report Report[]
revenue Revenue[]
sessionData SessionData[]
segment Segment[]
@@index([userId])
@@index([teamId])
@ -215,10 +218,10 @@ model Report {
id String @id() @unique() @map("report_id") @db.VarChar(36)
userId String @map("user_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36)
type String @map("type") @db.VarChar(200)
name String @map("name") @db.VarChar(200)
description String @map("description") @db.VarChar(500)
parameters String @map("parameters") @db.VarChar(6000)
type String @db.VarChar(200)
name String @db.VarChar(200)
description String @db.VarChar(500)
parameters Json
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
@ -231,3 +234,38 @@ model Report {
@@index([name])
@@map("report")
}
model Segment {
id String @id() @unique() @map("segment_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36)
type String @db.VarChar(200)
name String @db.VarChar(200)
parameters Json
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
website Website @relation(fields: [websiteId], references: [id])
@@index([websiteId])
@@map("segment")
}
model Revenue {
id String @id() @unique() @map("revenue_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36)
sessionId String @map("session_id") @db.VarChar(36)
eventId String @map("event_id") @db.VarChar(36)
eventName String @map("event_name") @db.VarChar(50)
currency String @db.VarChar(100)
revenue Decimal? @db.Decimal(19, 4)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
website Website @relation(fields: [websiteId], references: [id])
session Session @relation(fields: [sessionId], references: [id])
@@index([websiteId])
@@index([sessionId])
@@index([websiteId, createdAt])
@@index([websiteId, sessionId, createdAt])
@@map("revenue")
}

68
pnpm-lock.yaml generated
View file

@ -248,7 +248,7 @@ importers:
version: 14.2.30(eslint@8.57.1)(typescript@5.8.3)
eslint-config-prettier:
specifier: ^8.5.0
version: 8.10.0(eslint@8.57.1)
version: 8.10.1(eslint@8.57.1)
eslint-import-resolver-alias:
specifier: ^1.1.2
version: 1.1.2(eslint-plugin-import@2.32.0)
@ -266,7 +266,7 @@ importers:
version: 27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3)
eslint-plugin-prettier:
specifier: ^4.0.0
version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8)
version: 4.2.3(eslint-config-prettier@8.10.1(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8)
extract-react-intl-messages:
specifier: ^4.1.1
version: 4.1.1(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3))
@ -1308,14 +1308,14 @@ packages:
peerDependencies:
'@dicebear/core': ^9.0.0
'@emnapi/core@1.4.4':
resolution: {integrity: sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==}
'@emnapi/core@1.4.5':
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
'@emnapi/runtime@1.4.4':
resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==}
'@emnapi/runtime@1.4.5':
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
'@emnapi/wasi-threads@1.0.3':
resolution: {integrity: sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==}
'@emnapi/wasi-threads@1.0.4':
resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==}
'@esbuild/aix-ppc64@0.25.6':
resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==}
@ -3377,8 +3377,8 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
electron-to-chromium@1.5.186:
resolution: {integrity: sha512-lur7L4BFklgepaJxj4DqPk7vKbTEl0pajNlg2QjE5shefmlmBLm2HvQ7PMf1R/GvlevT/581cop33/quQcfX3A==}
electron-to-chromium@1.5.187:
resolution: {integrity: sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==}
emittery@0.13.1:
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
@ -3477,8 +3477,8 @@ packages:
typescript:
optional: true
eslint-config-prettier@8.10.0:
resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==}
eslint-config-prettier@8.10.1:
resolution: {integrity: sha512-mXi3I2ghYZv02pKsUS5C2IRcYlOs3WFNYzYtLKX5s9mFju7BIAjxJqA4UG2qN2DAC0yUECdnfs5iGnyUdgOWzA==}
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
@ -3566,8 +3566,8 @@ packages:
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
eslint-plugin-prettier@4.2.1:
resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
eslint-plugin-prettier@4.2.3:
resolution: {integrity: sha512-HOT5QrFj0ioo/USxnMvj9+E1kwJHg7HDpY9sf6mxNqisHbSegMnmdan/wfUtIPVZ8hwcfGGEJpyG1/TpeR5R1g==}
engines: {node: '>=12.0.0'}
peerDependencies:
eslint: '>=7.28.0'
@ -4796,8 +4796,8 @@ packages:
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
mdn-data@2.22.1:
resolution: {integrity: sha512-u9Xnc9zLuF/CL2IHPow7HcXPpb8okQyzYpwL5wFsY//JRedSWYglYRg3PYWoQCu1zO+tBTmWOJN/iM0mPC5CRQ==}
mdn-data@2.23.0:
resolution: {integrity: sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==}
memoize-one@5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
@ -4914,8 +4914,8 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
napi-postinstall@0.3.0:
resolution: {integrity: sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==}
napi-postinstall@0.3.1:
resolution: {integrity: sha512-is9eGpjKpRg+Z7ECny6NSOekea7+1eTs9+jTt5jJx43ez0o1BYRUKEBIf+ZZn15oQf5wvPnRf0Uat6MAtvVz+A==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
hasBin: true
@ -7840,18 +7840,18 @@ snapshots:
dependencies:
'@dicebear/core': 9.2.3
'@emnapi/core@1.4.4':
'@emnapi/core@1.4.5':
dependencies:
'@emnapi/wasi-threads': 1.0.3
'@emnapi/wasi-threads': 1.0.4
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.4.4':
'@emnapi/runtime@1.4.5':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.0.3':
'@emnapi/wasi-threads@1.0.4':
dependencies:
tslib: 2.8.1
optional: true
@ -8180,7 +8180,7 @@ snapshots:
'@img/sharp-wasm32@0.34.3':
dependencies:
'@emnapi/runtime': 1.4.4
'@emnapi/runtime': 1.4.5
optional: true
'@img/sharp-win32-arm64@0.34.3':
@ -8407,8 +8407,8 @@ snapshots:
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.4.4
'@emnapi/runtime': 1.4.4
'@emnapi/core': 1.4.5
'@emnapi/runtime': 1.4.5
'@tybys/wasm-util': 0.10.0
optional: true
@ -9458,7 +9458,7 @@ snapshots:
browserslist@4.25.1:
dependencies:
caniuse-lite: 1.0.30001727
electron-to-chromium: 1.5.186
electron-to-chromium: 1.5.187
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.1)
@ -10119,7 +10119,7 @@ snapshots:
dependencies:
jake: 10.9.2
electron-to-chromium@1.5.186: {}
electron-to-chromium@1.5.187: {}
emittery@0.13.1: {}
@ -10311,7 +10311,7 @@ snapshots:
- eslint-plugin-import-x
- supports-color
eslint-config-prettier@8.10.0(eslint@8.57.1):
eslint-config-prettier@8.10.1(eslint@8.57.1):
dependencies:
eslint: 8.57.1
@ -10423,13 +10423,13 @@ snapshots:
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8):
eslint-plugin-prettier@4.2.3(eslint-config-prettier@8.10.1(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8):
dependencies:
eslint: 8.57.1
prettier: 2.8.8
prettier-linter-helpers: 1.0.0
optionalDependencies:
eslint-config-prettier: 8.10.0(eslint@8.57.1)
eslint-config-prettier: 8.10.1(eslint@8.57.1)
eslint-plugin-promise@6.6.0(eslint@8.57.1):
dependencies:
@ -11914,7 +11914,7 @@ snapshots:
mdn-data@2.12.2:
optional: true
mdn-data@2.22.1:
mdn-data@2.23.0:
optional: true
memoize-one@5.2.1: {}
@ -12029,7 +12029,7 @@ snapshots:
nanoid@3.3.11: {}
napi-postinstall@0.3.0: {}
napi-postinstall@0.3.1: {}
natural-compare@1.4.0: {}
@ -13465,7 +13465,7 @@ snapshots:
css-tree: 3.1.0
is-plain-object: 5.0.0
known-css-properties: 0.36.0
mdn-data: 2.22.1
mdn-data: 2.23.0
postcss-media-query-parser: 0.2.3
postcss-resolve-nested-selector: 0.1.6
postcss-selector-parser: 7.1.0
@ -13789,7 +13789,7 @@ snapshots:
unrs-resolver@1.11.1:
dependencies:
napi-postinstall: 0.3.0
napi-postinstall: 0.3.1
optionalDependencies:
'@unrs/resolver-binding-android-arm-eabi': 1.11.1
'@unrs/resolver-binding-android-arm64': 1.11.1

View file

@ -0,0 +1,41 @@
-----------------------------------------------------
-- PostgreSQL
-----------------------------------------------------
INSERT INTO "revenue"
SELECT gen_random_uuid() revenue_id,
ed.website_id,
we.session_id,
we.event_id,
we.event_name,
currency.string_value currency,
coalesce(ed.number_value, cast(ed.string_value as numeric(19,4))) revenue,
ed.created_at
FROM event_data ed
JOIN website_event we
ON we.event_id = ed.website_event_id
JOIN (SELECT website_event_id, string_value
FROM event_data
WHERE data_key ilike '%currency%') currency
ON currency.website_event_id = ed.website_event_id
WHERE ed.data_key ilike '%revenue%';
-----------------------------------------------------
-- MySQL
-----------------------------------------------------
INSERT INTO `revenue`
SELECT UUID() revenue_id,
ed.website_id,
we.session_id,
we.event_id,
we.event_name,
currency.string_value currency,
coalesce(ed.number_value, cast(ed.string_value as decimal(19,4))) revenue,
ed.created_at
FROM event_data ed
JOIN website_event we
ON we.event_id = ed.website_event_id
JOIN (SELECT website_event_id, string_value
FROM event_data
WHERE data_key like '%currency%') currency
ON currency.website_event_id = ed.website_event_id
WHERE ed.data_key like '%revenue%';

View file

@ -9,16 +9,22 @@ import { useMessages } from '@/components/hooks';
import { Item, Tabs } from 'react-basics';
import { useState } from 'react';
import EventProperties from './EventProperties';
import { getItem, setItem } from '@/lib/storage';
export default function EventsPage({ websiteId }) {
const [label, setLabel] = useState(null);
const [tab, setTab] = useState('activity');
const [tab, setTab] = useState(getItem('eventTab') || 'activity');
const { formatMessage, labels } = useMessages();
const handleLabelClick = (value: string) => {
setLabel(value !== label ? value : '');
};
const onSelect = (value: 'activity' | 'properties') => {
setItem('eventTab', value);
setTab(value);
};
return (
<>
<WebsiteHeader websiteId={websiteId} />
@ -34,11 +40,7 @@ export default function EventsPage({ websiteId }) {
/>
</GridRow>
<div>
<Tabs
selectedKey={tab}
onSelect={(value: any) => setTab(value)}
style={{ marginBottom: 30 }}
>
<Tabs selectedKey={tab} onSelect={onSelect} style={{ marginBottom: 30 }}>
<Item key="activity">{formatMessage(labels.activity)}</Item>
<Item key="properties">{formatMessage(labels.properties)}</Item>
</Tabs>

View file

@ -3,7 +3,7 @@ import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/requ
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { filterParams, timezoneParam, unitParam } from '@/lib/schema';
import { getEventMetrics } from '@/queries';
import { getEventStats } from '@/queries';
export async function GET(
request: Request,
@ -39,7 +39,7 @@ export async function GET(
unit,
};
const data = await getEventMetrics(websiteId, filters);
const data = await getEventStats(websiteId, filters);
return json(data);
}

View file

@ -15,7 +15,12 @@ import {
} from '@/lib/constants';
import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request';
import { json, unauthorized, badRequest } from '@/lib/response';
import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries';
import {
getPageviewMetrics,
getSessionMetrics,
getEventMetrics,
getChannelMetrics,
} from '@/queries';
import { filterParams } from '@/lib/schema';
export async function GET(
@ -85,7 +90,13 @@ export async function GET(
}
if (EVENT_COLUMNS.includes(type)) {
const data = await getPageviewMetrics(websiteId, type, filters, limit, offset);
let data;
if (type === 'event') {
data = await getEventMetrics(websiteId, type, filters, limit, offset);
} else {
data = await getPageviewMetrics(websiteId, type, filters, limit, offset);
}
return json(data);
}

View file

@ -11,6 +11,7 @@ export * from '@/queries/sql/events/getEventDataValues';
export * from '@/queries/sql/events/getEventDataStats';
export * from '@/queries/sql/events/getEventDataUsage';
export * from '@/queries/sql/events/getEventMetrics';
export * from '@/queries/sql/events/getEventStats';
export * from '@/queries/sql/events/getWebsiteEvents';
export * from '@/queries/sql/events/getEventUsage';
export * from '@/queries/sql/events/saveEvent';

View file

@ -1,32 +1,46 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE } from '@/lib/constants';
import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters, WebsiteEventMetric } from '@/lib/types';
import { QueryFilters } from '@/lib/types';
export async function getEventMetrics(
...args: [websiteId: string, filters: QueryFilters]
): Promise<WebsiteEventMetric[]> {
...args: [
websiteId: string,
type: string,
filters: QueryFilters,
limit?: number | string,
offset?: number | string,
]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = prisma;
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.customEvent,
});
async function relationalQuery(
websiteId: string,
type: string,
filters: QueryFilters,
limit: number | string = 500,
offset: number | string = 0,
) {
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = prisma;
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(
websiteId,
{
...filters,
eventType: EVENT_TYPE.customEvent,
},
{ joinSession: SESSION_COLUMNS.includes(type) },
);
return rawQuery(
`
select
event_name x,
${getDateSQL('website_event.created_at', unit, timezone)} t,
count(*) y
select ${column} x,
count(*) as y
from website_event
${cohortQuery}
${joinSession}
@ -34,8 +48,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
${filterQuery}
group by 1, 2
order by 2
group by 1
order by 2 desc
limit ${limit}
offset ${offset}
`,
params,
);
@ -43,50 +59,32 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(
websiteId: string,
type: string,
filters: QueryFilters,
): Promise<{ x: string; t: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = clickhouse;
limit: number | string = 500,
offset: number | string = 0,
): Promise<{ x: string; y: number }[]> {
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.customEvent,
});
let sql = '';
if (filterQuery || cohortQuery) {
sql = `
select
event_name x,
${getDateSQL('created_at', unit, timezone)} t,
count(*) y
return rawQuery(
`select ${column} x,
count(*) as y
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
${filterQuery}
group by x, t
order by t
`;
} else {
sql = `
select
event_name x,
${getDateSQL('created_at', unit, timezone)} t,
count(*) y
from (
select arrayJoin(event_name) as event_name,
created_at
from 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}
) as g
group by x, t
order by t
`;
}
return rawQuery(sql, params);
group by x
order by y desc
limit ${limit}
offset ${offset}
`,
params,
);
}

View file

@ -0,0 +1,92 @@
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, WebsiteEventMetric } from '@/lib/types';
export async function getEventStats(
...args: [websiteId: string, filters: QueryFilters]
): Promise<WebsiteEventMetric[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = prisma;
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.customEvent,
});
return rawQuery(
`
select
event_name x,
${getDateSQL('website_event.created_at', unit, timezone)} t,
count(*) y
from website_event
${cohortQuery}
${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
order by 2
`,
params,
);
}
async function clickhouseQuery(
websiteId: string,
filters: QueryFilters,
): Promise<{ x: string; t: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
eventType: EVENT_TYPE.customEvent,
});
let sql = '';
if (filterQuery || cohortQuery) {
sql = `
select
event_name x,
${getDateSQL('created_at', unit, timezone)} t,
count(*) y
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
${filterQuery}
group by x, t
order by t
`;
} else {
sql = `
select
event_name x,
${getDateSQL('created_at', unit, timezone)} t,
count(*) y
from (
select arrayJoin(event_name) as event_name,
created_at
from 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}
) as g
group by x, t
order by t
`;
}
return rawQuery(sql, params);
}