Skip to content

Commit 1ee6704

Browse files
committed
fix(webapp): describe how far a report metric fell
A fall's multiplier rounds to 0 or 1, so every drop rendered as "flat" — a metric that collapsed from 100 to 5 read as unchanged. Measure the fall against the baseline instead, and show a bare arrow when it collapsed to nothing.
1 parent 05505d5 commit 1ee6704

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,12 +518,29 @@ function metricValue(metric: LayoutMetricInput, messages: ReportMessages): strin
518518
: fmtValue(metric.value, metric.unit);
519519
}
520520

521+
/**
522+
* How far a metric fell below its baseline: `undefined` when the fall doesn't round past 1×, and
523+
* `null` when it collapsed to nothing and no multiplier can say it.
524+
*/
525+
function fallMultiplier(metric: LayoutMetricInput): number | null | undefined {
526+
if (metric.normal === undefined || metric.normal <= 0) return undefined;
527+
if (metric.value <= 0) return null;
528+
const fall = Math.round(metric.normal / metric.value);
529+
return fall > 1 ? fall : undefined;
530+
}
531+
521532
/**
522533
* A metric's movement against its baseline. A multiplier only reads as movement once it rounds past
523534
* 1×; below that a metric with a baseline is flat, and one without has nothing to compare against.
524535
*/
525536
function metricDelta(metric: LayoutMetricInput): LayoutDelta | undefined {
526537
const delta = metric.delta;
538+
// A fall's own multiplier rounds to 0 or 1, so measure how far it fell instead.
539+
if (delta?.dir === "down") {
540+
const fall = fallMultiplier(metric);
541+
if (fall === null) return { text: REPORT_GLYPH.down, dir: "down" };
542+
if (fall !== undefined) return { text: `${REPORT_GLYPH.down} ${fall}×`, dir: "down" };
543+
}
527544
if (delta && delta.mult !== undefined && delta.mult > 1 && delta.dir !== "flat") {
528545
return {
529546
text: `${delta.dir === "up" ? REPORT_GLYPH.up : REPORT_GLYPH.down} ${delta.mult}×`,
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
buildReportLayout,
4+
type LayoutMetricInput,
5+
type LayoutViewModel,
6+
REPORT_GLYPH,
7+
} from "~/presenters/v3/reports/report-layout";
8+
import { reportMessages } from "~/presenters/v3/reports/report-messages";
9+
import { delta } from "~/presenters/v3/reports/report-view-model";
10+
11+
const messages = reportMessages("health");
12+
13+
function viewModel(overrides: Partial<LayoutViewModel> = {}): LayoutViewModel {
14+
return {
15+
title: "health",
16+
scope: "prod",
17+
period: "last 60m",
18+
windowMinutes: 60,
19+
summary: { severity: "warn", statements: [{ findingType: "flow", severity: "warn" }] },
20+
findings: [{ type: "flow", severity: "warn", reason: "backlog_growing", metricIds: [] }],
21+
metrics: [],
22+
footer: [],
23+
...overrides,
24+
};
25+
}
26+
27+
/** Builds the metric the way the presenter does, so the rounded multiplier is the real one. */
28+
function metricRow(metric: Omit<LayoutMetricInput, "delta">) {
29+
const withDelta: LayoutMetricInput = { ...metric, delta: delta(metric.value, metric.normal) };
30+
const layout = buildReportLayout(
31+
viewModel({
32+
findings: [
33+
{ type: "flow", severity: "warn", reason: "backlog_growing", metricIds: [metric.id] },
34+
],
35+
metrics: [withDelta],
36+
}),
37+
messages
38+
);
39+
const rows = [...(layout.hero?.metrics ?? []), ...layout.findings.flatMap((f) => f.metrics)];
40+
const row = rows.find((r) => r.id === metric.id);
41+
if (!row) throw new Error(`metric row ${metric.id} was not rendered`);
42+
return row;
43+
}
44+
45+
describe("report layout — a metric's movement against its baseline", () => {
46+
it("renders a collapse as a down arrow with how far it fell", () => {
47+
const row = metricRow({
48+
id: "throughput",
49+
value: 5,
50+
normal: 100,
51+
unit: "perMin",
52+
severity: "crit",
53+
});
54+
55+
expect(row.delta).toEqual({ text: `${REPORT_GLYPH.down} 20×`, dir: "down" });
56+
});
57+
58+
it("renders a collapse to zero as a bare down arrow", () => {
59+
const row = metricRow({
60+
id: "throughput",
61+
value: 0,
62+
normal: 100,
63+
unit: "perMin",
64+
severity: "crit",
65+
});
66+
67+
expect(row.delta).toEqual({ text: REPORT_GLYPH.down, dir: "down" });
68+
});
69+
70+
it("leaves a dip that doesn't round past 1× reading as flat", () => {
71+
const row = metricRow({
72+
id: "throughput",
73+
value: 95,
74+
normal: 100,
75+
unit: "perMin",
76+
severity: "ok",
77+
});
78+
79+
expect(row.delta).toEqual({ text: `${REPORT_GLYPH.flat} flat`, dir: "flat" });
80+
});
81+
82+
it("still renders a rise with its multiplier", () => {
83+
const row = metricRow({
84+
id: "pending",
85+
value: 1600,
86+
normal: 100,
87+
unit: "count",
88+
severity: "crit",
89+
});
90+
91+
expect(row.delta).toEqual({ text: `${REPORT_GLYPH.up} 16×`, dir: "up" });
92+
});
93+
94+
it("still renders an unmoved metric as flat", () => {
95+
const row = metricRow({
96+
id: "pending",
97+
value: 100,
98+
normal: 100,
99+
unit: "count",
100+
severity: "ok",
101+
});
102+
103+
expect(row.delta).toEqual({ text: `${REPORT_GLYPH.flat} flat`, dir: "flat" });
104+
});
105+
106+
it("says nothing about a metric with no baseline", () => {
107+
const row = metricRow({ id: "pending", value: 100, unit: "count", severity: "ok" });
108+
109+
expect(row.delta).toBeUndefined();
110+
});
111+
});

0 commit comments

Comments
 (0)