- Browser beacon
components/analytics-tracker.tsx - Page views, scroll depth, click cells and outbound clicks. Honours DNT and Sec-GPC before it collects anything.
- Ingest
app/api/analytics/collect/route.ts - Anonymous, rate-limited, and everything it receives is re-normalised server-side. Writes happen in after(), off the response path.
- PageView ledger
prisma/schema.prisma - One row per view, keyed by an idempotent viewId. No IP; the visitor pseudonym is a salted hash that rotates every UTC day.
- AnalyticsEvent ledger
prisma/schema.prisma - Clicks, downloads, outbound navigations and rage clicks.
- Rolled-up counters
lib/analytics/collect.ts - Denormalised totals per (kind, key), so the dashboard never groups over every event ever recorded.
- Heatmap cells
lib/analytics/collect.ts - A sparse counter per grid cell. Never raw points — an exact (x, y, timestamp) trail is a behavioural fingerprint.
- Retention prune
app/api/cron/prune-analytics/route.ts - Deletes ledger rows past 180 days in bounded batches. Aggregates are kept — they are the safe form.
- Warehouse export
lib/dataeng/pipeline.ts - Aggregates by day, path, country and device in Postgres. Nothing per-visitor leaves the database.
- DuckDB in the browser
components/dataeng/olap-console.tsx - The export is queried with real SQL on the visitor's machine. No query reaches a server.
- Admin dashboard
lib/analytics/report.ts - The read model. Reads the ledgers and counters; never writes.
- Live dashboard
lib/realtime/live.ts - The same ledgers over a narrow window, so live and daily cannot disagree.