| 1 | /** |
| 2 | * The doc and the validator are welded. |
| 3 | * |
| 4 | * `docs/TELEMETRY.md` is a promise to users, and the Rust suite already welds it |
| 5 | * to the client's structs. This file welds it to the *server*: the field names |
| 6 | * and enum spellings are parsed back out of the markdown and compared for set |
| 7 | * equality against `src/schema.ts`. A field published in the doc that this |
| 8 | * endpoint would reject fails here; a field this endpoint would accept that the |
| 9 | * doc never published fails here too. Neither can be fixed by editing only one |
| 10 | * side. |
| 11 | * |
| 12 | * The parsers below mirror `crates/telemetry/src/tests.rs` on purpose — same |
| 13 | * fenced-block extraction, same "identifier followed by a colon is a key" rule. |
| 14 | */ |
| 15 | |
| 16 | import { describe, expect, it } from "vitest"; |
| 17 | |
| 18 | import { |
| 19 | ARCHES, |
| 20 | BATCH_MAX_BYTES, |
| 21 | BATCH_MAX_EVENTS, |
| 22 | COLD_START_BUCKETS, |
| 23 | COUNTER_FIELDS, |
| 24 | DURATION_BUCKETS, |
| 25 | ENVELOPE_FIELDS, |
| 26 | ERROR_FIELDS, |
| 27 | EVENT_FIELDS, |
| 28 | EVENT_NAMES, |
| 29 | EXIT_CLASSES, |
| 30 | INSTALL_KINDS, |
| 31 | LIBCS, |
| 32 | MAX_BODY_BYTES, |
| 33 | OSES, |
| 34 | SCHEMA_VERSION, |
| 35 | SESSION_SOURCES, |
| 36 | SURFACES, |
| 37 | TURN_WALL_FIELDS, |
| 38 | } from "../src/schema"; |
| 39 | import { DOC, goldenBatch } from "./support"; |
| 40 | |
| 41 | /** Every fenced ```jsonc block, in document order. */ |
| 42 | function jsoncBlocks(doc: string): string[] { |
| 43 | const blocks: string[] = []; |
| 44 | let current: string[] | null = null; |
| 45 | for (const raw of doc.split("\n")) { |
| 46 | const line = raw.trimEnd(); |
| 47 | if (current === null) { |
| 48 | if (line.trim() === "```jsonc") current = []; |
| 49 | continue; |
| 50 | } |
| 51 | if (line.trim() === "```") { |
| 52 | blocks.push(current.join("\n")); |
| 53 | current = null; |
| 54 | } else { |
| 55 | current.push(line); |
| 56 | } |
| 57 | } |
| 58 | return blocks; |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Every `"name":` key in a block, nested objects included. Values are never |
| 63 | * matched: a key is an identifier-shaped string followed by a colon, and no |
| 64 | * value in these blocks has that shape. |
| 65 | */ |
| 66 | function documentedKeys(block: string): Set<string> { |
| 67 | const keys = new Set<string>(); |
| 68 | const pattern = /"([a-z0-9_]+)"\s*:/g; |
| 69 | for (const match of block.matchAll(pattern)) { |
| 70 | keys.add(match[1]); |
| 71 | } |
| 72 | return keys; |
| 73 | } |
| 74 | |
| 75 | /** The rows of the first markdown table after `anchor`, as `[first, rest]`. */ |
| 76 | function tableAfter(doc: string, anchor: string): Array<[string, string]> { |
| 77 | const lines = doc.split("\n"); |
| 78 | const start = lines.findIndex((line) => line.includes(anchor)); |
| 79 | expect(start, `anchor not found in docs/TELEMETRY.md: ${anchor}`).toBeGreaterThanOrEqual(0); |
| 80 | |
| 81 | const rows: Array<[string, string]> = []; |
| 82 | let inTable = false; |
| 83 | for (const line of lines.slice(start + 1)) { |
| 84 | const trimmed = line.trim(); |
| 85 | if (!trimmed.startsWith("|")) { |
| 86 | if (inTable) break; |
| 87 | continue; |
| 88 | } |
| 89 | inTable = true; |
| 90 | // Split on pipes that are not escaped as `\|` — escaped pipes are enum |
| 91 | // separators inside a cell, not cell boundaries. |
| 92 | const cells = trimmed |
| 93 | .replace(/^\|/, "") |
| 94 | .replace(/\|$/, "") |
| 95 | .split(/(?<!\\)\|/) |
| 96 | .map((cell) => cell.trim()); |
| 97 | const first = (cells[0] ?? "").replace(/`/g, "").trim(); |
| 98 | if (first === "" || /^[-:]+$/.test(first)) continue; |
| 99 | if (first.toLowerCase() === "field" || first.toLowerCase() === "file") continue; |
| 100 | rows.push([first, cells.slice(1).join(" | ")]); |
| 101 | } |
| 102 | return rows; |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * The enum list a table row publishes: the first backticked run in the row's |
| 107 | * remaining cells that contains a `\|` separator. |
| 108 | */ |
| 109 | function enumFromRow(rows: Array<[string, string]>, field: string): string[] { |
| 110 | const row = rows.find(([name]) => name === field); |
| 111 | expect(row, `no table row for ${field}`).toBeDefined(); |
| 112 | const rest = (row as [string, string])[1].replace(/\\\|/g, "|"); |
| 113 | for (const match of rest.matchAll(/`([^`]+)`/g)) { |
| 114 | if (match[1].includes("|")) { |
| 115 | return match[1].split("|").map((value) => value.trim()); |
| 116 | } |
| 117 | } |
| 118 | throw new Error(`no enum list published for ${field}`); |
| 119 | } |
| 120 | |
| 121 | /** Every token the doc puts inside backticks, split on `|` and `,`. */ |
| 122 | function documentedTokens(doc: string): Set<string> { |
| 123 | const tokens = new Set<string>(); |
| 124 | const unescaped = doc.replace(/\\\|/g, "|"); |
| 125 | for (const match of unescaped.matchAll(/`([^`\n]+)`/g)) { |
| 126 | for (const part of match[1].split(/[|,]/)) { |
| 127 | tokens.add(part.trim()); |
| 128 | } |
| 129 | } |
| 130 | return tokens; |
| 131 | } |
| 132 | |
| 133 | const BLOCKS = jsoncBlocks(DOC); |
| 134 | const TOKENS = documentedTokens(DOC); |
| 135 | |
| 136 | describe("the doc and the validator publish the same fields", () => { |
| 137 | it("finds one jsonc block for the envelope and one per event", () => { |
| 138 | expect(BLOCKS).toHaveLength(1 + EVENT_NAMES.length); |
| 139 | }); |
| 140 | |
| 141 | it("agrees on the envelope field set", () => { |
| 142 | expect([...documentedKeys(BLOCKS[0])].sort()).toEqual( |
| 143 | [...ENVELOPE_FIELDS].sort(), |
| 144 | ); |
| 145 | }); |
| 146 | |
| 147 | it("agrees on the install_or_upgrade field set", () => { |
| 148 | expect([...documentedKeys(BLOCKS[1])].sort()).toEqual( |
| 149 | [...EVENT_FIELDS.install_or_upgrade].sort(), |
| 150 | ); |
| 151 | }); |
| 152 | |
| 153 | it("agrees on the session_start field set", () => { |
| 154 | expect([...documentedKeys(BLOCKS[2])].sort()).toEqual( |
| 155 | [...EVENT_FIELDS.session_start].sort(), |
| 156 | ); |
| 157 | }); |
| 158 | |
| 159 | it("agrees on the session_end field set, nested objects included", () => { |
| 160 | const expected = [ |
| 161 | ...EVENT_FIELDS.session_end, |
| 162 | ...COUNTER_FIELDS, |
| 163 | ...ERROR_FIELDS, |
| 164 | ...TURN_WALL_FIELDS, |
| 165 | ].sort(); |
| 166 | expect([...documentedKeys(BLOCKS[3])].sort()).toEqual(expected); |
| 167 | }); |
| 168 | |
| 169 | it("agrees on the panic field set", () => { |
| 170 | expect([...documentedKeys(BLOCKS[4])].sort()).toEqual( |
| 171 | [...EVENT_FIELDS.panic].sort(), |
| 172 | ); |
| 173 | }); |
| 174 | |
| 175 | it("agrees on the counters table, field for field", () => { |
| 176 | const rows = tableAfter(DOC, "**`counters`** — closed field set"); |
| 177 | expect(rows.map(([name]) => name)).toEqual([...COUNTER_FIELDS]); |
| 178 | }); |
| 179 | |
| 180 | it("agrees on the errors table, field for field", () => { |
| 181 | const rows = tableAfter(DOC, "**`errors`** — closed field set"); |
| 182 | expect(rows.map(([name]) => name)).toEqual([...ERROR_FIELDS]); |
| 183 | }); |
| 184 | |
| 185 | it("agrees on the envelope table, field for field", () => { |
| 186 | const rows = tableAfter(DOC, "### Batch envelope"); |
| 187 | expect(rows.map(([name]) => name)).toEqual([...ENVELOPE_FIELDS]); |
| 188 | }); |
| 189 | }); |
| 190 | |
| 191 | describe("the doc and the validator publish the same enums", () => { |
| 192 | const rows = tableAfter(DOC, "### Batch envelope"); |
| 193 | |
| 194 | it.each([ |
| 195 | ["surface", SURFACES], |
| 196 | ["os", OSES], |
| 197 | ["arch", ARCHES], |
| 198 | ["libc", LIBCS], |
| 199 | ] as const)("agrees on the %s whitelist", (field, expected) => { |
| 200 | expect(enumFromRow(rows, field).sort()).toEqual([...expected].sort()); |
| 201 | }); |
| 202 | |
| 203 | it.each([ |
| 204 | ["install kind", INSTALL_KINDS], |
| 205 | ["session source", SESSION_SOURCES], |
| 206 | ["duration bucket", DURATION_BUCKETS], |
| 207 | ["exit class", EXIT_CLASSES], |
| 208 | ["cold start bucket", COLD_START_BUCKETS], |
| 209 | ] as const)("publishes every %s value it accepts", (_label, values) => { |
| 210 | for (const value of values) { |
| 211 | expect(TOKENS.has(value), `${value} is not in docs/TELEMETRY.md`).toBe( |
| 212 | true, |
| 213 | ); |
| 214 | } |
| 215 | }); |
| 216 | }); |
| 217 | |
| 218 | describe("the doc and the validator publish the same limits", () => { |
| 219 | it("agrees on SCHEMA_VERSION", () => { |
| 220 | const match = DOC.match(/`SCHEMA_VERSION\s*=\s*(\d+)`/); |
| 221 | expect(match).not.toBeNull(); |
| 222 | expect(Number((match as RegExpMatchArray)[1])).toBe(SCHEMA_VERSION); |
| 223 | expect(goldenBatch().schema_version).toBe(SCHEMA_VERSION); |
| 224 | }); |
| 225 | |
| 226 | it("agrees on the per-batch caps", () => { |
| 227 | const match = DOC.match(/Capped at (\d+) events or (\d+) KiB per batch/); |
| 228 | expect(match).not.toBeNull(); |
| 229 | const [, events, kib] = match as RegExpMatchArray; |
| 230 | expect(Number(events)).toBe(BATCH_MAX_EVENTS); |
| 231 | expect(Number(kib) * 1024).toBe(BATCH_MAX_BYTES); |
| 232 | }); |
| 233 | |
| 234 | it("caps the body above what a conforming client can send", () => { |
| 235 | // 65536 event bytes + 199 commas + ~375 bytes of envelope keys and values. |
| 236 | const worstCaseBody = BATCH_MAX_BYTES + (BATCH_MAX_EVENTS - 1) + 375; |
| 237 | expect(MAX_BODY_BYTES).toBeGreaterThan(worstCaseBody); |
| 238 | // …and not so far above it that the cap has stopped meaning anything. |
| 239 | expect(MAX_BODY_BYTES).toBeLessThan(worstCaseBody * 2); |
| 240 | }); |
| 241 | |
| 242 | it("agrees that the disk rings are larger than one batch", () => { |
| 243 | expect(DOC).toContain("rings capped at 512 records or\n256 KiB"); |
| 244 | expect(MAX_BODY_BYTES).toBeLessThan(256 * 1024); |
| 245 | }); |
| 246 | }); |
| 247 | |
| 248 | describe("the red line the doc publishes", () => { |
| 249 | it("still says batches are IP-stripped at ingest", () => { |
| 250 | // If this line ever leaves the doc, the deploy of this Worker needs a |
| 251 | // second look before it ships. |
| 252 | expect(DOC).toContain("Batches are **IP-stripped at ingest**"); |
| 253 | expect(DOC).toContain( |
| 254 | "No IP is stored, logged, or joined to `install_id`", |
| 255 | ); |
| 256 | }); |
| 257 | }); |
| 258 |