-
-
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 2 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 |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| * sentry release list | ||
| * | ||
| * List releases in an organization with pagination support. | ||
| * Includes per-project health/adoption metrics when available. | ||
| */ | ||
|
|
||
| import type { OrgReleaseResponse } from "@sentry/api"; | ||
|
|
@@ -19,23 +20,48 @@ export const PAGINATION_KEY = "release-list"; | |
|
|
||
| type ReleaseWithOrg = OrgReleaseResponse & { orgSlug?: string }; | ||
|
|
||
| /** | ||
| * Extract health data from the first project that has it. | ||
| * | ||
| * A release spans multiple projects; each gets independent health data. | ||
| * For the list table we pick the first project with `hasHealthData: true`. | ||
| */ | ||
| function getHealthData(release: OrgReleaseResponse) { | ||
| return release.projects?.find((p) => p.healthData?.hasHealthData)?.healthData; | ||
| } | ||
|
|
||
| /** Format a percentage value with one decimal, or "—" when absent. */ | ||
| function fmtPct(value: number | null | undefined): string { | ||
| if (value === null || value === undefined) { | ||
| return "—"; | ||
| } | ||
| return `${value.toFixed(1)}%`; | ||
| } | ||
|
|
||
| const RELEASE_COLUMNS: Column<ReleaseWithOrg>[] = [ | ||
| { header: "ORG", value: (r) => r.orgSlug || "" }, | ||
| { | ||
| header: "VERSION", | ||
| value: (r) => escapeMarkdownCell(r.shortVersion || r.version), | ||
| }, | ||
| { | ||
| header: "STATUS", | ||
| value: (r) => (r.dateReleased ? "Finalized" : "Unreleased"), | ||
| }, | ||
| { | ||
| header: "CREATED", | ||
| value: (r) => (r.dateCreated ? formatRelativeTime(r.dateCreated) : ""), | ||
| }, | ||
| { | ||
| header: "RELEASED", | ||
| value: (r) => (r.dateReleased ? formatRelativeTime(r.dateReleased) : "—"), | ||
| header: "ADOPTION", | ||
| value: (r) => fmtPct(getHealthData(r)?.adoption), | ||
| align: "right", | ||
|
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. Inconsistent missing-value styling in list table columnsLow Severity The CRASH-FREE column uses Additional Locations (1)Reviewed by Cursor Bugbot for commit 71591f1. Configure here. |
||
| }, | ||
| { | ||
| header: "CRASH-FREE", | ||
| value: (r) => fmtPct(getHealthData(r)?.crashFreeSessions), | ||
| align: "right", | ||
| }, | ||
| { | ||
| header: "ISSUES", | ||
| value: (r) => String(r.newGroups ?? 0), | ||
| align: "right", | ||
| }, | ||
| { header: "COMMITS", value: (r) => String(r.commitCount ?? 0) }, | ||
| { header: "DEPLOYS", value: (r) => String(r.deployCount ?? 0) }, | ||
|
|
@@ -48,20 +74,27 @@ const releaseListConfig: OrgListConfig<OrgReleaseResponse, ReleaseWithOrg> = { | |
| commandPrefix: "sentry release list", | ||
| // listForOrg fetches a buffer page for multi-org fan-out. | ||
| // The framework truncates results to --limit after aggregation. | ||
| // health=true to populate per-project adoption/crash-free metrics. | ||
| listForOrg: async (org) => { | ||
| const { data } = await listReleasesPaginated(org, { perPage: 100 }); | ||
| const { data } = await listReleasesPaginated(org, { | ||
| perPage: 100, | ||
| health: true, | ||
| }); | ||
| return data; | ||
| }, | ||
| listPaginated: (org, opts) => listReleasesPaginated(org, opts), | ||
| listPaginated: (org, opts) => | ||
| listReleasesPaginated(org, { ...opts, health: true }), | ||
| withOrg: (release, orgSlug) => ({ ...release, orgSlug }), | ||
| displayTable: (releases: ReleaseWithOrg[]) => | ||
| formatTable(releases, RELEASE_COLUMNS), | ||
| }; | ||
|
|
||
| const docs: OrgListCommandDocs = { | ||
| brief: "List releases", | ||
| brief: "List releases with adoption and health metrics", | ||
| fullDescription: | ||
| "List releases in an organization.\n\n" + | ||
| "List releases in an organization with adoption and crash-free metrics.\n\n" + | ||
| "Health data (adoption %, crash-free session rate) is shown per-release\n" + | ||
| "from the first project that has session data.\n\n" + | ||
| "Target specification:\n" + | ||
| " sentry release list # auto-detect from DSN or config\n" + | ||
| " sentry release list <org>/ # list all releases in org (paginated)\n" + | ||
|
|
||
| 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"; | ||
|
|
@@ -26,6 +29,81 @@ import { | |
| import { resolveOrg } from "../../lib/resolve-target.js"; | ||
| import { parseReleaseArg } from "./parse.js"; | ||
|
|
||
| /** Format a percentage with one decimal, colorized by threshold. */ | ||
| function fmtPct(value: number | null | undefined): string { | ||
| if (value === null || value === undefined) { | ||
| return "—"; | ||
| } | ||
| return `${value.toFixed(1)}%`; | ||
| } | ||
BYK marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** 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. |
||
|
|
||
| /** Format a count with thousands separators, or "—" when absent. */ | ||
| function fmtCount(value: number | null | undefined): string { | ||
| if (value === null || value === undefined) { | ||
| return "—"; | ||
| } | ||
| return value.toLocaleString("en-US"); | ||
| } | ||
|
|
||
| /** | ||
| * 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 +155,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 +227,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}` | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.