Skip to content

Commit 0db1cf0

Browse files
committed
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.
1 parent 1ee6704 commit 0db1cf0

2 files changed

Lines changed: 125 additions & 10 deletions

File tree

apps/webapp/app/presenters/v3/reports/report-layout.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,35 @@ export const REPORT_LABELS = {
5050
read: "read:",
5151
/** The footer heading. */
5252
nextSteps: "Next steps",
53-
/** Shown beside the report's name when the data can't be trusted. */
54-
staleBadge: "stale data",
55-
staleNote:
56-
"The telemetry behind this report is stale, so the numbers below are informational only.",
5753
} as const;
5854

55+
/** The flag beside the report's name, and the caveat under its headline. */
56+
export type LayoutTrust = { badge: string; note: string };
57+
58+
/**
59+
* Why a report's numbers can't be trusted, in its own words. Stale, absent and unmeasured are three
60+
* different states, and a snapshot with no telemetry feed must not be called stale.
61+
*/
62+
const TRUST_CAVEATS: Record<string, LayoutTrust> = {
63+
telemetry_stale: {
64+
badge: "stale data",
65+
note: "The telemetry behind this report is stale, so the numbers below are informational only.",
66+
},
67+
telemetry_absent: {
68+
badge: "no telemetry",
69+
note: "No telemetry reached this report, so the numbers below are a point-in-time snapshot rather than a measured window.",
70+
},
71+
flow_unmeasured: {
72+
badge: "unmeasured",
73+
note: "Throughput could not be measured over this window, so the numbers below are informational only.",
74+
},
75+
};
76+
77+
const TRUST_CAVEAT_FALLBACK: LayoutTrust = {
78+
badge: "unverified data",
79+
note: "The data behind this report could not be verified, so the numbers below are informational only.",
80+
};
81+
5982
/**
6083
* The report's sections, top to bottom. A renderer walks this order; a new section has to be added
6184
* 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"]);
87110
const NEUTRAL_REASONS = new Set(["freshness_unknown", "flow_unmeasured"]);
88111

89112
/**
90-
* `facts.trustworthy === false` means the telemetry behind the verdict is stale, so the numbers are
91-
* informational only. Absent = trustworthy (the common case, and what pre-`facts` snapshots imply).
113+
* `facts.trustworthy === false` means the numbers behind the verdict are informational only. Absent
114+
* = trustworthy (the common case, and what pre-`facts` snapshots imply).
92115
*/
93116
export function reportIsTrustworthy(vm: { facts?: Record<string, unknown> }): boolean {
94117
return vm.facts?.trustworthy !== false;
95118
}
96119

