import { beforeEach, describe, expect, it } from "bun:test"; import { Database } from "bun:sqlite"; import { VISITOR_ANALYTICS_ADDED_COLUMNS, VISITOR_ANALYTICS_COLUMNS, VISITOR_ANALYTICS_DDL, VISITOR_ANALYTICS_INSERT } from "./analytics-schema"; import { NOOK_CHECKOUT_PATH, NOOK_DOWNLOAD_PATH, NOOK_HOST, ensureAnalyticsSchema, getWeeklyReport, pickMover, readCampaigns, readFunnel, readTraffic, renderWeeklyMarkdown, windowsFor, type Connections, type FunnelCounts, type MetricsConn } from "./weekly-metrics"; // Same reasoning as the backfill suite: the queries run real SQL, so an // in-memory SQLite shaped like production is the only honest stand-in. A stub // that returned canned rows would pass with a column or a label the live table // does not have. const NOOK_DDL = [ `CREATE TABLE trials ( fingerprint TEXT PRIMARY KEY, started_at TEXT NOT NULL, email TEXT )`, `CREATE TABLE licenses ( id TEXT PRIMARY KEY, key TEXT UNIQUE NOT NULL, email TEXT NOT NULL, stripe_session_id TEXT UNIQUE NOT NULL, created_at TEXT NOT NULL, revoked INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE activations ( id TEXT PRIMARY KEY, license_id TEXT NOT NULL, device_fingerprint TEXT NOT NULL, device_name TEXT NOT NULL, activated_at TEXT NOT NULL, deactivated_at TEXT )` ]; function adapter(db: Database): MetricsConn { return { async execute(query) { const args = (query.args ?? []) as never[]; const statement = db.query(query.sql); if (/^\s*(select|pragma|with)\b/i.test(query.sql)) { return { rows: statement.all(...args) as unknown[], rowsAffected: 0 }; } return { rows: [] as unknown[], rowsAffected: statement.run(...args).changes }; } }; } let analyticsDb: Database; let nookDb: Database; let conns: Connections; const NOW = new Date("2026-10-29T16:00:00Z"); // A row as the post-FRE-366 writers record it: real client address, the host // it addressed, and a stage label where the row is a download. const row = ( overrides: Record ): Record => ({ id: crypto.randomUUID(), user_id: null, path: "/", method: "GET", referrer: null, user_agent: "Mozilla/5.0 (Macintosh)", // What the writers store since the FRE-369 privacy floor: a /24 (or /56 for // IPv6), never the address itself. ip_address: "203.0.113.0", country: "GB", device_type: "desktop", browser: "safari", os: "macos", utm_source: null, utm_medium: null, utm_campaign: null, duration_ms: null, fcp: null, lcp: null, cls: null, fid: null, inp: null, ttfb: null, dom_load: null, load_complete: null, stage: null, host: NOOK_HOST, ...overrides }); function insertAnalytics(overrides: Record): void { const values = row(overrides); analyticsDb .prepare(VISITOR_ANALYTICS_INSERT) .run(...(VISITOR_ANALYTICS_COLUMNS.map((column) => values[column]) as never[])); // `created_at` is not in VISITOR_ANALYTICS_COLUMNS (it defaults to now), so // the window each row belongs to has to be set after the insert. if (values.created_at) { analyticsDb .prepare(`UPDATE VisitorAnalytics SET created_at = ? WHERE id = ?`) .run(values.created_at as string, values.id as string); } } // Inside the current window for NOW - 7 days: 2026-10-22 16:00 → 2026-10-29 16:00. const THIS_WEEK = "2026-10-27 10:00:00"; // Inside the previous window: 2026-10-15 16:00 → 2026-10-22 16:00. const LAST_WEEK = "2026-10-18 10:00:00"; beforeEach(() => { analyticsDb = new Database(":memory:"); analyticsDb.run(VISITOR_ANALYTICS_DDL); nookDb = new Database(":memory:"); for (const ddl of NOOK_DDL) nookDb.run(ddl); conns = { analytics: adapter(analyticsDb), nook: adapter(nookDb) }; }); describe("windowsFor", () => { it("puts the previous window directly before the current one", () => { const windows = windowsFor(NOW, 7); expect(windows.current).toEqual({ start: "2026-10-22 16:00:00", end: "2026-10-29 16:00:00" }); expect(windows.previous).toEqual({ start: "2026-10-15 16:00:00", end: "2026-10-22 16:00:00" }); }); }); describe("readFunnel", () => { it("counts each stage by its own label inside its window", async () => { // Page views on the Nook host count; other sites, API calls, legacy // rows with no host, and the other window do not. insertAnalytics({ path: "/", created_at: THIS_WEEK }); insertAnalytics({ path: "/downloads", created_at: THIS_WEEK }); insertAnalytics({ path: "/", host: "freno.me", created_at: THIS_WEEK }); insertAnalytics({ path: "/", host: null, created_at: THIS_WEEK }); insertAnalytics({ path: "/api/trpc/anything", created_at: THIS_WEEK }); insertAnalytics({ path: "/", created_at: LAST_WEEK }); // The old checkout proxy: a /checkout page view is a visit, not a // started checkout. insertAnalytics({ path: "/checkout", created_at: THIS_WEEK }); // The download label splits by stage; a row with no stage predates the // split and counts in neither. insertAnalytics({ path: NOOK_DOWNLOAD_PATH, stage: "click", created_at: THIS_WEEK }); insertAnalytics({ path: NOOK_DOWNLOAD_PATH, stage: "served", created_at: THIS_WEEK }); insertAnalytics({ path: NOOK_DOWNLOAD_PATH, stage: "served", created_at: THIS_WEEK }); insertAnalytics({ path: NOOK_DOWNLOAD_PATH, stage: null, created_at: THIS_WEEK }); // A created Stripe session, recorded by the checkout route itself. insertAnalytics({ path: NOOK_CHECKOUT_PATH, method: "POST", created_at: THIS_WEEK }); // The licence DB stores ISO-8601 with milliseconds. nookDb .prepare(`INSERT INTO trials (fingerprint, started_at) VALUES (?, ?)`) .run("fp-1", "2026-10-28T20:28:46.806Z"); nookDb .prepare(`INSERT INTO trials (fingerprint, started_at) VALUES (?, ?)`) .run("fp-old", "2026-08-26T19:26:12.791Z"); nookDb .prepare( `INSERT INTO licenses (id, key, email, stripe_session_id, created_at) VALUES (?, ?, ?, ?, ?)` ) .run("lic-1", "NOOK-1", "buyer@example.com", "cs_1", "2026-10-25T12:00:00.000Z"); nookDb .prepare( `INSERT INTO activations (id, license_id, device_fingerprint, device_name, activated_at) VALUES (?, ?, ?, ?, ?)` ) .run("act-1", "lic-1", "device-1", "Hermes", "2026-10-25T12:05:00.000Z"); const window = windowsFor(NOW, 7).current; const funnel = await readFunnel(conns, window); expect(funnel).toEqual({ visit: 3, // "/", "/downloads" and the "/checkout" page view on the Nook host downloadClick: 1, dmgServed: 2, trial: 1, checkout: 1, purchase: 1, activation: 1 }); }); it("reports zeros against an empty database", async () => { const window = windowsFor(NOW, 7).current; expect(await readFunnel(conns, window)).toEqual({ visit: 0, downloadClick: 0, dmgServed: 0, trial: 0, checkout: 0, purchase: 0, activation: 0 }); }); }); describe("readTraffic", () => { it("separates legacy rows, live loopback and automation from clean traffic", async () => { // Post-instrumentation rows: host set. Two of them still loopback — in the // truncated shapes the privacy floor writes — which is the shape the report // must flag as "the fix is not live". insertAnalytics({ path: NOOK_DOWNLOAD_PATH, stage: "click", created_at: THIS_WEEK }); insertAnalytics({ path: NOOK_DOWNLOAD_PATH, stage: "served", ip_address: "127.0.0.0", user_agent: "The Nook/0.13.4 Sparkle/2.9.6", created_at: THIS_WEEK }); insertAnalytics({ path: "/", ip_address: "0:0:0:0:0:0:0:0", created_at: THIS_WEEK }); insertAnalytics({ path: "/", created_at: THIS_WEEK }); // Pre-instrumentation rows: no host, proxy's address — legacy, never // counted as "the fix is not live". insertAnalytics({ path: NOOK_DOWNLOAD_PATH, stage: null, host: null, ip_address: "127.0.0.1", user_agent: "curl/8.7.1", created_at: THIS_WEEK }); insertAnalytics({ path: "/", host: null, ip_address: "::1", created_at: THIS_WEEK }); const traffic = await readTraffic(conns, windowsFor(NOW, 7).current); expect(traffic).toEqual({ analyticsRows: 6, hostRows: 4, legacyRows: 2, hostLoopbackRows: 2, downloadRows: 3, downloadClickRows: 1, downloadServedRows: 1, downloadUnstagedRows: 1, downloadNonBrowserRows: 2 }); }); }); describe("readCampaigns", () => { it("groups tagged rows, excludes untagged, and orders by volume", async () => { insertAnalytics({ path: "/", utm_source: "hn", utm_medium: "post", utm_campaign: "launch", created_at: THIS_WEEK }); insertAnalytics({ path: "/", utm_source: "hn", utm_medium: "post", utm_campaign: "launch", created_at: THIS_WEEK }); insertAnalytics({ path: "/", utm_source: "r_macapps", utm_campaign: "week2", created_at: THIS_WEEK }); insertAnalytics({ path: "/", created_at: THIS_WEEK }); const campaigns = await readCampaigns(conns.analytics, windowsFor(NOW, 7).current); expect(campaigns).toEqual([ { source: "hn", medium: "post", campaign: "launch", count: 2 }, { source: "r_macapps", medium: null, campaign: "week2", count: 1 } ]); }); it("caps the list at ten rows", async () => { for (let i = 0; i < 12; i += 1) { insertAnalytics({ path: "/", utm_source: `source-${i}`, created_at: THIS_WEEK }); } const campaigns = await readCampaigns(conns.analytics, windowsFor(NOW, 7).current); expect(campaigns.length).toBe(10); }); }); describe("ensureAnalyticsSchema", () => { it("adds the post-hoc columns to a table that predates them, idempotently", async () => { // The table as production held it before any post-hoc column existed. // (`created_at` is not in the insert column list — it defaults DB-side — // so the simulated table has to name it explicitly.) const oldDb = new Database(":memory:"); const withoutAdded = [ ...VISITOR_ANALYTICS_COLUMNS.filter( (c) => !VISITOR_ANALYTICS_ADDED_COLUMNS.some((a) => a.name === c) ), "created_at" ]; oldDb.run( `CREATE TABLE VisitorAnalytics (${withoutAdded.map((c) => `${c} TEXT`).join(", ")})` ); const oldConn = adapter(oldDb); await ensureAnalyticsSchema(oldConn); await ensureAnalyticsSchema(oldConn); // a second run is a no-op // The writers' full INSERT — stage and host included — now works. const values = row({ stage: "click", path: NOOK_DOWNLOAD_PATH }); expect(() => oldDb .prepare(VISITOR_ANALYTICS_INSERT) .run( ...(VISITOR_ANALYTICS_COLUMNS.map((c) => values[c]) as never[]) ) ).not.toThrow(); oldDb .prepare(`UPDATE VisitorAnalytics SET created_at = ? WHERE id = ?`) .run(THIS_WEEK, values.id as string); // And the funnel reads its stage split from the migrated table. const funnel = await readFunnel( { analytics: oldConn, nook: conns.nook }, windowsFor(NOW, 7).current ); expect(funnel.downloadClick).toBe(1); }); }); describe("pickMover", () => { const base: FunnelCounts = { visit: 0, downloadClick: 0, dmgServed: 0, trial: 0, checkout: 0, purchase: 0, activation: 0 }; it("picks the stage with the largest absolute change", () => { const mover = pickMover( { ...base, visit: 61, dmgServed: 156, trial: 2 }, { ...base, visit: 56 } ); expect(mover).toEqual({ key: "dmgServed", label: "DMG served", now: 156, before: 0, delta: 156 }); }); it("breaks ties toward the earlier stage", () => { const mover = pickMover( { ...base, visit: 5, downloadClick: 5 }, { ...base, visit: 0, downloadClick: 0 } ); expect(mover?.key).toBe("visit"); }); it("returns null when nothing moved", () => { expect(pickMover({ ...base, visit: 3 }, { ...base, visit: 3 })).toBeNull(); }); }); describe("getWeeklyReport / renderWeeklyMarkdown", () => { it("renders seven distinct stages with no merged-label or proxy caveats", async () => { insertAnalytics({ path: "/", created_at: THIS_WEEK }); insertAnalytics({ path: NOOK_DOWNLOAD_PATH, stage: "click", created_at: THIS_WEEK }); insertAnalytics({ path: NOOK_DOWNLOAD_PATH, stage: "served", created_at: THIS_WEEK }); insertAnalytics({ path: NOOK_DOWNLOAD_PATH, stage: "served", created_at: THIS_WEEK }); insertAnalytics({ path: NOOK_CHECKOUT_PATH, method: "POST", created_at: THIS_WEEK }); nookDb .prepare(`INSERT INTO trials (fingerprint, started_at) VALUES (?, ?)`) .run("fp-1", "2026-10-28T20:28:46.806Z"); const report = await getWeeklyReport(conns, { now: NOW, days: 7 }); const markdown = renderWeeklyMarkdown(report); expect(markdown).toContain( "Window: **2026-10-22 16:00:00 → 2026-10-29 16:00:00 UTC** (7 days)" ); expect(markdown).toContain(`| visit (${NOOK_HOST} page views) | 1 | 0 | +1 |`); expect(markdown).toContain("| download click | 1 | 0 | +1 |"); expect(markdown).toContain("| DMG served | 2 | 0 | +2 |"); expect(markdown).toContain("| checkout started (Stripe session) | 1 | 0 | +1 |"); const order = [ `visit (${NOOK_HOST} page views)`, "download click", "DMG served", "trial started", "checkout started (Stripe session)", "purchase (licence issued)", "activation" ]; for (let i = 1; i < order.length; i += 1) { expect(markdown.indexOf(order[i - 1]!)).toBeLessThan( markdown.indexOf(order[i]!) ); } // The caveats this issue retired stay retired. expect(markdown).not.toContain("one label"); expect(markdown).not.toContain("not recorded"); expect(markdown).not.toContain("no host dimension"); expect(markdown).not.toContain("†"); expect(report.mover?.key).toBe("dmgServed"); expect(markdown).toContain("**DMG served: 0 → 2 (+2).**"); }); it("states what pre-instrumentation rows cannot say, computed from the window", async () => { insertAnalytics({ path: "/", created_at: THIS_WEEK }); // Legacy rows: no host, proxy's address, no stage label. insertAnalytics({ path: "/", host: null, ip_address: "127.0.0.1", created_at: THIS_WEEK }); insertAnalytics({ path: NOOK_DOWNLOAD_PATH, stage: null, host: null, ip_address: "127.0.0.1", user_agent: "curl/8.7.1", created_at: THIS_WEEK }); const report = await getWeeklyReport(conns, { now: NOW, days: 7 }); const markdown = renderWeeklyMarkdown(report); expect(markdown).toContain( "2 of 3 rows in the window predate the FRE-366 instrumentation" ); expect(markdown).toContain("predate the click/served stage label"); // Legacy loopback is not "the fix is not live". expect(markdown).not.toContain("still carry a loopback address"); }); it("flags loopback only when rows recorded since the instrumentation carry it", async () => { // Both spellings a live writer can produce: the raw proxy address, and the // /24 the privacy floor stores instead of it. insertAnalytics({ path: "/", ip_address: "127.0.0.1", created_at: THIS_WEEK }); insertAnalytics({ path: "/", ip_address: "127.0.53.0", created_at: THIS_WEEK }); const report = await getWeeklyReport(conns, { now: NOW, days: 7 }); const markdown = renderWeeklyMarkdown(report); expect(markdown).toContain( "2 rows recorded since the instrumentation still carry a loopback address" ); }); it("says nothing moved rather than inventing a winner", async () => { const report = await getWeeklyReport(conns, { now: NOW, days: 7 }); const markdown = renderWeeklyMarkdown(report); expect(report.mover).toBeNull(); expect(markdown).toContain("_Nothing moved: every stage held its previous-week value._"); expect(markdown).toContain("_No tagged traffic in the window._"); }); it("rejects a window that is not a positive number of days", async () => { await expect(getWeeklyReport(conns, { now: NOW, days: 0 })).rejects.toThrow( "days must be a positive number" ); }); });