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.
This commit is contained in:
@@ -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<string, LayoutTrust> = {
|
||||
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<string, unknown> }): boolean {
|
||||
return vm.facts?.trustworthy !== false;
|
||||
}
|
||||
|
||||
/** The caveat for an untrustworthy report, chosen by `facts.untrustworthyReason`. */
|
||||
export function reportTrust(vm: { facts?: Record<string, unknown> }): 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),
|
||||
|
||||
@@ -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<string, unknown>): 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user