Release Health
A per-release stability readout — adoption derived from session counts, crash-free session and user rates against the payload's own bars, the change from the previous release by semver precedence, and the top crash groups with what it would take to clear the bar.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/release-health.jsonPrompt
Build a React + TypeScript + Tailwind "ReleaseHealth" panel with zod and
lucide-react, on shadcn Badge and Button. It reads one crash reporter's window
and answers a single question: is this release safe to roll further?
Contract
- A zod schema in a sibling contract file is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready";
releases: { version, releasedAt (ISO instant with offset), sessions (int),
users (int), crashFreeSessionsPct, crashFreeUsersPct, adoptionPct? }[];
thresholds: { sessions, users };
crashes: { id?, version, title, count (int), firstSeenVersion }[] }.
- Rates are percentage POINTS (0-100), carried at full precision: crash-free
rates live between 99 and 100 and 99.18 vs 99.20 is the whole decision.
- sessions / users are required because they are the evidence. "99.1% crash-free"
over 2,000 sessions and over 200,000 sessions is the same rate and a completely
different amount of certainty — and without a denominator the panel cannot turn
a rate back into a number of crashes, which is what "what would clear the bar"
needs.
- adoptionPct is the reporter's FLEET-wide share, across versions the payload may
not list. Never draw the bars from it: derive adoption from sessions instead, so
the bar and its label come out of the same arithmetic, and report the fleet
figure beside the derived one, labelled, so the two can differ without lying.
- crashes[].count is EVENTS, not sessions — one session can log the same crash
twice. Say so in the UI instead of pretending the two reconcile.
- Props = z.infer of the schema plus title, description, selectedVersion /
defaultSelectedVersion / onSelectVersion, onRetry, emptyState, locale
(default "en-US"), timeZone (default "UTC"), skeletonReleases (clamped 1-8) and
className. Forward the ref and spread the rest onto the root.
Behavior — the arithmetic (this is what the component is for)
- Implement semver 2.0.0 properly, with the regex semver.org recommends, plus an
accepted-and-stripped leading "v" for git tags. Be strict: numeric identifiers
may not carry leading zeroes (§2, §9), so "1.02.3" and calendar versions like
"2026.08.1" are NOT semver. Build metadata is parsed only to be discarded (§10).
Precedence follows §11: major/minor/patch numerically, a release outranks any
prerelease of the same core, prerelease identifiers left to right with numeric
ones below alphanumeric ones and a shorter set below a longer one sharing its
prefix. String comparison would put 4.10.0 below 4.9.0 and 4.3.0-rc.2 above
4.3.0 — a hotfix reported as older than the build it fixes.
- Decide the ordering mode ONCE for the whole list: precedence when every version
parses, release date for everybody when even one does not. A per-pair fallback
is an intransitive comparator, and Array.sort may then return any permutation.
Print which rule won — "newest" means different things under the two.
- Adoption share = each release's sessions over the listed total, apportioned by
LARGEST REMAINDER so the column always adds up to exactly 100. Zero total
sessions yields no share at all, not a column of 0%.
- Verdict per rate: >= threshold clears it (a release exactly on the bar is
healthy); a rate over zero sessions is "not measurable", never a perfect 100%.
The release verdict is the worse of the two.
- Compare at full precision, print at two decimals — and mind the gap between the
two: a rate that misses its bar by less than half a hundredth (99.197 against a
99.2 bar) prints as the bar's own number, so a tile reads "99.20%" directly
above "below the 99.20% bar". Do not round the comparison to match; print that
one as "just under 99.20%", so no figure on screen equals the bar it is failing.
- Change versus the previous release is reported in percentage points (pp) against
the named version, and it reads the two VERDICTS, not the two raw numbers: with
no earlier release listed, or with either side not measurable (this build or the
one below it has no sessions), name that reason instead of signing a pp figure.
Subtracting a rate the panel itself prints as "—" puts "+0.50 pp" under a tile
that just said there is no rate, and flags the next release as a regression
against a rate nobody ever measured.
- Below the bar, state what would clear it, in whole units:
affected = round(total x (100 - rate) / 100), allowed = floor(total x
(100 - threshold) / 100), gap = affected - allowed. Offer the second lever too:
holding the crash count, the rate reaches the bar at affected / (1 - T/100)
sessions. At a 100% bar there is no such volume — say so rather than quoting an
impossible number. When the gap falls below one whole session, say only that:
it is a rounding artefact of the reported rate and says nothing about volume
(41,280 sessions produce it as readily as forty). Add "too small for the bar to
tell a bad build from a quiet one" only when the volume really is too small —
total x (100 - T) / 100 < 1, i.e. one crashed session already breaks the bar.
- Crash groups: filter to the selected version, sort by count desc, apportion
their shares by largest remainder (of the LISTED events — this is a top list,
not the whole tail, and the caption says so). Zero events across every listed
group is no share at all, same as zero sessions: the caption must not promise
shares the rows then do not print. Label each group by comparing
firstSeenVersion with the selected version: equal is "New in this version",
lower is "Carried over since X", higher is "Also on the later X". When the two
cannot be ordered (a nightly tag that is neither semver nor listed), print the
version and claim no direction at all.
- Four first-class status branches: loading (skeleton with the real anatomy in an
aria-hidden subtree plus one sr-only role=status), empty (replaceable
zero-state, also used when status is ready with no releases), error (message,
no partial rates, "Try again" only when onRetry exists), ready (the panel).
Interaction
- The release list is a vertical tablist: role=tablist / role=tab with
aria-selected and aria-controls, roving tabindex, and automatic activation —
Up/Down (and Left/Right) plus Home/End move focus and selection together, so
the panel below always describes the focused release. The panel is a
role=tabpanel, labelled by its tab and focusable.
- Selection is uncontrolled by default and can be controlled with
selectedVersion. Never sync it through an effect: derive the active release
each render, so a payload that drops the selected version falls back to the
newest instead of rendering an empty panel.
Rendering & styling
- Semantic tokens only: bg-card shell, bg-muted tracks and tiles,
text-muted-foreground captions, primary for the selected row and the "clears
the bar" tone, destructive for a breach and for a crash group new in this
release, border and ring. Do not hardcode a green — "good" is the plain
foreground plus the accent, and only "bad" spends the chromatic token.
- Colour is never the only channel: every verdict is a word ("Below the bar"),
every row carries an icon, and every row's aria-label spells out version, date,
share, volume and verdict as a sentence.
- Width comes from @container queries, not viewport breakpoints, so the panel
works at 420px in a sidebar and at 900px on a dashboard. Figures are
tabular-nums; long crash titles break-words rather than truncate.
- Only the skeleton animates, with motion-reduce:animate-none. There are no
timers, observers or global listeners at all — nothing to leak.
Customization levers
- Bars: thresholds are data, not a constant. Feeding them from the release train's
policy (a stricter bar for the 100% rollout stage than for the 1% canary)
relabels every verdict and rewrites every remediation sentence.
- Metric pair: the two tiles are one component rendered twice — swap crash-free
users for ANR-free sessions, watchdog terminations or hang rate by adding the
rate and its bar to the contract and repeating the tile.
- Density: drop the crash-group list for a dashboard tile, or drop the adoption
bars and keep the verdict pill for a rollout checklist; the remediation line is
the one piece that should not be cut.
- Ordering: swap the mode decision for "always by date" if your versions are
calendar-based and precedence never applies.
- Verdict vocabulary: the label/icon/tone tables are three maps — "Ship it" /
"Hold" / "Roll back" fits the same structure without touching the arithmetic.
- Actions: the panel reports and never mutates. Wire a "Halt rollout" button in
the header via the spread props and let the host own the mutation.Concepts
- Adoption is derived, never trusted — the bars come from each release's
sessionsover the listed total, apportioned by largest remainder so the column adds up to exactly 100. The reporter's ownadoptionPctis fleet-wide (it counts versions this payload never lists), so it is printed beside the derived share and labelled instead of quietly replacing it — two numbers that mean different things, both true. - Semver precedence, implemented not approximated — string ordering puts
4.10.0below4.9.0and4.3.0-rc.2above4.3.0, which reports a hotfix as older than the build it fixes. The parser follows semver.org strictly enough to reject calendar versions, and rejection is useful information: the whole list falls back to release dates under one consistent rule rather than a comparator that contradicts itself. - A rate over zero sessions is not a rate — a build that shipped and was never launched reads as "not measurable". Reporting it as 100% crash-free is the failure mode that makes an unlaunched build look like the safest thing in the list.
- What it would take, in whole units — below the bar, the panel converts the rate back through its denominator: this many sessions crashed, this many are allowed at this volume, so this many have to go — or this many more sessions have to land clean at the same crash count. At a 100% bar no volume of clean traffic suffices, and the copy says so instead of quoting an impossible number.
- New versus carried over — a crash group first seen on the selected version is a regression this release introduced; one first seen earlier merely rode along. The two call for opposite decisions (fix and re-cut, or ship and schedule), so they are labelled separately — and when the versions cannot be ordered, no direction is claimed at all.
- Events are not sessions — crash counts are events, and one session can log several, so the group counts never reconcile with the crashed-session figure the rate implies. The shares are of the listed events, and the caption says both things rather than letting a reader add up a top-five list into a total.
- Selection with a real consequence — the release list is a tablist with roving tabindex and automatic activation: arrowing down re-reads the whole panel (rates, change, remediation, crash groups) for that build. Selection is derived each render rather than synced in an effect, so a payload that drops the selected version falls back to the newest instead of blanking.
QR Login
A phone-scan sign-in panel that walks waiting → scanned → approved → expired, attributes the scan so a wrong one can be rejected, and shows a derived pairing phrase instead of the code.
Feature Flag Rollout
The rollout control for one flag — a hashed percentage gate with its estimated reach, ordered first-match-wins targeting rules, and a live evaluation preview that names the deciding rule or bucket.