diff --git a/apps/track/package.json b/apps/track/package.json new file mode 100644 index 0000000..a7f924a --- /dev/null +++ b/apps/track/package.json @@ -0,0 +1,19 @@ +{ + "name": "@stack/track", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "bun --watch run ./src/index.ts", + "lint:fix": "prettier --check . && eslint . --fix" + }, + "dependencies": { + "@clickhouse/client": "1.4.1", + "@hattip/adapter-bun": "0.0.47", + "@hattip/response": "0.0.47", + "@hattip/router": "0.0.47" + }, + "devDependencies": { + "@stack/typescript-config": "workspace:*", + "@types/bun": "1.1.6" + } +} diff --git a/apps/track/src/client.ts b/apps/track/src/client.ts new file mode 100644 index 0000000..6375a9f --- /dev/null +++ b/apps/track/src/client.ts @@ -0,0 +1,42 @@ +import { createClient } from '@clickhouse/client'; + +import { PAGE_VIEWS_TABLE } from './db'; + +type Statistics = { + bytes_read: number; + elapsed: number; + rows_read: number; +}; + +type CHResult> = { + data: Data[]; + meta: Meta[]; + rows: number; + statistics: Statistics; +}; + +export const initClient = async () => { + const client = createClient({ + password: process.env['DB_PASSOWRD'], + url: process.env['DB_URL'] + }); + + // Check if the database contains all the tables + const row = await client.query({ + query: 'show tables from default' + }); + const json = (await row.json()) as CHResult<{ name: string }>; + if (!json.data || json.data.length === 0) { + // TODO: Create the different tables + await client.query({ + query: PAGE_VIEWS_TABLE + }); + } else { + console.log( + '[Server] Fond tables', + `[${json.data.map((t) => t.name).join(', ')}]` + ); + } + + return client; +}; diff --git a/apps/track/src/db.ts b/apps/track/src/db.ts new file mode 100644 index 0000000..84c066b --- /dev/null +++ b/apps/track/src/db.ts @@ -0,0 +1,81 @@ +export const PAGE_VIEWS_TABLE = ` +CREATE TABLE page_views ( + event_id UUID DEFAULT generateUUIDv4(), + app_name String, + user_id String, + session_id String, + page_url String, + referrer_url String, + timestamp DateTime DEFAULT now() +) ENGINE = MergeTree() +ORDER BY (app_name, timestamp, user_id) +PARTITION BY toYYYYMM(timestamp) +TTL timestamp + INTERVAL 1 YEAR +SETTINGS index_granularity = 8192; +`; + +export const USER_ACTIONS_TABLE = ` +CREATE TABLE user_actions ( + event_id UUID DEFAULT generateUUIDv4(), + app_name String, + user_id String, + session_id String, + action_type String, + action_details String, + timestamp DateTime DEFAULT now() +) ENGINE = MergeTree() +ORDER BY (app_name, timestamp, user_id, action_type) +PARTITION BY toYYYYMM(timestamp) +TTL timestamp + INTERVAL 1 YEAR +SETTINGS index_granularity = 8192; +`; + +export const SESSIONS_TABLE = ` +CREATE TABLE conversions ( + event_id UUID DEFAULT generateUUIDv4(), + app_name String, + user_id String, + session_id String, + conversion_type String, + conversion_value Float32, + timestamp DateTime DEFAULT now() +) ENGINE = MergeTree() +ORDER BY (app_name, timestamp, user_id, conversion_type) +PARTITION BY toYYYYMM(timestamp) +TTL timestamp + INTERVAL 1 YEAR +SETTINGS index_granularity = 8192; +`; + +export const ERROR_TABLE = ` +CREATE TABLE errors ( + event_id UUID DEFAULT generateUUIDv4(), + app_name String, + user_id String, + session_id String, + error_type String, + error_message String, + stack_trace String, + page_url String, + timestamp DateTime DEFAULT now() +) ENGINE = MergeTree() +ORDER BY (app_name, timestamp, user_id, error_type) +PARTITION BY toYYYYMM(timestamp) +TTL timestamp + INTERVAL 1 YEAR +SETTINGS index_granularity = 8192; +`; + +export const USER_FLOW_TALBE = ` +CREATE TABLE user_flow ( + event_id UUID DEFAULT generateUUIDv4(), + app_name String, + user_id String, + session_id String, + from_page String, + to_page String, + timestamp DateTime DEFAULT now() +) ENGINE = MergeTree() +ORDER BY (app_name, timestamp, user_id, from_page) +PARTITION BY toYYYYMM(timestamp) +TTL timestamp + INTERVAL 1 YEAR +SETTINGS index_granularity = 8192; +`; diff --git a/apps/track/src/index.ts b/apps/track/src/index.ts new file mode 100644 index 0000000..86ff8bb --- /dev/null +++ b/apps/track/src/index.ts @@ -0,0 +1,15 @@ +// entry-node.js +import adapter from '@hattip/adapter-bun'; + +import { initClient } from './client'; +import { router } from './routes'; + +const client = await initClient(); +client.close(); + +// export default router; +export default adapter( + router, + // @ts-expect-error + { port: 3001 } +); diff --git a/apps/track/src/routes/events/action.ts b/apps/track/src/routes/events/action.ts new file mode 100644 index 0000000..789a6cf --- /dev/null +++ b/apps/track/src/routes/events/action.ts @@ -0,0 +1,13 @@ +import type { TrackRouter } from '../router'; + +import { eventRoute } from './shared'; + +export const actionRoute = (router: TrackRouter) => { + router.get( + eventRoute('action'), + async () => + new Response(undefined, { + status: 200 + }) + ); +}; diff --git a/apps/track/src/routes/events/error.ts b/apps/track/src/routes/events/error.ts new file mode 100644 index 0000000..e6c9a3f --- /dev/null +++ b/apps/track/src/routes/events/error.ts @@ -0,0 +1,13 @@ +import type { TrackRouter } from '../router'; + +import { eventRoute } from './shared'; + +export const errorRoute = (router: TrackRouter) => { + router.get( + eventRoute('error'), + async () => + new Response(undefined, { + status: 200 + }) + ); +}; diff --git a/apps/track/src/routes/events/index.ts b/apps/track/src/routes/events/index.ts new file mode 100644 index 0000000..8f5047b --- /dev/null +++ b/apps/track/src/routes/events/index.ts @@ -0,0 +1,8 @@ +import type { TrackRouter } from '../router'; + +import { pageViewRote } from './page-view'; + +export const addEventsRoutes = (router: TrackRouter) => { + // TODO: Add other routers + pageViewRote(router); +}; diff --git a/apps/track/src/routes/events/page-view.ts b/apps/track/src/routes/events/page-view.ts new file mode 100644 index 0000000..0f5e2cd --- /dev/null +++ b/apps/track/src/routes/events/page-view.ts @@ -0,0 +1,13 @@ +import type { TrackRouter } from '../router'; + +import { eventRoute } from './shared'; + +export const pageViewRote = (router: TrackRouter) => { + router.get( + eventRoute('page-view'), + async () => + new Response(undefined, { + status: 200 + }) + ); +}; diff --git a/apps/track/src/routes/events/session.ts b/apps/track/src/routes/events/session.ts new file mode 100644 index 0000000..d6b9e98 --- /dev/null +++ b/apps/track/src/routes/events/session.ts @@ -0,0 +1,23 @@ +import type { TrackRouter } from '../router'; + +import { eventRoute } from './shared'; + +export const startSessionRoute = (router: TrackRouter) => { + router.get( + eventRoute('session-start'), + async () => + new Response(undefined, { + status: 200 + }) + ); +}; + +export const endSessionRoute = (router: TrackRouter) => { + router.get( + eventRoute('session-end'), + async () => + new Response(undefined, { + status: 200 + }) + ); +}; diff --git a/apps/track/src/routes/events/shared.ts b/apps/track/src/routes/events/shared.ts new file mode 100644 index 0000000..ba5f4ae --- /dev/null +++ b/apps/track/src/routes/events/shared.ts @@ -0,0 +1 @@ +export const eventRoute = (route: string) => `/events/${route}`; diff --git a/apps/track/src/routes/events/user-flow.ts b/apps/track/src/routes/events/user-flow.ts new file mode 100644 index 0000000..7fce8d4 --- /dev/null +++ b/apps/track/src/routes/events/user-flow.ts @@ -0,0 +1,13 @@ +import type { TrackRouter } from '../router'; + +import { eventRoute } from './shared'; + +export const userFlowRoute = (router: TrackRouter) => { + router.get( + eventRoute('flow'), + async () => + new Response(undefined, { + status: 200 + }) + ); +}; diff --git a/apps/track/src/routes/index.ts b/apps/track/src/routes/index.ts new file mode 100644 index 0000000..c8f9256 --- /dev/null +++ b/apps/track/src/routes/index.ts @@ -0,0 +1 @@ +export { default as router } from './router'; diff --git a/apps/track/src/routes/router.ts b/apps/track/src/routes/router.ts new file mode 100644 index 0000000..9ce47e7 --- /dev/null +++ b/apps/track/src/routes/router.ts @@ -0,0 +1,13 @@ +import type { BunPlatformInfo } from '@hattip/adapter-bun'; +import type { Router } from '@hattip/router'; + +import { createRouter } from '@hattip/router'; + +import { addEventsRoutes } from './events'; + +const router = createRouter(); + +addEventsRoutes(router); + +export type TrackRouter = Router; +export default router.buildHandler(); diff --git a/apps/track/tsconfig.json b/apps/track/tsconfig.json new file mode 100644 index 0000000..04d874b --- /dev/null +++ b/apps/track/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@stack/typescript-config/server.json" +} diff --git a/bun.lockb b/bun.lockb index 5715442..77e957c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/config-typescript/server.json b/packages/config-typescript/server.json new file mode 100644 index 0000000..40e3270 --- /dev/null +++ b/packages/config-typescript/server.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "allowJs": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noPropertyAccessFromIndexSignature": true + } +}