@@ -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).
400419export 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`.
555581async 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 (
0 commit comments