analytics: make each weekly funnel stage separately measurable (FRE-366)

Gap 1 — click and served share one label: trackDownload now takes a
stage ("click" | "served") carried on a new VisitorAnalytics.stage
column; the Nook door and the tRPC proxy record clicks, the DMG file
route records served. summariseDownloads() and the /analytics downloads
block are untouched — the stage is a separate column, so the path
grouping cannot change.

Gap 2 — checkout start records nothing: the checkout route records a
created Stripe session through the shared trackRequest writer (same
referrer, campaign, country dimensions as every door) under its own
path, and STAGES.checkout reads it instead of the /checkout page proxy.

Gap 3 — loopback addresses, no host dimension: every writer now
resolves the client address through resolveClientIp, which trusts
x-forwarded-for only when the direct peer is the loopback production
proxy (trust boundary stated in request-client.ts), and records the
host on each row through the same site-context extractor routing uses.
stage and host reach the table through VISITOR_ANALYTICS_ADDED_COLUMNS;
the weekly job applies the same bootstrap before reading so it renders
seven stages against a database no deploy has migrated yet.

The weekly read splits downloadClick/dmgServed, scopes visits to
nook.freno.me, and computes its caveats from the window: pre-instrumentation
rows are stated and excluded, and the loopback warning now fires only
when rows recorded since the instrumentation still carry it.
This commit is contained in:
Paperclip Agent
2026-10-29 17:02:39 +00:00
parent 89f56fc78a
commit 39d874a7e2
19 changed files with 643 additions and 221 deletions

View File

@@ -38,9 +38,10 @@ export interface ServerSiteEvent {
* the raw Node request via `event.nativeEvent.node.req.headers`. We try
* both so this works for API route handlers (`APIEvent`), the SSR document
* handler (`PageEvent`), and tRPC procedures operating on the underlying
* `H3Event`.
* `H3Event`. Also used by the analytics writers to record the site dimension
* (`host` column) on every row (FRE-366).
*/
function hostFromEventLike(event: ServerSiteEvent | unknown): string | null {
export function hostFromEventLike(event: ServerSiteEvent | unknown): string | null {
// 1) Web FetchEvent / APIEvent / PageEvent → standard `Request` headers.
try {
const req = (event as { request?: Request } | null)?.request;