fix(db): make seed script deterministic and produce realistic funnels
Addresses part of #969
Problem
bun run db:seed has two real bugs, separate from the larger scenario-engine proposal in #969:
- It never calls
faker.seed(), so two runs with the same arguments produce different data — no way to diff output for screenshot comparisons or reproduce a specific dataset. - Paths are picked independently per event, so any funnel or goal built on the seeded data shows near-zero, meaningless conversion — there's no correlation between the pages one visitor views in sequence.
Fix
- Added a
--seed <n>CLI flag (default42), applied viafaker.seed()before any faker call, including the module-levelUSER_POOL/SESSION_POOLgeneration. Existing positional usage (db:seed <WEBSITE_ID> [EVENT_COUNT]) is untouched — the flag is stripped fromprocess.argvbefore positional parsing so both forms compose. - Replaced the independent per-event random path pick with a weighted page-transition table and per-session path tracking, so a stable share of visitors who view one page go on to view a related next page (e.g. pricing → signup), instead of i.i.d. random paths.
Verification
bun run check-types(36 packages) andbun run lint(ultracite + lint:policies): clean.packages/db's own test suite: 115 pass, 0 fail.- Full monorepo pre-push suite: 1101 pass, 0 fail.
- Determinism: ran the seed script twice with the same
--seed, different website ids, then queried ClickHouse for the full event row shape ordered by time — 300/300 rows byte-identical between runs. - Funnel realism: seeded 20,000 events and queried ClickHouse — sessions that viewed
/pricingthen later/signupin the same session: 24.9% (686/2752), vs. 16.8% baseline/signupvisitation across all sessions./signup→/dashboard: 21.7% vs. 12% baseline. Confirms funnels built on these paths now show non-degenerate conversion.
Explicitly out of scope
This is two objective bug fixes to the existing script, not the scenario-engine redesign proposed in #969 (SaaS journey model, custom events, revenue, profiles, uptime/alarms, flags, links, or reconciling with the separate e2e ClickHouse generator). That's gated on an open product question in the issue ("Is SaaS the right story, or e-commerce?") only the maintainer can answer. packages/db/src/seed.ts also still has no DB-target safety guard of its own (packages/db/src/test-env.ts's loopback guard is scoped to test bootstrap, not this script) — adding one is part of that larger proposal, not one of the two bugs here, so I left it out; verification above was done against localhost only.
Disclosure: implemented with AI assistance (Claude Code); verified and tested by hand — ran the seed script against local Postgres/ClickHouse, confirmed byte-identical output across two runs with the same --seed, and confirmed pricing→signup and signup→dashboard conversion lift via ClickHouse queries before submitting.
Summary by cubic
Makes bun run db:seed deterministic and gives seeded visitors realistic page-to-page sequences so funnels and goals built on seeded data show meaningful conversion.
- Adds a
--seed <n>CLI flag (default42) applied viafaker.seed()before any faker call, so two runs with the same arguments produce identical data. Existing positional usage is untouched. - Replaces independent per-event random path picks with a weighted page-transition table and per-session path tracking, so visitors who view one page have a stable chance of viewing a related next page (e.g. pricing → signup).
- Fixes
created_atto use the event's own time instead of a per-eventDate.now()call, and moves flag parsing and path selection intoseed-helpers.tswith unit tests.