From 0db1cf0d23ce07b596a56dff7f1099487b493265 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Sat, 8 Aug 2026 08:25:25 +0000 Subject: [PATCH] fix(webapp): say why a report's numbers can't be trusted Absent telemetry and an unmeasured flow were both labelled stale data, so every snapshot-based report claimed staleness it could not have measured. Choose the badge and caveat from the reason instead. --- .../presenters/v3/reports/report-layout.ts | 50 ++++++++--- apps/webapp/test/reportTrust.test.ts | 85 +++++++++++++++++++ 2 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 apps/webapp/test/reportTrust.test.ts diff --git a/apps/webapp/app/presenters/v3/reports/report-layout.ts b/apps/webapp/app/presenters/v3/reports/report-layout.ts index d07eae273..3597c8b58 100644 --- a/apps/webapp/app/presenters/v3/reports/report-layout.ts +++ b/apps/webapp/app/presenters/v3/reports/report-layout.ts @@ -50,12 +50,35 @@ export const REPORT_LABELS = { read: "read:", /** The footer heading. */ nextSteps: "Next steps", - /** Shown beside the report's name when the data can't be trusted. */ - staleBadge: "stale data", - staleNote: - "The telemetry behind this report is stale, so the numbers below are informational only.", } as const; +/** The flag beside the report's name, and the caveat under its headline. */ +export type LayoutTrust = { badge: string; note: string }; + +/** + * Why a report's numbers can't be trusted, in its own words. Stale, absent and unmeasured are three + * different states, and a snapshot with no telemetry feed must not be called stale. + */ +const TRUST_CAVEATS: Record = { + telemetry_stale: { + badge: "stale data", + note: "The telemetry behind this report is stale, so the numbers below are informational only.", + }, + telemetry_absent: { + badge: "no telemetry", + note: "No telemetry reached this report, so the numbers below are a point-in-time snapshot rather than a measured window.", + }, + flow_unmeasured: { + badge: "unmeasured", + note: "Throughput could not be measured over this window, so the numbers below are informational only.", + }, +}; + +const TRUST_CAVEAT_FALLBACK: LayoutTrust = { + badge: "unverified data", + note: "The data behind this report could not be verified, so the numbers below are informational only.", +}; + /** * The report's sections, top to bottom. A renderer walks this order; a new section has to be added * here first, which is what keeps the surfaces aligned. `trust` spans two places: a flag beside the @@ -87,13 +110,20 @@ const UNASSESSABLE_REASONS = new Set(["unknown", "flow_unmeasured"]); const NEUTRAL_REASONS = new Set(["freshness_unknown", "flow_unmeasured"]); /** - * `facts.trustworthy === false` means the telemetry behind the verdict is stale, so the numbers are - * informational only. Absent = trustworthy (the common case, and what pre-`facts` snapshots imply). + * `facts.trustworthy === false` means the numbers behind the verdict are informational only. Absent + * = trustworthy (the common case, and what pre-`facts` snapshots imply). */ export function reportIsTrustworthy(vm: { facts?: Record }): boolean { return vm.facts?.trustworthy !== false; } +/** The caveat for an untrustworthy report, chosen by `facts.untrustworthyReason`. */ +export function reportTrust(vm: { facts?: Record }): LayoutTrust | undefined { + if (reportIsTrustworthy(vm)) return undefined; + const reason = vm.facts?.untrustworthyReason; + return (typeof reason === "string" ? TRUST_CAVEATS[reason] : undefined) ?? TRUST_CAVEAT_FALLBACK; +} + export function reportTone(severity: Severity, reason?: string): ReportTone { return reason !== undefined && NEUTRAL_REASONS.has(reason) ? "neutral" : severity; } @@ -283,7 +313,7 @@ export type LayoutFooterEntry = { export type ReportLayout = { header: { name: string; meta: string }; /** Present only when the data can't be trusted. */ - trust?: { badge: string; note: string }; + trust?: LayoutTrust; headline: { tone: ReportTone; glyph: string; severity: Severity; phrase: string; text?: string }; /** The finding the headline speaks for, always expanded. */ hero?: LayoutFinding; @@ -343,14 +373,14 @@ export function buildReportLayout(vm: LayoutViewModel, messages: ReportMessages) .filter((finding) => finding.read !== undefined && !UNASSESSABLE_REASONS.has(finding.reason)) .map((finding) => fillTokens(messages.readMessage(finding.read!), tokens)); + const trust = reportTrust(vm); + return { header: { name: vm.title, meta: [vm.scope, vm.period, vm.baselineLabel].filter(Boolean).join(" · "), }, - ...(reportIsTrustworthy(vm) - ? {} - : { trust: { badge: REPORT_LABELS.staleBadge, note: REPORT_LABELS.staleNote } }), + ...(trust === undefined ? {} : { trust }), headline: { severity: vm.summary.severity, tone: reportTone(vm.summary.severity, heroStatement?.reason), diff --git a/apps/webapp/test/reportTrust.test.ts b/apps/webapp/test/reportTrust.test.ts new file mode 100644 index 000000000..6a3005e2a --- /dev/null +++ b/apps/webapp/test/reportTrust.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; +import { + buildReportLayout, + type LayoutViewModel, + reportTrust, +} from "~/presenters/v3/reports/report-layout"; +import { reportMessages } from "~/presenters/v3/reports/report-messages"; + +const messages = reportMessages("health"); + +function viewModel(facts?: Record): LayoutViewModel { + return { + title: "health", + scope: "prod", + period: "last 60m", + windowMinutes: 60, + summary: { severity: "warn", statements: [{ findingType: "flow", severity: "warn" }] }, + findings: [{ type: "flow", severity: "warn", reason: "backlog_growing", metricIds: [] }], + metrics: [], + footer: [], + ...(facts === undefined ? {} : { facts }), + }; +} + +function trustFor(untrustworthyReason?: string) { + return reportTrust({ + facts: { trustworthy: false, ...(untrustworthyReason ? { untrustworthyReason } : {}) }, + }); +} + +describe("report layout — why the numbers can't be trusted", () => { + it("calls stale telemetry stale", () => { + expect(trustFor("telemetry_stale")?.badge).toBe("stale data"); + expect(trustFor("telemetry_stale")?.note).toContain("stale"); + }); + + it("does not call a report with no telemetry stale", () => { + const trust = trustFor("telemetry_absent"); + + expect(trust?.badge).toBe("no telemetry"); + expect(trust?.note).toContain("No telemetry"); + expect(trust?.note).not.toContain("stale"); + }); + + it("does not call an unmeasured flow stale", () => { + const trust = trustFor("flow_unmeasured"); + + expect(trust?.badge).toBe("unmeasured"); + expect(trust?.note).not.toContain("stale"); + }); + + it("gives the three untrustworthy states three different badges", () => { + const badges = ["telemetry_stale", "telemetry_absent", "flow_unmeasured"].map( + (reason) => trustFor(reason)?.badge + ); + + expect(new Set(badges).size).toBe(3); + }); + + it("falls back without claiming staleness when the reason is missing", () => { + expect(trustFor()).toBeDefined(); + expect(trustFor()?.note).not.toContain("stale"); + }); + + it("says nothing when the report is trustworthy", () => { + expect(reportTrust({ facts: { trustworthy: true } })).toBeUndefined(); + expect(reportTrust({})).toBeUndefined(); + }); + + it("carries the chosen caveat into the layout", () => { + const layout = buildReportLayout( + viewModel({ trustworthy: false, untrustworthyReason: "telemetry_absent" }), + messages + ); + + expect(layout.trust).toEqual({ + badge: "no telemetry", + note: expect.stringContaining("No telemetry"), + }); + }); + + it("leaves a trustworthy report with no caveat at all", () => { + expect(buildReportLayout(viewModel(), messages).trust).toBeUndefined(); + }); +});