返回 CodeWhale
report-active-installs.test.ts
根目录 / telemetry-ingest / test / report-active-installs.test.ts
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
216 lines TYPESCRIPT