Skip to content

Commit cabf40c

Browse files
pmaxhoganclaude
andcommitted
fix(telemetry): exclude pre-schema rows from latency rollup
The Analytics Engine SQL API has no NULLs: any double a row never wrote is materialized as 0. Rows written by the pre-latency Worker therefore read scan_p50 == 0, which passed the rollup's `>= 0` sentinel filter and polluted the per-day aggregates as fake 0 ms samples (seen live: a pre-deploy day showing samples with avg 0). Fix: writePing appends a schema-version marker (double11 = 1) on every row that carries the latency doubles, and each per-metric rollup query adds `AND double11 >= 1`. A legacy row materializes the marker as 0 and is excluded; the existing `>= 0` sentinel still excludes empty-latency new rows while keeping a legit 0 ms. Both write + query sites comment the AE missing-double=0 behavior. Tests: new row is marked (double11=1); an empty-latency new row is marked yet still sentinel-excluded; both metric queries include the `double11 >= 1` filter. Worker typecheck + lint + test (58) green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZQVP2tUuTLh8oL31D8heC
1 parent b9ef6ed commit cabf40c

3 files changed

Lines changed: 90 additions & 14 deletions

File tree

telemetry-worker/README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,22 @@ One data point per ping (`writePing`):
3636
| `blob1..6` | `os`, `arch`, `channel`, `version`, `os_version` (`""` if absent), `errors_by_class` JSON |
3737
| `double1..6` | `files_uploaded`, `bytes_uploaded`, `deep_verify_runs`, `update_applied` (0/1), `total_errors`, `ts` (epoch ms) |
3838
| `double7..10` | `scan_p50`, `scan_p95`, `upload_per_mb_p50`, `upload_per_mb_p95` (ms) |
39+
| `double11` | `latency_schema_version` (schema marker, `1`) |
3940

40-
The 4 latency doubles (DESIGN s13) are **appended** so the original columns keep
41+
The latency doubles (DESIGN s13) are **appended** so the original columns keep
4142
their positions. When the client had no samples for a metric this window (its
4243
array is empty), the pair is written as the sentinel **`-1`** so the rollup query
4344
can tell "no samples" apart from a legitimate `0 ms` (a sub-millisecond per-file
4445
scan rounds to 0).
4546

47+
`double11` is a **schema marker** (`1`) written on every row that carries the
48+
latency doubles. It exists because the Analytics Engine SQL API has no NULLs and
49+
materializes any double a row never wrote as `0`: rows written by the pre-latency
50+
Worker have `scan_p50 == 0` (a materialized 0, not a real sample) and would
51+
otherwise pass the `>= 0` sentinel filter and pollute the rollup as fake `0 ms`
52+
samples. The rollup filters `WHERE double11 >= 1`, so pre-latency rows (marker
53+
materializes as `0`) are excluded.
54+
4655
## `GET /telemetry/v1/stats/latency`
4756

4857
Per-day aggregates of the client-reported percentiles. **Authenticated** - it
@@ -68,10 +77,11 @@ Authorization: Bearer <QUERY_TOKEN>
6877

6978
Per metric, per UTC day: `avg_p50_ms` (mean of the pinged p50s), `avg_p95_ms`
7079
(mean of the pinged p95s), `max_p95_ms` (worst pinged p95), and `samples` (number
71-
of pings that reported the metric). Empty-latency pings (the `-1` sentinel) are
72-
excluded per metric via `WHERE <p50col> >= 0`, so a real `0 ms` still counts. The
73-
two metrics are queried separately (each filters its own sentinel column) via the
74-
Analytics Engine SQL API.
80+
of pings that reported the metric). Each metric query excludes two kinds of
81+
non-sample rows (`WHERE double11 >= 1 AND <p50col> >= 0`): pre-latency rows (the
82+
schema marker materializes as `0`) and empty-latency pings (the `-1` sentinel),
83+
while keeping a real `0 ms`. The two metrics are queried separately (each filters
84+
its own sentinel column) via the Analytics Engine SQL API.
7585

7686
Status codes: `200` success; `401` missing/wrong bearer; `405` non-GET;
7787
`502` upstream AE SQL query failed; `503` the endpoint is not configured

telemetry-worker/src/index.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -386,17 +386,36 @@ function latencyPair(arr: number[]): [number, number] {
386386
return [NO_LATENCY, NO_LATENCY];
387387
}
388388

