Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/report-start-latency-unknown.md
Original file line number Diff line number Diff line change
@@ -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
40 changes: 29 additions & 11 deletions apps/webapp/app/presenters/v3/reports/health/health-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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 = {
Expand Down
21 changes: 16 additions & 5 deletions apps/webapp/app/presenters/v3/reports/health/health-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: {
Expand All @@ -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.
Expand Down Expand Up @@ -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",
Expand All @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion apps/webapp/app/presenters/v3/reports/renderMarkdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`;
Expand Down Expand Up @@ -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":
Expand Down
31 changes: 31 additions & 0 deletions apps/webapp/test/reportHealth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
46 changes: 46 additions & 0 deletions apps/webapp/test/reportHealthData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
Loading