Display

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { AlertCircle, Bug, Minus, PackageOpen, ShieldAlert, ShieldCheck, TrendingDown, TrendingUp } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import type { ReleaseHealthCrash, ReleaseHealthData, ReleaseHealthItem } from "./release-health.contract"

export interface ReleaseHealthProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">, ReleaseHealthData {
  /** Panel heading, and the accessible name of the card. */
  title?: string
  /** Second header line — the reporting window belongs here. */
  description?: string

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/release-health.json

Prompt

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 sessions over the listed total, apportioned by largest remainder so the column adds up to exactly 100. The reporter's own adoptionPct is 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.0 below 4.9.0 and 4.3.0-rc.2 above 4.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.

On This Page