diff --git a/docs/telemetry-contract.md b/docs/telemetry-contract.md index 66868a0..3c1b21d 100644 --- a/docs/telemetry-contract.md +++ b/docs/telemetry-contract.md @@ -23,10 +23,43 @@ older than 14 days nightly. | `player` | `av_delay_applied` | `{audioMs, host}` — user dialed in an audio offset (the "a track falls behind" signal) | | `session` | `session_summary` | one row per playback — see below. **Written only by `SessionFeedbackService._write`.** | | `qa` | `qa_session` | `{sid, lines[], truncated?}` — the QA decision log for one playback (issue #92), off by default | +| `source` | `source_select` | one row per source sheet — timings and counts, see below | +| `source` | `source_pick` | **what was chosen and what it beat** — see below | `host` is only the stream's `scheme://host` — **never** a full signed URL or an addon token (redacted client-side in `TelemetryService.redactUrl` before write). +### `source_pick` — the row that answers "why did it play that one" + +Added 2026-09-09, because a real complaint could not be answered without it. +"Continue played a 1080p when a 4K was there" took an hour of reading the +ranker and reproducing its arithmetic in a test; `source_select` records how +long each stage took and how many candidates there were, and says nothing about +*which* one won. + +One row per automatic pick: + +| field | meaning | +|---|---| +| `stage` | `known` (a candidate was already probed) or the probing stage | +| `outcome` | `chosen` / `none` | +| `wonOn` | **the term it beat the runner-up on** — `language`, `resolutionMatch`, `quality`, `identity`… or `only_candidate` | +| `res`, `resMeasured` | the winner's height, and whether that was measured or read off the filename | +| `lang` | the winner's `SourceRanker.languageTier`: 5 confirmed primary audio · 4 claimed · 3 subtitles or fallback · 2 probed-unsuitable · 1 unprobed | +| `czech` | `dub` / `sub` / absent, as the addon tagged it | +| `quality`, `provider` | bitrate estimate; which addon it came from | +| `prefHeight` | **what the viewer asked for**, so the pick can be judged against their preference rather than an assumption | +| `alt*` | the best candidate it did NOT choose: `altRes`, `altResMeasured`, `altLang`, `altProbed` | + +`altProbed=false` with a higher `altRes` is the signature of the whole class: +the better file existed and had not been measured yet, so it lost on `language` +(unprobed is tier 1) before resolution was ever consulted. + +**It is not gated on QA logging**, unlike `qa_session`. It used to be, to save +re-scoring the field for the runner-up; that saving cost an hour of +reconstruction the first time somebody asked a question it would have answered. +The pass is arithmetic over a few dozen candidates, once per pick. + ### `session_summary`, and the two numbers that are not the same `{sessionId, title, probeKey, host, durationS, watchedS, positionS, stalls,