Skip to content

feat(db): deterministic full-scenario seed shared by local dev and dashboard e2e #969

Open

Description

@FindMalek

@izadoesdev

Problem

Most dashboard pages can't be checked locally without setting data up by hand:

  • bun run db:seed <WEBSITE_ID> [EVENT_COUNT] (packages/db/src/seed.ts) writes only events, outgoing_links, error_spans, and web_vitals_spans. It creates no custom events, revenue, profiles, or uptime history, and no Postgres rows, so goals, funnels, flags, monitors, alarms, and links pages stay empty.
  • Paths are picked at random per event, so any funnel or goal defined on top of the data shows near-zero, meaningless conversion.
  • It never calls faker.seed, so every run produces different data and screenshots can't be compared.
  • Dashboard e2e has a second, separate generator in apps/dashboard/app/api/test/e2e/clickhouse/route.ts, so the two drift independently.

Proposal

One deterministic scenario engine in @databuddy/db, used by both the CLI and e2e:

bun run db:seed --scenario saas --email you@example.com [--size small|large] [--days 90] [--seed 42] [--reset]
  • Ownership: attaches a "Demo" organization and website to an existing local user found by email, so it never bypasses Better Auth. The current db:seed <WEBSITE_ID> [EVENT_COUNT] form keeps working.
  • A coherent story: a fictional SaaS whose sessions follow a journey model (landing → pricing → signup → onboarding → upgrade) with realistic drop-off, so seeded goals and funnels show real conversion. On top of that:
    • matching custom events and revenue for upgrades
    • identified profiles
    • an error spike after a deploy annotation
    • web vitals per page
    • an uptime monitor with history and one incident, plus alarms
    • flags, links, and AI crawler traffic
  • Relative to now: dates are anchored to the current time so default date ranges always show data, and a fixed --seed gives identical output across runs.
  • Safe: refuses to run unless DATABASE_URL and CLICKHOUSE_URL point at a loopback host, like packages/db/src/test-env.ts does.
  • Can't rot silently: inserts are typed against the Drizzle schema and ClickHouse row types, and an integration test runs the small scenario against the test databases in CI.

Delivery plan

  1. Engine plus web analytics: events, custom events, goals, funnels, revenue, profiles, errors, vitals.
  2. Uptime, alarms, status page, links, and flags.
  3. Point the e2e ClickHouse route at the engine and delete its generator.

Questions

  • Is SaaS the right story, or would you rather have e-commerce?
  • Are two sizes (small for UI work, large for query performance) worth it, or is one enough?
  • Should step 3 happen at all, or do you want e2e data kept separate on purpose?

Disclosure: I used Claude Code to compare the seed and e2e generators and to draft this write-up; I verified the file references by hand.

Activity

  1. FindMalek commented on Sep 28, 2026

    @FindMalek
    ContributorAuthor

    How I plan to solve this, slice 1 (the engine plus web analytics):

    Layout: packages/db/src/seed/

    • scenario.ts: a journey model for a fictional SaaS. Sessions move between states (landing → pricing → signup → onboarding → first report → upgrade) with per-step continue probabilities, plus acquisition-source mix, device/geo pools, and weekday seasonality.
    • postgres.ts: typed Drizzle inserts for the demo organization, member, website, goals, funnelDefinitions, and annotations.
    • clickhouse.ts: events, custom_events, revenue, error_spans, web_vitals_spans, and outgoing_links rows generated from the same sessions, so goals, funnels, revenue, and errors all describe one consistent story, including an error spike right after a "deploy" annotation.

    Command: bun run db:seed --scenario saas --email you@example.com [--size small|large] [--days 90] [--seed 42] [--reset]

    • It uses faker.seed(seed) and a single "now" anchor, so repeated runs produce identical data that always lands in default date ranges.
    • --reset removes only the demo website's rows.
    • The existing db:seed <WEBSITE_ID> [EVENT_COUNT] form keeps working.
    • It refuses to run unless DATABASE_URL and CLICKHOUSE_URL point at a loopback host.

    Test

    • An integration test seeds the small scenario into the test databases, then runs the real queries: processFunnelAnalytics must show conversion inside the scenario's expected band, goal completions must match the seeded journey, and ClickHouse revenue must equal the seeded upgrades.
    • That checks the scenario is coherent, not just that rows exist.
    • packages/db gets a test:integration script, so the repo's turbo test:integration task runs it in CI and the seed can't rot.

    Slice 2 adds uptime history in uptime.uptime_monitor with an incident, alarms, a status page, links with link_visits, flags, and AI crawler traffic. Slice 3 points apps/dashboard/app/api/test/e2e/clickhouse/route.ts at the engine and deletes its generator.

  2. FindMalek commented on Sep 28, 2026

    @FindMalek
    ContributorAuthor

    Two refinements after checking the details.

    Attaching to an existing user is required, not just simpler. @databuddy/auth depends on @databuddy/db, so a seed engine inside packages/db can't call Better Auth to create users. It attaches the demo organization to an account that already signed up locally.

    --reset has to account for materialized views. Five views feed three tables:

    • daily_pageviews, a SummingMergeTree: re-inserting after deleting only from events would double every total
    • identity_anon_pairs and identity_session_pairs, plain MergeTree fed from both events and custom_events: the seeded profiles land here, and stale pairs would otherwise stick around

    So --reset deletes the demo website in Postgres (cascading its goals, funnels and annotations) and seeds under a fresh website id, rather than running slow ALTER TABLE ... DELETE mutations across every table. The unreachable old rows can be dropped later with an optional --purge. The e2e generator never sets profile_id, so its current cleanup doesn't need the identity tables.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions