diff --git a/db/mysql/migrations/11_add_segment/migration.sql b/db/mysql/migrations/11_add_segment/migration.sql new file mode 100644 index 00000000..c79e916d --- /dev/null +++ b/db/mysql/migrations/11_add_segment/migration.sql @@ -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; diff --git a/db/mysql/migrations/12_update_report_parameter/migration.sql b/db/mysql/migrations/12_update_report_parameter/migration.sql new file mode 100644 index 00000000..f6a99c3f --- /dev/null +++ b/db/mysql/migrations/12_update_report_parameter/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `report` MODIFY `parameters` JSON NOT NULL; diff --git a/db/mysql/migrations/13_add_revenue/migration.sql b/db/mysql/migrations/13_add_revenue/migration.sql new file mode 100644 index 00000000..96115a33 --- /dev/null +++ b/db/mysql/migrations/13_add_revenue/migration.sql @@ -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; diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma index 2a5513ab..67bd24d2 100644 --- a/db/mysql/schema.prisma +++ b/db/mysql/schema.prisma @@ -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") +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a25a6594..bd8c46f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/scripts/data-migrations/populate-revenue-table.sql b/scripts/data-migrations/populate-revenue-table.sql new file mode 100644 index 00000000..9df75189 --- /dev/null +++ b/scripts/data-migrations/populate-revenue-table.sql @@ -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%'; \ No newline at end of file diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx index 285a230e..46970b51 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx @@ -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 ( <> @@ -34,11 +40,7 @@ export default function EventsPage({ websiteId }) { />
- setTab(value)} - style={{ marginBottom: 30 }} - > + {formatMessage(labels.activity)} {formatMessage(labels.properties)} diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts index 5b5bc88c..a30741c6 100644 --- a/src/app/api/websites/[websiteId]/events/series/route.ts +++ b/src/app/api/websites/[websiteId]/events/series/route.ts @@ -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); } diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts index 5bc4e522..488f85fc 100644 --- a/src/app/api/websites/[websiteId]/metrics/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -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); } diff --git a/src/queries/index.ts b/src/queries/index.ts index b9495bcd..0d57b294 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -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'; diff --git a/src/queries/sql/events/getEventMetrics.ts b/src/queries/sql/events/getEventMetrics.ts index 42dc8862..b028d759 100644 --- a/src/queries/sql/events/getEventMetrics.ts +++ b/src/queries/sql/events/getEventMetrics.ts @@ -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 { + ...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, + ); } diff --git a/src/queries/sql/events/getEventStats.ts b/src/queries/sql/events/getEventStats.ts new file mode 100644 index 00000000..56f31363 --- /dev/null +++ b/src/queries/sql/events/getEventStats.ts @@ -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 { + 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); +}