120+
/** The caveat for an untrustworthy report, chosen by `facts.untrustworthyReason`. */
121+
export function reportTrust(vm: { facts?: Record<string, unknown> }): LayoutTrust | undefined {
122+
if (reportIsTrustworthy(vm)) return undefined;
123+
const reason = vm.facts?.untrustworthyReason;
124+
return (typeof reason === "string" ? TRUST_CAVEATS[reason] : undefined) ?? TRUST_CAVEAT_FALLBACK;
125+
}
126+
97127
export function reportTone(severity: Severity, reason?: string): ReportTone {
98128
return reason !== undefined && NEUTRAL_REASONS.has(reason) ? "neutral" : severity;
99129
}
@@ -283,7 +313,7 @@ export type LayoutFooterEntry = {
283313
export type ReportLayout = {
284314
header: { name: string; meta: string };
285315
/** Present only when the data can't be trusted. */
286-
trust?: { badge: string; note: string };
316+
trust?: LayoutTrust;
287317
headline: { tone: ReportTone; glyph: string; severity: Severity; phrase: string; text?: string };
288318
/** The finding the headline speaks for, always expanded. */
289319
hero?: LayoutFinding;
@@ -343,14 +373,14 @@ export function buildReportLayout(vm: LayoutViewModel, messages: ReportMessages)
343373
.filter((finding) => finding.read !== undefined && !UNASSESSABLE_REASONS.has(finding.reason))
344374
.map((finding) => fillTokens(messages.readMessage(finding.read!), tokens));
345375

376+
const trust = reportTrust(vm);
377+
346378
return {
347379
header: {
348380
name: vm.title,
349381
meta: [vm.scope, vm.period, vm.baselineLabel].filter(Boolean).join(" · "),
350382
},
351-
...(reportIsTrustworthy(vm)
352-
? {}
353-
: { trust: { badge: REPORT_LABELS.staleBadge, note: REPORT_LABELS.staleNote } }),
383+
...(trust === undefined ? {} : { trust }),
354384
headline: {
355385
severity: vm.summary.severity,
356386
tone: reportTone(vm.summary.severity, heroStatement?.reason),
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
buildReportLayout,
4+
type LayoutViewModel,
5+
reportTrust,
6+
} from "~/presenters/v3/reports/report-layout";
7+
import { reportMessages } from "~/presenters/v3/reports/report-messages";
8+
9+
const messages = reportMessages("health");
10+
11+
function viewModel(facts?: Record<string, unknown>): LayoutViewModel {
12+
return {
13+
title: "health",
14+
scope: "prod",
15+
period: "last 60m",
16+
windowMinutes: 60,
17+
summary: { severity: "warn", statements: [{ findingType: "flow", severity: "warn" }] },
18+
findings: [{ type: "flow", severity: "warn", reason: "backlog_growing", metricIds: [] }],
19+
metrics: [],
20+
footer: [],
21+
...(facts === undefined ? {} : { facts }),
22+
};
23+
}
24+
25+
function trustFor(untrustworthyReason?: string) {
26+
return reportTrust({
27+
facts: { trustworthy: false, ...(untrustworthyReason ? { untrustworthyReason } : {}) },
28+
});
29+
}
30+
31+
describe("report layout — why the numbers can't be trusted", () => {
32+
it("calls stale telemetry stale", () => {
33+
expect(trustFor("telemetry_stale")?.badge).toBe("stale data");
34+
expect(trustFor("telemetry_stale")?.note).toContain("stale");
35+
});
36+
37+
it("does not call a report with no telemetry stale", () => {
38+
const trust = trustFor("telemetry_absent");
39+
40+
expect(trust?.badge).toBe("no telemetry");
41+
expect(trust?.note).toContain("No telemetry");
42+
expect(trust?.note).not.toContain("stale");
43+
});
44+
45+
it("does not call an unmeasured flow stale", () => {
46+
const trust = trustFor("flow_unmeasured");
47+
48+
expect(trust?.badge).toBe("unmeasured");
49+
expect(trust?.note).not.toContain("stale");
50+
});
51+
52+
it("gives the three untrustworthy states three different badges", () => {
53+
const badges = ["telemetry_stale", "telemetry_absent", "flow_unmeasured"].map(
54+
(reason) => trustFor(reason)?.badge
55+
);
56+
57+
expect(new Set(badges).size).toBe(3);
58+
});
59+
60+
it("falls back without claiming staleness when the reason is missing", () => {
61+
expect(trustFor()).toBeDefined();
62+
expect(trustFor()?.note).not.toContain("stale");
63+
});
64+
65+
it("says nothing when the report is trustworthy", () => {
66+
expect(reportTrust({ facts: { trustworthy: true } })).toBeUndefined();
67+
expect(reportTrust({})).toBeUndefined();
68+
});
69+
70+
it("carries the chosen caveat into the layout", () => {
71+
const layout = buildReportLayout(
72+
viewModel({ trustworthy: false, untrustworthyReason: "telemetry_absent" }),
73+
messages
74+
);
75+
76+
expect(layout.trust).toEqual({
77+
badge: "no telemetry",
78+
note: expect.stringContaining("No telemetry"),
79+
});
80+
});
81+
82+
it("leaves a trustworthy report with no caveat at all", () => {
83+
expect(buildReportLayout(viewModel(), messages).trust).toBeUndefined();
84+
});
85+
});

0 commit comments

Comments
 (0)