forked from Mike/freno-dev
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:
@@ -3,6 +3,7 @@ import { env } from "~/env/server";
|
||||
import { TURNSTILE_CONFIG } from "~/config";
|
||||
import { verifyTurnstileToken } from "~/server/fetch-utils";
|
||||
import { json, error } from "./_lib";
|
||||
import { trackRequest } from "~/server/track-download";
|
||||
|
||||
/**
|
||||
* POST /api/the-nook/checkout
|
||||
@@ -12,6 +13,14 @@ import { json, error } from "./_lib";
|
||||
* session for the one-time $10 The Nook license. Returns the hosted
|
||||
* checkout URL for the client to redirect to.
|
||||
*
|
||||
* Analytics: a created session is the funnel's "checkout started" stage
|
||||
* (FRE-366). Until then the only trace was the client-side /checkout page
|
||||
* view — a page, not a started checkout. Recorded through the shared
|
||||
* `trackRequest` writer (referrer, campaign tags, country, real client
|
||||
* address, host) under this route's own path, best-effort: a failed
|
||||
* analytics write never blocks a checkout, and an attempt Turnstile or
|
||||
* Stripe rejects is correctly not a started checkout.
|
||||
*
|
||||
* Uses raw `fetch` (Node 24 / Vercel Node functions have global fetch) — no
|
||||
* `stripe` npm dependency added.
|
||||
*/
|
||||
@@ -67,5 +76,14 @@ export async function POST(event: APIEvent) {
|
||||
if (!data.url) {
|
||||
return error("Stripe returned no checkout URL", 502);
|
||||
}
|
||||
|
||||
// The session exists: that is "checkout started". Awaited so the row is
|
||||
// flushed before the response — a buffered write is not guaranteed to
|
||||
// survive the response on serverless.
|
||||
await trackRequest(event, {
|
||||
path: "/api/the-nook/checkout",
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
return json({ checkoutUrl: data.url });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user