feat(platform-analytics): Add synthetic engagement seed data for analytics testing and initial database population

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-19 21:20:51 -07:00
parent 40dc1004df
commit f44717499f

View file

@ -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<T>(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<void> {
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)
})