Skip to content

Commit c00fb9c

Browse files
authored
fix(webapp): report start latency as unknown when there is no data (#4544)
When the health report had no start-latency measurement for the window, it printed a confident "p95 0ms" and graded it healthy. It now shows "unknown" for that metric and skips grading it, so an absent measurement can't read as a green signal. A genuinely measured 0ms is still shown as 0ms: the loader keeps "no measurement" distinct from a measured zero instead of coercing both to 0.
1 parent 6e00aaf commit c00fb9c

6 files changed

Lines changed: 133 additions & 17 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
The health report now says start latency is "unknown" when there is no data for it, instead of showing a healthy-looking 0ms

apps/webapp/app/presenters/v3/reports/health/health-core.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,16 @@ export type HealthInput = {
2121
* series measured (v2) or estimated (v1).
2222
*/
2323
pending: { now: number; normal?: number; series: number[]; estimated: boolean };
24-
startLatency: { p95Ms: number; normalP95Ms: number; series: number[] };
24+
/**
25+
* p95 wait. `availability: "unknown"` = the source had no measurement, so `p95Ms` is a
26+
* placeholder that must not be graded — a 0 would read as a confident green.
27+
*/
28+
startLatency: {
29+
p95Ms: number;
30+
normalP95Ms?: number;
31+
series: number[];
32+
availability?: "measured" | "unknown";
33+
};
2534
throughput: { donePerMin: number; triggeredPerMin: number; normalTriggeredPerMin: number };
2635
failures: { rate: number; normalRate: number; series: number[] };
2736
duration: { p95Ms: number; normalP95Ms: number };
@@ -116,21 +125,30 @@ export function buildMetrics(input: HealthInput): Metric[] {
116125
const t = HEALTH_THRESHOLDS;
117126
const ev = input.flowEvidence;
118127

128+
const startLatencyUnknown = input.startLatency.availability === "unknown";
119129
const startLatency: Metric = {
120130
id: "start_latency_p95",
121131
value: input.startLatency.p95Ms,
132+
availability: startLatencyUnknown ? "unknown" : "measured",
122133
unit: "ms",
123134
aggregation: "p95",
124-
normal: input.startLatency.normalP95Ms,
125-
delta: delta(input.startLatency.p95Ms, input.startLatency.normalP95Ms),
126-
series: { points: input.startLatency.series, kind: "measured" },
127-
severity: multiplierSeverity(
128-
input.startLatency.p95Ms,
129-
input.startLatency.normalP95Ms,
130-
t.startLatency.warnMult,
131-
t.startLatency.critMult,
132-
t.startLatency.floor
133-
),
135+
normal: startLatencyUnknown ? undefined : input.startLatency.normalP95Ms,
136+
delta: startLatencyUnknown
137+
? undefined
138+
: delta(input.startLatency.p95Ms, input.startLatency.normalP95Ms),
139+
series: startLatencyUnknown
140+
? undefined
141+
: { points: input.startLatency.series, kind: "measured" },
142+
// Nothing measured -> nothing to classify (a placeholder must never grade green).
143+
severity: startLatencyUnknown
144+
? "ok"
145+
: multiplierSeverity(
146+
input.startLatency.p95Ms,
147+
input.startLatency.normalP95Ms,
148+
t.startLatency.warnMult,
149+
t.startLatency.critMult,
150+
t.startLatency.floor
151+
),
134152
};
135153

136154
const pending: Metric = {

apps/webapp/app/presenters/v3/reports/health/health-data.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ function num(value: unknown, fallback = 0): number {
3939
return Number.isFinite(n) ? n : fallback;
4040
}
4141

42+
/** Like `num`, but keeps "no measurement" distinct from a measured 0. */
43+
function optionalNum(value: unknown): number | undefined {
44+
const n = num(value, NaN);
45+
return Number.isFinite(n) ? n : undefined;
46+
}
47+
4248
function mean(xs: number[]): number {
4349
return xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length;
4450
}
@@ -313,7 +319,7 @@ async function tryQuery(
313319
export type FlowData = {
314320
flowSource: HealthInput["flowSource"];
315321
pending: { now: number; normal?: number; series: number[]; estimated: boolean };
316-
startLatency: { p95Ms: number; normalP95Ms: number; series: number[] };
322+
startLatency: HealthInput["startLatency"];
317323
evidence: HealthInput["flowEvidence"];
318324
/**
319325
* Epoch ms of the freshest telemetry the source saw (latest env_metrics bucket and/or latest
@@ -432,6 +438,8 @@ function buildQueueMetricsFlow(
432438
// env_metrics (still a real number) rather than a misleading confident zero (#7).
433439
const lastMeasuredQueued = num(series[series.length - 1]?.queued);
434440

441+
const waitP95 = optionalNum(liveScalar.wait_p95);
442+
435443
return {
436444
flowSource: "queue_metrics_v1",
437445
pending: {
@@ -441,9 +449,10 @@ function buildQueueMetricsFlow(
441449
estimated: false, // measured
442450
},
443451
startLatency: {
444-
p95Ms: num(liveScalar.wait_p95),
445-
normalP95Ms: num(baselineScalar.wait_p95),
452+
p95Ms: waitP95 ?? 0,
453+
normalP95Ms: optionalNum(baselineScalar.wait_p95),
446454
series: resampleSeries(series.map((r) => num(r.wait_p95))),
455+
availability: waitP95 === undefined ? "unknown" : "measured",
447456
},
448457
evidence: {
449458
// native resolution — cause discriminators read shares off this series.
@@ -476,6 +485,7 @@ export const SnapshotFlowSource: FlowSource = {
476485
return backlog;
477486
});
478487
const series = resampleSeries(proxy);
488+
const startLatencyP95 = optionalNum(ctx.liveScalar.start_latency_p95);
479489

480490
return {
481491
flowSource: "snapshot+runs",
@@ -488,9 +498,10 @@ export const SnapshotFlowSource: FlowSource = {
488498
estimated: true,
489499
},
490500
startLatency: {
491-
p95Ms: num(ctx.liveScalar.start_latency_p95),
492-
normalP95Ms: num(ctx.baselineScalar.start_latency_p95),
501+
p95Ms: startLatencyP95 ?? 0,
502+
normalP95Ms: optionalNum(ctx.baselineScalar.start_latency_p95),
493503
series: resampleSeries(ctx.liveSeries.map((r) => num(r.start_latency_p95))),
504+
availability: startLatencyP95 === undefined ? "unknown" : "measured",
494505
},
495506
// No cause-tree evidence; interpret falls back to v1 symptoms.
496507
evidence: EMPTY_EVIDENCE,

apps/webapp/app/presenters/v3/reports/renderMarkdown.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ function annotationSegment(metric: Metric, vm: ReportViewModel): string {
178178
}
179179

180180
function metricValueText(metric: Metric, msg: ReportMessages): string {
181+
// No measurement -> say so; `value` is a placeholder, not a real reading.
182+
if (metric.availability === "unknown") return "unknown";
181183
// concurrency etc. carry a limit -> "running/limit".
182184
if (metric.unit === "count" && metric.breakdown?.limit !== undefined) {
183185
return `${fmtCount(metric.value)}/${fmtCount(metric.breakdown.limit)}`;
@@ -259,7 +261,9 @@ function compactFact(metric: Metric): string | undefined {
259261
case "pending":
260262
return `pending ${fmtCount(metric.value)}${metric.normal !== undefined ? ` (normal ~${fmtCount(metric.normal)})` : ""}`;
261263
case "start_latency_p95":
262-
return `starts p95 ${fmtDuration(metric.value)}`;
264+
return metric.availability === "unknown"
265+
? "starts p95 unknown"
266+
: `starts p95 ${fmtDuration(metric.value)}`;
263267
case "failures":
264268
return `failures ${fmtPct(metric.value)}${metric.normal !== undefined ? ` (normal ~${fmtPct(metric.normal)})` : ""}`;
265269
case "dur_p95":

apps/webapp/test/reportHealth.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,37 @@ describe("freshness unknown is distinct from lagging", () => {
495495
});
496496
});
497497

498+
describe("start latency with no measurement", () => {
499+
const unknownInput: HealthInput = {
500+
...INPUT_B,
501+
startLatency: { p95Ms: 0, normalP95Ms: undefined, series: [], availability: "unknown" },
502+
};
503+
504+
it("marks the metric 'unknown' and doesn't classify it", () => {
505+
const metric = interpret(unknownInput).metrics.find((m) => m.id === "start_latency_p95")!;
506+
expect(metric.availability).toBe("unknown");
507+
expect(metric.severity).toBe("ok");
508+
expect(metric.normal).toBeUndefined();
509+
expect(metric.series).toBeUndefined(); // no sparkline for a placeholder
510+
});
511+
512+
it("renders 'unknown', never a confident 0ms", () => {
513+
const md = renderReportMarkdown(interpret(unknownInput));
514+
expect(md).toContain("starts p95 unknown");
515+
expect(md).not.toContain("starts p95 0ms");
516+
});
517+
518+
it("keeps a genuine 0 a measured 0ms", () => {
519+
const measured = interpret({
520+
...INPUT_B,
521+
startLatency: { p95Ms: 0, normalP95Ms: 7000, series: [0, 0], availability: "measured" },
522+
});
523+
const metric = measured.metrics.find((m) => m.id === "start_latency_p95")!;
524+
expect(metric.availability).toBe("measured");
525+
expect(renderReportMarkdown(measured)).toContain("starts p95 0ms");
526+
});
527+
});
528+
498529
describe("zero baseline is not a false green (absolute floors)", () => {
499530
it("pending spiking from a 0 baseline is not healthy", () => {
500531
const vm = interpret({

apps/webapp/test/reportHealthData.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,50 @@ describe("loadHealthInput — orchestration (query seam)", () => {
239239
expect(input.flowSource).toBe("queue_metrics_v1");
240240
expect(input.pending.now).toBe(900);
241241
});
242+
243+
it("no wait_p95 measurement -> start latency 'unknown', not a confident 0", async () => {
244+
const input = await loadHealthInput(
245+
fakeEnv,
246+
"1h",
247+
NOW,
248+
makeDeps({
249+
runs: RUNS_SCALAR,
250+
envSeries: [{ t: "a", queued: 10, running: 5, throttled: 0 }],
251+
envScalar: [{ wait_p95: null, avg_queued: 8, env_limit: 100 }],
252+
})
253+
);
254+
expect(input.flowSource).toBe("queue_metrics_v1");
255+
expect(input.startLatency.availability).toBe("unknown");
256+
expect(input.startLatency.normalP95Ms).toBeUndefined();
257+
});
258+
259+
it("a measured wait_p95 of 0 stays measured", async () => {
260+
const input = await loadHealthInput(
261+
fakeEnv,
262+
"1h",
263+
NOW,
264+
makeDeps({
265+
runs: RUNS_SCALAR,
266+
envSeries: [{ t: "a", queued: 10, running: 5, throttled: 0, wait_p95: 0 }],
267+
envScalar: [{ wait_p95: 0, avg_queued: 8, env_limit: 100 }],
268+
})
269+
);
270+
expect(input.startLatency.availability).toBe("measured");
271+
expect(input.startLatency.p95Ms).toBe(0);
272+
});
273+
274+
it("snapshot path with no start_latency_p95 -> 'unknown'", async () => {
275+
const input = await loadHealthInput(
276+
fakeEnv,
277+
"1h",
278+
NOW,
279+
makeDeps({
280+
runs: [{ ...RUNS_SCALAR[0], start_latency_p95: null }],
281+
runsSeries: [{ t: "a", triggered: 10, completed: 8, failures: 0 }],
282+
envSeries: [],
283+
})
284+
);
285+
expect(input.flowSource).toBe("snapshot+runs");
286+
expect(input.startLatency.availability).toBe("unknown");
287+
});
242288
});

0 commit comments

Comments
 (0)