A failed result row carries a structured failureKind distinguishing tracking-broken vs site-broken vs robot-broken vs timeout. The classification surfaces in Grafana and Slack so an analyst can route the issue to the right team. Phase 10 will extend this enum with param-mismatch, duplicate, under-counted, unclassified.
Strictly additive; builds on Phase 2's outcome field; does NOT change matching, capture, or any Phase 1–6 contract. outcome answers what happened (validated/aborted/skipped); failureKind answers which team owns the fix. Both are needed.
failureKind? on both row types.export type FailureKind = "tracking-broken" | "site-broken" | "robot-broken" | "timeout";
Absent on validated/passing rows. Set whenever passed: false, regardless of outcome.
| Origin | outcome | failureKind |
|---|---|---|
validateEvents finds source missing | validated | tracking-broken |
Per-section catch in bi-measurement.ts | section-aborted | classifyError(err) |
reconcileMissingFlows synthesizes placeholder | flow-skipped | robot-broken |
| Passing row | validated | (absent) |
classifyError(err: unknown): FailureKind/timeout/i OR error name TimeoutError (Playwright) → "timeout"."robot-broken".net::ERR_, ERR_HTTP, ECONN, ENOTFOUND, getaddrinfo, ERR_CONNECTION_, Cannot navigate, page.goto: with network → "site-broken"."robot-broken" (the runner observed the error; attribute to robot/tooling absent stronger signal).Pure, deterministic, no side effects. Lives at src/utils/failureClassification.ts.
failureKind is "robot-broken", distinct from a tracking miss.page.goto timeout produces a row whose failureKind is "timeout", layered on Phase 2's outcome="section-aborted".failureKind is "tracking-broken" while outcome stays "validated".failureKind="robot-broken" because the robot's guard return is the proximate cause.failureKind undefined — never falsy-but-present.failureKind as a template variable and filterable facet; dashboard version 63 → 64.failureKind: <kind> suffix; no new blocks, no reorder.