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.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
area: webapp
|
||||
type: fix
|
||||
---
|
||||
|
||||
The health report now says start latency is "unknown" when there is no data for it, instead of showing a healthy-looking 0ms
|
||||
@@ -21,7 +21,16 @@ export type HealthInput = {
|
||||
* series measured (v2) or estimated (v1).
|
||||
*/
|
||||
pending: { now: number; normal?: number; series: number[]; estimated: boolean };
|
||||
startLatency: { p95Ms: number; normalP95Ms: number; series: number[] };
|
||||
/**
|
||||
* p95 wait. `availability: "unknown"` = the source had no measurement, so `p95Ms` is a
|
||||
* placeholder that must not be graded — a 0 would read as a confident green.
|
||||
*/
|
||||
startLatency: {
|
||||
p95Ms: number;
|
||||
normalP95Ms?: number;
|
||||
series: number[];
|
||||
availability?: "measured" | "unknown";
|
||||
};
|
||||
throughput: { donePerMin: number; triggeredPerMin: number; normalTriggeredPerMin: number };
|
||||
failures: { rate: number; normalRate: number; series: number[] };
|
||||
duration: { p95Ms: number; normalP95Ms: number };
|
||||
@@ -116,21 +125,30 @@ export function buildMetrics(input: HealthInput): Metric[] {
|
||||
const t = HEALTH_THRESHOLDS;
|
||||
const ev = input.flowEvidence;
|
||||
|
||||
const startLatencyUnknown = input.startLatency.availability === "unknown";
|
||||
const startLatency: Metric = {
|
||||
id: "start_latency_p95",
|
||||
value: input.startLatency.p95Ms,
|
||||
availability: startLatencyUnknown ? "unknown" : "measured",
|
||||
unit: "ms",
|
||||
aggregation: "p95",
|
||||
normal: input.startLatency.normalP95Ms,
|
||||
delta: delta(input.startLatency.p95Ms, input.startLatency.normalP95Ms),
|
||||
series: { points: input.startLatency.series, kind: "measured" },
|
||||
severity: multiplierSeverity(
|
||||
input.startLatency.p95Ms,
|
||||
input.startLatency.normalP95Ms,
|
||||
t.startLatency.warnMult,
|
||||
t.startLatency.critMult,
|
||||
t.startLatency.floor
|
||||
),
|
||||
normal: startLatencyUnknown ? undefined : input.startLatency.normalP95Ms,
|
||||
delta: startLatencyUnknown
|
||||
? undefined
|
||||
: delta(input.startLatency.p95Ms, input.startLatency.normalP95Ms),
|
||||
series: startLatencyUnknown
|
||||
? undefined
|
||||
: { points: input.startLatency.series, kind: "measured" },
|
||||
// Nothing measured -> nothing to classify (a placeholder must never grade green).
|
||||
severity: startLatencyUnknown
|
||||
? "ok"
|
||||
: multiplierSeverity(
|
||||
input.startLatency.p95Ms,
|
||||
input.startLatency.normalP95Ms,
|
||||
t.startLatency.warnMult,
|
||||
t.startLatency.critMult,
|
||||
t.startLatency.floor
|
||||
),
|
||||
};
|
||||
|
||||
const pending: Metric = {
|
||||
|
||||
@@ -39,6 +39,12 @@ function num(value: unknown, fallback = 0): number {
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
/** Like `num`, but keeps "no measurement" distinct from a measured 0. */
|
||||
function optionalNum(value: unknown): number | undefined {
|
||||
const n = num(value, NaN);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
|
||||
function mean(xs: number[]): number {
|
||||
return xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length;
|
||||
}
|
||||
@@ -313,7 +319,7 @@ async function tryQuery(
|
||||
export type FlowData = {
|
||||
flowSource: HealthInput["flowSource"];
|
||||
pending: { now: number; normal?: number; series: number[]; estimated: boolean };
|
||||
startLatency: { p95Ms: number; normalP95Ms: number; series: number[] };
|
||||
startLatency: HealthInput["startLatency"];
|
||||
evidence: HealthInput["flowEvidence"];
|
||||
/**
|
||||
* Epoch ms of the freshest telemetry the source saw (latest env_metrics bucket and/or latest
|
||||
@@ -432,6 +438,8 @@ function buildQueueMetricsFlow(
|
||||
// env_metrics (still a real number) rather than a misleading confident zero (#7).
|
||||
const lastMeasuredQueued = num(series[series.length - 1]?.queued);
|
||||
|
||||
const waitP95 = optionalNum(liveScalar.wait_p95);
|
||||
|
||||
return {
|
||||
flowSource: "queue_metrics_v1",
|
||||
pending: {
|
||||
@@ -441,9 +449,10 @@ function buildQueueMetricsFlow(
|
||||
estimated: false, // measured
|
||||
},
|
||||
startLatency: {
|
||||
p95Ms: num(liveScalar.wait_p95),
|
||||
normalP95Ms: num(baselineScalar.wait_p95),
|
||||
p95Ms: waitP95 ?? 0,
|
||||
normalP95Ms: optionalNum(baselineScalar.wait_p95),
|
||||
series: resampleSeries(series.map((r) => num(r.wait_p95))),
|
||||
availability: waitP95 === undefined ? "unknown" : "measured",
|
||||
},
|
||||
evidence: {
|
||||
// native resolution — cause discriminators read shares off this series.
|
||||
@@ -476,6 +485,7 @@ export const SnapshotFlowSource: FlowSource = {
|
||||
return backlog;
|
||||
});
|
||||
const series = resampleSeries(proxy);
|
||||
const startLatencyP95 = optionalNum(ctx.liveScalar.start_latency_p95);
|
||||
|
||||
return {
|
||||
flowSource: "snapshot+runs",
|
||||
@@ -488,9 +498,10 @@ export const SnapshotFlowSource: FlowSource = {
|
||||
estimated: true,
|
||||
},
|
||||
startLatency: {
|
||||
p95Ms: num(ctx.liveScalar.start_latency_p95),
|
||||
normalP95Ms: num(ctx.baselineScalar.start_latency_p95),
|
||||
p95Ms: startLatencyP95 ?? 0,
|
||||
normalP95Ms: optionalNum(ctx.baselineScalar.start_latency_p95),
|
||||
series: resampleSeries(ctx.liveSeries.map((r) => num(r.start_latency_p95))),
|
||||
availability: startLatencyP95 === undefined ? "unknown" : "measured",
|
||||
},
|
||||
// No cause-tree evidence; interpret falls back to v1 symptoms.
|
||||
evidence: EMPTY_EVIDENCE,
|
||||
|
||||
@@ -178,6 +178,8 @@ function annotationSegment(metric: Metric, vm: ReportViewModel): string {
|
||||
}
|
||||
|
||||
function metricValueText(metric: Metric, msg: ReportMessages): string {
|
||||
// No measurement -> say so; `value` is a placeholder, not a real reading.
|
||||
if (metric.availability === "unknown") return "unknown";
|
||||
// concurrency etc. carry a limit -> "running/limit".
|
||||
if (metric.unit === "count" && metric.breakdown?.limit !== undefined) {
|
||||
return `${fmtCount(metric.value)}/${fmtCount(metric.breakdown.limit)}`;
|
||||
@@ -259,7 +261,9 @@ function compactFact(metric: Metric): string | undefined {
|
||||
case "pending":
|
||||
return `pending ${fmtCount(metric.value)}${metric.normal !== undefined ? ` (normal ~${fmtCount(metric.normal)})` : ""}`;
|
||||
case "start_latency_p95":
|
||||
return `starts p95 ${fmtDuration(metric.value)}`;
|
||||
return metric.availability === "unknown"
|
||||
? "starts p95 unknown"
|
||||
: `starts p95 ${fmtDuration(metric.value)}`;
|
||||
case "failures":
|
||||
return `failures ${fmtPct(metric.value)}${metric.normal !== undefined ? ` (normal ~${fmtPct(metric.normal)})` : ""}`;
|
||||
case "dur_p95":
|
||||
|
||||
@@ -495,6 +495,37 @@ describe("freshness unknown is distinct from lagging", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("start latency with no measurement", () => {
|
||||
const unknownInput: HealthInput = {
|
||||
...INPUT_B,
|
||||
startLatency: { p95Ms: 0, normalP95Ms: undefined, series: [], availability: "unknown" },
|
||||
};
|
||||
|
||||
it("marks the metric 'unknown' and doesn't classify it", () => {
|
||||
const metric = interpret(unknownInput).metrics.find((m) => m.id === "start_latency_p95")!;
|
||||
expect(metric.availability).toBe("unknown");
|
||||
expect(metric.severity).toBe("ok");
|
||||
expect(metric.normal).toBeUndefined();
|
||||
expect(metric.series).toBeUndefined(); // no sparkline for a placeholder
|
||||
});
|
||||
|
||||
it("renders 'unknown', never a confident 0ms", () => {
|
||||
const md = renderReportMarkdown(interpret(unknownInput));
|
||||
expect(md).toContain("starts p95 unknown");
|
||||
expect(md).not.toContain("starts p95 0ms");
|
||||
});
|
||||
|
||||
it("keeps a genuine 0 a measured 0ms", () => {
|
||||
const measured = interpret({
|
||||
...INPUT_B,
|
||||
startLatency: { p95Ms: 0, normalP95Ms: 7000, series: [0, 0], availability: "measured" },
|
||||
});
|
||||
const metric = measured.metrics.find((m) => m.id === "start_latency_p95")!;
|
||||
expect(metric.availability).toBe("measured");
|
||||
expect(renderReportMarkdown(measured)).toContain("starts p95 0ms");
|
||||
});
|
||||
});
|
||||
|
||||
describe("zero baseline is not a false green (absolute floors)", () => {
|
||||
it("pending spiking from a 0 baseline is not healthy", () => {
|
||||
const vm = interpret({
|
||||
|
||||
@@ -239,4 +239,50 @@ describe("loadHealthInput — orchestration (query seam)", () => {
|
||||
expect(input.flowSource).toBe("queue_metrics_v1");
|
||||
expect(input.pending.now).toBe(900);
|
||||
});
|
||||
|
||||
it("no wait_p95 measurement -> start latency 'unknown', not a confident 0", async () => {
|
||||
const input = await loadHealthInput(
|
||||
fakeEnv,
|
||||
"1h",
|
||||
NOW,
|
||||
makeDeps({
|
||||
runs: RUNS_SCALAR,
|
||||
envSeries: [{ t: "a", queued: 10, running: 5, throttled: 0 }],
|
||||
envScalar: [{ wait_p95: null, avg_queued: 8, env_limit: 100 }],
|
||||
})
|
||||
);
|
||||
expect(input.flowSource).toBe("queue_metrics_v1");
|
||||
expect(input.startLatency.availability).toBe("unknown");
|
||||
expect(input.startLatency.normalP95Ms).toBeUndefined();
|
||||
});
|
||||
|
||||
it("a measured wait_p95 of 0 stays measured", async () => {
|
||||
const input = await loadHealthInput(
|
||||
fakeEnv,
|
||||
"1h",
|
||||
NOW,
|
||||
makeDeps({
|
||||
runs: RUNS_SCALAR,
|
||||
envSeries: [{ t: "a", queued: 10, running: 5, throttled: 0, wait_p95: 0 }],
|
||||
envScalar: [{ wait_p95: 0, avg_queued: 8, env_limit: 100 }],
|
||||
})
|
||||
);
|
||||
expect(input.startLatency.availability).toBe("measured");
|
||||
expect(input.startLatency.p95Ms).toBe(0);
|
||||
});
|
||||
|
||||
it("snapshot path with no start_latency_p95 -> 'unknown'", async () => {
|
||||
const input = await loadHealthInput(
|
||||
fakeEnv,
|
||||
"1h",
|
||||
NOW,
|
||||
makeDeps({
|
||||
runs: [{ ...RUNS_SCALAR[0], start_latency_p95: null }],
|
||||
runsSeries: [{ t: "a", triggered: 10, completed: 8, failures: 0 }],
|
||||
envSeries: [],
|
||||
})
|
||||
);
|
||||
expect(input.flowSource).toBe("snapshot+runs");
|
||||
expect(input.startLatency.availability).toBe("unknown");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user