-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat(release): surface adoption and health metrics in list and view (#463) #680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
e1b6a18
e8264ea
49c74de
c27f041
c3e442e
5472658
5276d9c
d09c7da
46507ce
48aac15
71591f1
dfaf768
82e0bd2
9521fe1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| /** | ||
| * Shared formatting utilities for release commands. | ||
| * | ||
| * Small helpers used by both `list.ts` and `view.ts` to format | ||
| * health/adoption metrics consistently. | ||
| */ | ||
|
|
||
| /** | ||
| * Format a percentage value with one decimal place, or "—" when absent. | ||
| * | ||
| * @example fmtPct(42.3) // "42.3%" | ||
| * @example fmtPct(null) // "—" | ||
| */ | ||
| export function fmtPct(value: number | null | undefined): string { | ||
| if (value === null || value === undefined) { | ||
| return "—"; | ||
| } | ||
| return `${value.toFixed(1)}%`; | ||
| } | ||
|
|
||
| /** | ||
| * Format an integer count with thousands separators, or "—" when absent. | ||
| * | ||
| * @example fmtCount(52000) // "52,000" | ||
| * @example fmtCount(null) // "—" | ||
| */ | ||
| export function fmtCount(value: number | null | undefined): string { | ||
| if (value === null || value === undefined) { | ||
| return "—"; | ||
| } | ||
| return value.toLocaleString("en-US"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| /** | ||
| * sentry release view | ||
| * | ||
| * View details of a specific release. | ||
| * View details of a specific release, including per-project | ||
| * health and adoption metrics when available. | ||
| */ | ||
|
|
||
| import type { OrgReleaseResponse } from "@sentry/api"; | ||
|
|
@@ -11,8 +12,10 @@ import { buildCommand } from "../../lib/command.js"; | |
| import { ContextError } from "../../lib/errors.js"; | ||
| import { | ||
| colorTag, | ||
| escapeMarkdownCell, | ||
| escapeMarkdownInline, | ||
| mdKvTable, | ||
| mdTableHeader, | ||
| renderMarkdown, | ||
| safeCodeSpan, | ||
| } from "../../lib/formatters/markdown.js"; | ||
|
|
@@ -24,8 +27,68 @@ import { | |
| FRESH_FLAG, | ||
| } from "../../lib/list-command.js"; | ||
| import { resolveOrg } from "../../lib/resolve-target.js"; | ||
| import { fmtCount, fmtPct } from "./format.js"; | ||
| import { parseReleaseArg } from "./parse.js"; | ||
|
|
||
| /** Format a crash-free rate with color coding (green ≥ 99, yellow ≥ 95, red < 95). */ | ||
| function fmtCrashFree(value: number | null | undefined): string { | ||
| if (value === null || value === undefined) { | ||
| return "—"; | ||
| } | ||
| const formatted = `${value.toFixed(1)}%`; | ||
| if (value >= 99) { | ||
| return colorTag("green", formatted); | ||
| } | ||
| if (value >= 95) { | ||
| return colorTag("yellow", formatted); | ||
| } | ||
| return colorTag("red", formatted); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing muted styling for null crash-free values in listLow Severity In the per-project health table, Reviewed by Cursor Bugbot for commit 82e0bd2. Configure here. |
||
|
|
||
| /** | ||
| * Build a markdown table of per-project health data. | ||
| * | ||
| * Only includes projects that have health data. Returns empty string | ||
| * if no project has data (so the section is skipped entirely). | ||
| */ | ||
| function formatProjectHealthTable(release: OrgReleaseResponse): string { | ||
| const projects = release.projects?.filter((p) => p.healthData?.hasHealthData); | ||
| if (!projects?.length) { | ||
| return ""; | ||
| } | ||
|
|
||
| const lines: string[] = []; | ||
| lines.push("### Health by Project"); | ||
| lines.push(""); | ||
|
|
||
| // Table header: right-align numeric columns with trailing ":" | ||
| lines.push( | ||
| mdTableHeader([ | ||
| "PROJECT", | ||
| "ADOPTION:", | ||
| "CRASH-FREE USERS:", | ||
| "CRASH-FREE SESSIONS:", | ||
| "USERS (24h):", | ||
| "SESSIONS (24h):", | ||
| ]) | ||
| ); | ||
|
|
||
| for (const project of projects) { | ||
| const h = project.healthData; | ||
| const cells = [ | ||
| escapeMarkdownCell(project.slug), | ||
| fmtPct(h?.adoption), | ||
| fmtCrashFree(h?.crashFreeUsers), | ||
| fmtCrashFree(h?.crashFreeSessions), | ||
| fmtCount(h?.totalUsers24h), | ||
| fmtCount(h?.totalSessions24h), | ||
| ]; | ||
| lines.push(`| ${cells.join(" | ")} |`); | ||
| } | ||
|
|
||
| return lines.join("\n"); | ||
| } | ||
|
|
||
| function formatReleaseDetails(release: OrgReleaseResponse): string { | ||
| const lines: string[] = []; | ||
|
|
||
|
|
@@ -77,14 +140,23 @@ function formatReleaseDetails(release: OrgReleaseResponse): string { | |
| } | ||
|
|
||
| lines.push(mdKvTable(kvRows)); | ||
|
|
||
| // Per-project health breakdown (only if any project has data) | ||
| const healthTable = formatProjectHealthTable(release); | ||
| if (healthTable) { | ||
| lines.push(""); | ||
| lines.push(healthTable); | ||
| } | ||
|
|
||
| return renderMarkdown(lines.join("\n")); | ||
| } | ||
|
|
||
| export const viewCommand = buildCommand({ | ||
| docs: { | ||
| brief: "View release details", | ||
| brief: "View release details with health metrics", | ||
| fullDescription: | ||
| "Show detailed information about a Sentry release.\n\n" + | ||
| "Show detailed information about a Sentry release, including\n" + | ||
| "per-project adoption and crash-free metrics.\n\n" + | ||
| "Examples:\n" + | ||
| " sentry release view 1.0.0\n" + | ||
| " sentry release view my-org/1.0.0\n" + | ||
|
|
@@ -140,7 +212,10 @@ export const viewCommand = buildCommand({ | |
| "sentry release view [<org>/]<version>" | ||
| ); | ||
| } | ||
| const release = await getRelease(resolved.org, version); | ||
| const release = await getRelease(resolved.org, version, { | ||
| health: true, | ||
| adoptionStages: true, | ||
| }); | ||
| yield new CommandOutput(release); | ||
| const hint = resolved.detectedFrom | ||
| ? `Detected from ${resolved.detectedFrom}` | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent missing-value styling in list table columns
Low Severity
The CRASH-FREE column uses
fmtCrashFreewhich returns a plain"—"for missing values, while the ADOPTION and CRASHES columns in the same table usecolorTag("muted", "—"). This causes mismatched styling within a single row — some dashes appear dimmed and others appear at full brightness.fmtCrashFreewas designed forview.tsmarkdown tables where plain text is fine, but in the list table context it breaks the visual consistency established by the other columns.Additional Locations (1)
src/commands/release/view.ts#L33-L36Reviewed by Cursor Bugbot for commit 71591f1. Configure here.