| 1 | import { readFileSync } from "node:fs"; |
| 2 | import { fileURLToPath } from "node:url"; |
| 3 | |
| 4 | import { describe, expect, it } from "vitest"; |
| 5 | |
| 6 | import { |
| 7 | COVERAGE_CAVEATS, |
| 8 | activeInstallsSql, |
| 9 | formatAge, |
| 10 | formatReport, |
| 11 | freshnessSql, |
| 12 | newestEventFromResponse, |
| 13 | parseArgs, |
| 14 | rowsFromResponse, |
| 15 | trendSummary, |
| 16 | } from "../scripts/report-active-installs.mjs"; |
| 17 | |
| 18 | const NOW = new Date("2026-08-09T20:00:00Z"); |
| 19 | |
| 20 | /** 15 complete days of fixture data ending yesterday: last-7 sums to 70, previous-7 to 35. */ |
| 21 | function fixtureRows() { |
| 22 | const rows: Array<{ day: string; active_installs: number; sessions_started: number }> = []; |
| 23 | for (let offset = 1; offset <= 15; offset += 1) { |
| 24 | const day = new Date(Date.UTC(2026, 7, 9) - offset * 86_400_000) |
| 25 | .toISOString() |
| 26 | .slice(0, 10); |
| 27 | rows.push({ |
| 28 | day, |
| 29 | active_installs: offset <= 7 ? 10 : 5, |
| 30 | sessions_started: 20, |
| 31 | }); |
| 32 | } |
| 33 | return rows; |
| 34 | } |
| 35 | |
| 36 | describe("observed active installs owner report", () => { |
| 37 | it("bounds the query to Analytics Engine retention", () => { |
| 38 | expect(parseArgs([])).toEqual({ days: 15, json: false }); |
| 39 | expect(parseArgs(["--days", "7", "--json"])).toEqual({ days: 7, json: true }); |
| 40 | expect(() => parseArgs(["--days", "0"])).toThrow(/1 through 90/); |
| 41 | expect(() => parseArgs(["--days", "91"])).toThrow(/1 through 90/); |
| 42 | }); |
| 43 | |
| 44 | it("counts distinct installs only from session starts", () => { |
| 45 | const sql = activeInstallsSql(15); |
| 46 | expect(sql).toContain("count(DISTINCT index1) AS active_installs"); |
| 47 | expect(sql).toContain("sum(_sample_interval) AS sessions_started"); |
| 48 | expect(sql).toContain("AND blob1 = 'session_start'"); |
| 49 | expect(sql).toContain("INTERVAL '14' DAY"); |
| 50 | expect(sql).toContain("GROUP BY day"); |
| 51 | }); |
| 52 | |
| 53 | it("labels the partial UTC day and the lower-bound definition", () => { |
| 54 | const rows = rowsFromResponse({ |
| 55 | data: [ |
| 56 | { |
| 57 | day: "2026-08-09", |
| 58 | active_installs: "43", |
| 59 | sessions_started: "67", |
| 60 | }, |
| 61 | ], |
| 62 | }); |
| 63 | const report = formatReport(rows, { days: 15, now: NOW }); |
| 64 | expect(report).toContain("2026-08-09*"); |
| 65 | expect(report).toContain("43"); |
| 66 | expect(report).toContain("Not people, not accounts, not total installs"); |
| 67 | expect(report).toContain("clients older than the telemetry feature"); |
| 68 | }); |
| 69 | |
| 70 | it("never labels the count as users, people, accounts, or total installs", () => { |
| 71 | const report = formatReport(fixtureRows(), { days: 15, now: NOW, newestEvent: NOW }); |
| 72 | expect(report).toContain("observed active installs"); |
| 73 | expect(report).not.toMatch(/\bDAU\b/); |
| 74 | expect(report).not.toMatch(/daily active users/i); |
| 75 | expect(report).not.toMatch(/unique (users|people|accounts)/i); |
| 76 | // "people", "accounts", "total installs" appear only inside negations. |
| 77 | for (const line of report.split("\n")) { |
| 78 | if (/people|accounts|total installs/i.test(line)) { |
| 79 | expect(line).toMatch(/Not people, not accounts, not total installs/); |
| 80 | } |
| 81 | } |
| 82 | }); |
| 83 | }); |
| 84 | |
| 85 | describe("7-day trend", () => { |
| 86 | it("compares the last 7 complete UTC days against the previous 7", () => { |
| 87 | expect(trendSummary(fixtureRows(), 15, NOW)).toEqual({ |
| 88 | last7: 70, |
| 89 | previous7: 35, |
| 90 | changePct: 100, |
| 91 | }); |
| 92 | }); |
| 93 | |
| 94 | it("excludes the partial current day from both windows", () => { |
| 95 | const rows = [ |
| 96 | { day: "2026-08-09", active_installs: 999, sessions_started: 999 }, |
| 97 | ...fixtureRows(), |
| 98 | ]; |
| 99 | expect(trendSummary(rows, 15, NOW).last7).toBe(70); |
| 100 | }); |
| 101 | |
| 102 | it("treats missing days as zero rather than skipping them", () => { |
| 103 | const rows = fixtureRows().filter((row) => row.day !== "2026-08-05"); |
| 104 | expect(trendSummary(rows, 15, NOW).last7).toBe(60); |
| 105 | }); |
| 106 | |
| 107 | it("refuses to fabricate a comparison the window does not cover", () => { |
| 108 | const short = trendSummary(fixtureRows().slice(0, 7), 8, NOW); |
| 109 | expect(short.last7).toBe(70); |
| 110 | expect(short.previous7).toBeNull(); |
| 111 | expect(short.changePct).toBeNull(); |
| 112 | expect(trendSummary([], 3, NOW).last7).toBeNull(); |
| 113 | }); |
| 114 | |
| 115 | it("prints the trend with the not-a-retention-metric caveat inline", () => { |
| 116 | const report = formatReport(fixtureRows(), { days: 15, now: NOW }); |
| 117 | expect(report).toContain("7-day trend"); |
| 118 | expect(report).toContain("last 7 days: 70"); |
| 119 | expect(report).toContain("previous 7 days: 35"); |
| 120 | expect(report).toContain("+100%"); |
| 121 | expect(report).toContain("(id rotation: not a retention metric)"); |
| 122 | }); |
| 123 | |
| 124 | it("says so when the window is too small instead of printing zeros", () => { |
| 125 | const report = formatReport(fixtureRows().slice(0, 7), { days: 8, now: NOW }); |
| 126 | expect(report).toContain("not covered — use --days 15 or more"); |
| 127 | const tiny = formatReport([], { days: 3, now: NOW }); |
| 128 | expect(tiny).toContain("window too small for a 7-day trend"); |
| 129 | }); |
| 130 | }); |
| 131 | |
| 132 | describe("event freshness", () => { |
| 133 | it("asks only for the newest timestamp", () => { |
| 134 | const sql = freshnessSql(); |
| 135 | expect(sql).toContain("max(timestamp) AS newest_event"); |
| 136 | expect(sql).not.toContain("blob"); |
| 137 | expect(sql).not.toContain("index1"); |
| 138 | }); |
| 139 | |
| 140 | it("parses the SQL API's zone-less UTC timestamps", () => { |
| 141 | const parsed = newestEventFromResponse({ |
| 142 | data: [{ newest_event: "2026-08-09 17:46:00" }], |
| 143 | }); |
| 144 | expect(parsed?.toISOString()).toBe("2026-08-09T17:46:00.000Z"); |
| 145 | expect(newestEventFromResponse({ data: [] })).toBeNull(); |
| 146 | expect(newestEventFromResponse({ data: [{ newest_event: null }] })).toBeNull(); |
| 147 | }); |
| 148 | |
| 149 | it("prints how stale the newest ingested event is", () => { |
| 150 | const newestEvent = new Date("2026-08-09T17:46:00Z"); |
| 151 | const report = formatReport(fixtureRows(), { days: 15, now: NOW, newestEvent }); |
| 152 | expect(report).toContain( |
| 153 | "Freshness: newest ingested event 2026-08-09T17:46:00.000Z (2h 14m ago)", |
| 154 | ); |
| 155 | }); |
| 156 | |
| 157 | it("says plainly when nothing has ever been ingested", () => { |
| 158 | const report = formatReport([], { days: 15, now: NOW, newestEvent: null }); |
| 159 | expect(report).toContain("Freshness: no events ingested in the retention window"); |
| 160 | }); |
| 161 | |
| 162 | it("formats ages at minute, hour, and day scale", () => { |
| 163 | expect(formatAge(30_000)).toBe("0m"); |
| 164 | expect(formatAge(14 * 60_000)).toBe("14m"); |
| 165 | expect(formatAge((2 * 60 + 14) * 60_000)).toBe("2h 14m"); |
| 166 | expect(formatAge((3 * 1440 + 5 * 60) * 60_000)).toBe("3d 5h"); |
| 167 | }); |
| 168 | }); |
| 169 | |
| 170 | describe("coverage caveats travel with the numbers", () => { |
| 171 | it("prints every caveat in both report modes' shared source of truth", () => { |
| 172 | const report = formatReport(fixtureRows(), { days: 15, now: NOW }); |
| 173 | for (const caveat of COVERAGE_CAVEATS) { |
| 174 | expect(report).toContain(caveat); |
| 175 | } |
| 176 | }); |
| 177 | |
| 178 | it("names the invisible populations and the rotation caveat", () => { |
| 179 | const joined = COVERAGE_CAVEATS.join("\n"); |
| 180 | expect(joined).toContain("clients older than the telemetry feature"); |
| 181 | expect(joined).toContain("opted-out installs"); |
| 182 | expect(joined).toContain("non-emitting environments"); |
| 183 | expect(joined).toContain("rotate every 90 days"); |
| 184 | expect(joined).toContain("not a retention metric"); |
| 185 | }); |
| 186 | }); |
| 187 | |
| 188 | describe("the report path stays inside the exclusion guarantees", () => { |
| 189 | const ROOT = fileURLToPath(new URL("..", import.meta.url)); |
| 190 | const SOURCE = readFileSync(`${ROOT}scripts/report-active-installs.mjs`, "utf8"); |
| 191 | |
| 192 | it("aggregates install ids and never selects one back out", () => { |
| 193 | for (const sql of [activeInstallsSql(15), freshnessSql()]) { |
| 194 | // index1 (the install id) may appear only inside count(DISTINCT …). |
| 195 | expect(sql.replace(/count\(DISTINCT index1\)/g, "")).not.toContain("index1"); |
| 196 | } |
| 197 | }); |
| 198 | |
| 199 | it("touches no column that carries anything beyond the event name", () => { |
| 200 | // blob1 is the event-name enum; blob2..blob17 and the doubles carry the |
| 201 | // platform/counter payload and have no business in an install count. |
| 202 | expect(SOURCE).not.toMatch(/blob(?!1\b)\d+/); |
| 203 | expect(SOURCE).not.toMatch(/double\d+/); |
| 204 | }); |
| 205 | |
| 206 | it("reads nothing but the two SQL aggregates", () => { |
| 207 | // No prompts, paths, repo names, account identity, or network identity |
| 208 | // exist in the dataset (see no-ip.test.ts and schema-doc.test.ts); this |
| 209 | // pins the report to the read side of the same contract. |
| 210 | expect(SOURCE).not.toMatch(/cf-connecting-ip|x-forwarded-for|x-real-ip/i); |
| 211 | expect(SOURCE).not.toMatch(/\bemail\b|\baccount_name\b|\busername\b/i); |
| 212 | const selects = SOURCE.match(/SELECT[\s\S]*?FROM/g) ?? []; |
| 213 | expect(selects.length).toBe(2); |
| 214 | }); |
| 215 | }); |
| 216 |