diff --git a/features/platform-analytics/backend-api/src/seeds/seed-engagement.ts b/features/platform-analytics/backend-api/src/seeds/seed-engagement.ts new file mode 100644 index 000000000..be58cb04f --- /dev/null +++ b/features/platform-analytics/backend-api/src/seeds/seed-engagement.ts @@ -0,0 +1,122 @@ +/** + * Targeted re-seed for engagement_metrics only. + * Run from project root: bun run src/seeds/seed-engagement.ts + */ +import 'reflect-metadata' +import { DataSource } from 'typeorm' +import { randomUUID } from 'crypto' + +import { EngagementMetric, MetricType as EngagementMetricType, TargetType } from '../entities/engagement-metric.entity' + +const DS = new DataSource({ + type: 'postgres', + host: process.env.DB_HOST ?? 'localhost', + port: Number(process.env.DB_PORT ?? '25434'), + username: process.env.DB_USER ?? 'lilith', + password: process.env.DB_PASSWORD ?? 'analytics_dev_password', + database: process.env.DB_NAME ?? 'lilith_analytics', + entities: [EngagementMetric], + synchronize: false, + logging: false, +}) + +function createRng(seed: number) { + let s = seed >>> 0 + return { + next(): number { + s += 0x6d2b79f5 + let t = Math.imul(s ^ (s >>> 15), 1 | s) + t ^= t + Math.imul(t ^ (t >>> 7), 61 | t) + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + }, + int(min: number, max: number): number { + return min + Math.floor(this.next() * (max - min + 1)) + }, + pick(arr: readonly T[]): T { + return arr[Math.floor(this.next() * arr.length)] + }, + } +} + +const rng = createRng(0xdeadbeef) +const NOW = Date.now() + +const PROVIDER_USER_IDS = [ + 'a0000002-0000-0000-0000-000000000001', + 'a0000002-0000-0000-0000-000000000002', + 'a0000002-0000-0000-0000-000000000003', + 'a0000002-0000-0000-0000-000000000004', + 'a0000002-0000-0000-0000-000000000005', + 'a0000002-0000-0000-0000-000000000006', + 'a0000002-0000-0000-0000-000000000007', + 'a0000002-0000-0000-0000-000000000008', + 'a0000002-0000-0000-0000-000000000009', + 'a0000002-0000-0000-0000-00000000000a', + 'a0000002-0000-0000-0000-00000000000b', + 'a0000002-0000-0000-0000-00000000000c', +] as const + +async function main(): Promise { + process.stdout.write('Connecting to lilith_analytics...\n') + await DS.initialize() + + const existing = await DS.getRepository(EngagementMetric).count() + if (existing > 0) { + process.stdout.write(`engagement_metrics already has ${existing} rows. Truncating...\n`) + await DS.query('TRUNCATE TABLE engagement_metrics') + } + + const SESSION_COUNT = 120 + const contentIds = Array.from({ length: 40 }, (_, i) => + `c${String(i + 1).padStart(7, '0')}-0000-0000-0000-000000000000`, + ) + const metricTypes = [ + EngagementMetricType.VIEW, + EngagementMetricType.LIKE, + EngagementMetricType.FAVORITE, + EngagementMetricType.SHARE, + ] as const + + const repo = DS.getRepository(EngagementMetric) + const rows: EngagementMetric[] = [] + let rowId = 1 + + for (let s = 0; s < SESSION_COUNT; s++) { + const sessionId = randomUUID() + const userId = rng.next() > 0.3 ? rng.pick(PROVIDER_USER_IDS) : null + const sessionStart = new Date(NOW - rng.int(0, 90) * 86400000 - rng.int(0, 86400) * 1000) + const eventCount = rng.int(5, 30) + const sessionDurationMs = rng.int(2 * 60 * 1000, 30 * 60 * 1000) + + for (let e = 0; e < eventCount; e++) { + const offsetMs = Math.floor((e / Math.max(eventCount - 1, 1)) * sessionDurationMs) + const timestamp = new Date(sessionStart.getTime() + offsetMs) + rows.push( + repo.create({ + id: String(rowId++), + timestamp, + userId: userId ?? undefined, + sessionId, + metricType: e === 0 ? EngagementMetricType.VIEW : rng.pick(metricTypes), + targetId: rng.pick(contentIds), + targetType: e % 7 === 0 ? TargetType.PROFILE : TargetType.CONTENT, + value: 1, + metadata: {}, + }), + ) + } + } + + const CHUNK = 500 + for (let i = 0; i < rows.length; i += CHUNK) { + await repo.save(rows.slice(i, i + CHUNK)) + } + + process.stdout.write(`✓ ${rows.length} engagement metrics across ${SESSION_COUNT} sessions\n`) + await DS.destroy() +} + +main().catch((err) => { + process.stderr.write(`Seed failed: ${String(err)}\n`) + process.exit(1) +})