389+
/// Schema-version marker written on EVERY row that carries the latency doubles
390+
/// (DESIGN s13). Load-bearing for the rollup: the Analytics Engine SQL API has NO
391+
/// NULLs and materializes any double a row never wrote as `0`, so rows written by
392+
/// the PRE-latency Worker have `scan_p50 == 0` (a materialized 0, not a real
393+
/// sample) and would otherwise pass the `>= 0` sentinel filter and pollute the
394+
/// rollup as fake `0 ms` samples. A row with this marker `>= 1` provably has the
395+
/// latency schema; a legacy row materializes the marker as `0`, so the rollup's
396+
/// `WHERE <marker> >= 1` excludes it. Bump this only if the latency-double LAYOUT
397+
/// changes (append-only, like the doubles themselves).
398+
const LATENCY_SCHEMA_VERSION = 1;
399+
400+
/// The AE double column the schema marker occupies (double11, appended after the
401+
/// 4 latency percentiles). The rollup filters `>= 1` on this so pre-latency rows
402+
/// (which materialize it as 0) are excluded.
403+
const LATENCY_SCHEMA_COL = "double11";
404+
389405
/// Write one validated ping to Analytics Engine (SPEC s16, DESIGN s13). Schema:
390406
/// - indexes: [install_id] (the sampling/grouping key - anonymous)
391407
/// - blobs: [os, arch, channel, version, os_version, errors_by_class JSON]
392408
/// (low-card dims; os_version is "" when the client did not send one)
393409
/// - doubles: [files_uploaded, bytes_uploaded, deep_verify_runs, update_applied,
394410
/// total_errors, ts, // double1..double6
395-
/// scan_p50, scan_p95, upload_per_mb_p50, upload_per_mb_p95]
411+
/// scan_p50, scan_p95, upload_per_mb_p50, upload_per_mb_p95,
396412
/// // double7..double10
397-
/// The 4 latency doubles are appended (never reordered) so existing columns keep
398-
/// their positions; an absent metric writes the NO_LATENCY (-1) sentinel.
399-
/// Writes are non-blocking (no await / waitUntil needed per the CF docs).
413+
/// latency_schema_version] // double11
414+
/// The latency doubles are appended (never reordered) so existing columns keep
415+
/// their positions; an absent metric writes the NO_LATENCY (-1) sentinel, and
416+
/// double11 marks the row as carrying the latency schema (LATENCY_SCHEMA_VERSION)
417+
/// so the rollup can exclude pre-latency rows (whose missing doubles AE
418+
/// materializes as 0). Writes are non-blocking (no await / waitUntil per CF docs).
400419
export function writePing(env: Env, p: PingPayload): void {
401420
const [scanP50, scanP95] = latencyPair(p.latency_p50_p95_ms.scan);
402421
const [upP50, upP95] = latencyPair(p.latency_p50_p95_ms.upload_per_mb);
@@ -427,6 +446,10 @@ export function writePing(env: Env, p: PingPayload): void {
427446
scanP95,
428447
upP50,
429448
upP95,
449+
// DESIGN s13: schema-version marker (double11). Present (>= 1) on every row
450+
// that carries the latency doubles above; a pre-latency row has no double11
451+
// so AE materializes it as 0, letting the rollup exclude such rows.
452+
LATENCY_SCHEMA_VERSION,
430453
],
431454
});
432455
}
@@ -548,10 +571,13 @@ function clampStatsDays(raw: string | null): number {
548571

549572
/// Query one latency metric's per-day aggregates over the AE SQL API. `p50Col` /
550573
/// `p95Col` are the AE double column names for this metric (e.g. `double7` /
551-
/// `double8`). Rows carrying the NO_LATENCY sentinel (`< 0`, an empty-latency
552-
/// ping) are excluded via `WHERE p50 >= 0`, so a legit `0 ms` still counts. The
553-
/// response is the CF `{ meta, data }` JSON (NOT ndjson); each `data[]` row's
554-
/// numeric columns arrive as strings, so they are coerced with `Number`.
574+
/// `double8`). Two `WHERE` filters exclude non-samples (both needed because AE has
575+
/// no NULLs - a never-written double reads as 0): the schema marker (excludes
576+
/// PRE-latency rows whose latency doubles materialize as 0) and the NO_LATENCY
577+
/// sentinel (`< 0`, excludes latency-schema rows with no samples this window, while
578+
/// keeping a legit `0 ms`). The response is the CF `{ meta, data }` JSON (NOT
579+
/// ndjson); each `data[]` row's numeric columns arrive as strings, coerced with
580+
/// `Number`.
555581
async function queryLatencyMetric(
556582
env: Env,
557583
accountId: string,
@@ -561,14 +587,24 @@ async function queryLatencyMetric(
561587
): Promise<LatencyDayRow[]> {
562588
// `days` is a validated integer (clampStatsDays) and the column names are
563589
// internal constants, so this interpolation carries no injection surface.
590+
//
591+
// Two filters, both load-bearing (the AE SQL API has NO NULLs - a double a row
592+
// never wrote is materialized as 0):
593+
// - `${LATENCY_SCHEMA_COL} >= 1`: exclude PRE-latency rows. Those rows never
594+
// wrote the latency doubles, so their `${p50Col}` materializes as 0 and
595+
// would otherwise pass the sentinel filter below as a fake `0 ms` sample.
596+
// Only rows carrying the latency schema wrote the marker (>= 1).
597+
// - `${p50Col} >= 0`: exclude latency-schema rows whose metric had NO samples
598+
// this window (written as the -1 sentinel), while KEEPING a legit 0 ms.
564599
const sql =
565600
`SELECT toDate(timestamp) AS day, ` +
566601
`AVG(${p50Col}) AS avg_p50, ` +
567602
`AVG(${p95Col}) AS avg_p95, ` +
568603
`MAX(${p95Col}) AS max_p95, ` +
569604
`COUNT() AS samples ` +
570605
`FROM ${DATASET} ` +
571-
`WHERE timestamp > NOW() - INTERVAL '${days}' DAY AND ${p50Col} >= 0 ` +
606+
`WHERE timestamp > NOW() - INTERVAL '${days}' DAY ` +
607+
`AND ${LATENCY_SCHEMA_COL} >= 1 AND ${p50Col} >= 0 ` +
572608
`GROUP BY day ORDER BY day`;
573609

574610
const resp = await fetch(

telemetry-worker/test/handler.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,8 @@ const SCAN_P50 = 6;
572572
const SCAN_P95 = 7;
573573
const UP_P50 = 8;
574574
const UP_P95 = 9;
575+
/// The schema-version marker double (double11), appended after the 4 percentiles.
576+
const SCHEMA_MARKER = 10;
575577

576578
describe("telemetry worker latency doubles (DESIGN s13)", () => {
577579
it("writes the client-reported [p50, p95] latency doubles", async () => {
@@ -589,6 +591,31 @@ describe("telemetry worker latency doubles (DESIGN s13)", () => {
589591
expect(dp.doubles[UP_P95]).toBe(110);
590592
});
591593

594+
it("marks every new latency-schema row with the schema-version marker (double11)", async () => {
595+
// The marker (>= 1) is what lets the rollup exclude pre-latency rows: AE
596+
// materializes a missing double as 0, so a legacy row reads double11 == 0.
597+
const { env, writes } = mockEnv();
598+
const p = validPayload();
599+
p.latency_p50_p95_ms = { scan: [3, 12], upload_per_mb: [40, 110] };
600+
const res = await handle(postPing(JSON.stringify(p)), env);
601+
expect(res.status).toBe(204);
602+
const dp = writes[0] as { doubles: number[] };
603+
expect(dp.doubles[SCHEMA_MARKER]).toBe(1);
604+
});
605+
606+
it("marks a new row EVEN WHEN its latency is empty (marker present, sentinels -1)", async () => {
607+
// A new-but-empty row is marked as latency-schema (double11 == 1) yet still
608+
// carries the -1 sentinels, so the rollup's sentinel filter (not the marker)
609+
// is what excludes it - the two filters are independent.
610+
const { env, writes } = mockEnv();
611+
const res = await handle(postPing(JSON.stringify(validPayload())), env);
612+
expect(res.status).toBe(204);
613+
const dp = writes[0] as { doubles: number[] };
614+
expect(dp.doubles[SCHEMA_MARKER]).toBe(1); // marked new...
615+
expect(dp.doubles[SCAN_P50]).toBe(-1); // ...but sentinel-excluded from the rollup
616+
expect(dp.doubles[UP_P50]).toBe(-1);
617+
});
618+
592619
it("writes the -1 sentinel for a metric with no samples (empty array)", async () => {
593620
const { env, writes } = mockEnv();
594621
const p = validPayload();
@@ -701,6 +728,9 @@ describe("telemetry worker GET /stats/latency (DESIGN s13)", () => {
701728
expect(fetchMock).toHaveBeenCalledTimes(2);
702729
expect(sqls.some((s) => s.includes("double7") && s.includes("double7 >= 0"))).toBe(true);
703730
expect(sqls.some((s) => s.includes("double9") && s.includes("double9 >= 0"))).toBe(true);
731+
// BOTH queries also filter the schema marker (excludes pre-latency rows whose
732+
// missing latency doubles AE materializes as 0).
733+
expect(sqls.every((s) => s.includes("double11 >= 1"))).toBe(true);
704734
// The default 7-day window is in the SQL.
705735
expect(sqls.every((s) => s.includes("INTERVAL '7' DAY"))).toBe(true);
706736
});

0 commit comments

Comments
 (0)