Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/src/content/docs/commands/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Work with Sentry releases

### `sentry release list <org/project>`

List releases
List releases with adoption and health metrics

**Arguments:**

Expand All @@ -27,7 +27,7 @@ List releases

### `sentry release view <org/version...>`

View release details
View release details with health metrics

**Arguments:**

Expand Down
4 changes: 2 additions & 2 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,8 @@ Manage Sentry dashboards

Work with Sentry releases

- `sentry release list <org/project>` — List releases
- `sentry release view <org/version...>` — View release details
- `sentry release list <org/project>` — List releases with adoption and health metrics
- `sentry release view <org/version...>` — View release details with health metrics
- `sentry release create <org/version...>` — Create a release
- `sentry release finalize <org/version...>` — Finalize a release
- `sentry release delete <org/version...>` — Delete a release
Expand Down
4 changes: 2 additions & 2 deletions plugins/sentry-cli/skills/sentry-cli/references/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Work with Sentry releases

### `sentry release list <org/project>`

List releases
List releases with adoption and health metrics

**Flags:**
- `-n, --limit <value> - Maximum number of releases to list - (default: "25")`
Expand All @@ -22,7 +22,7 @@ List releases

### `sentry release view <org/version...>`

View release details
View release details with health metrics

**Flags:**
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
Expand Down
32 changes: 32 additions & 0 deletions src/commands/release/format.ts
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");
}
46 changes: 36 additions & 10 deletions src/commands/release/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -14,28 +15,46 @@ import {
type OrgListCommandDocs,
} from "../../lib/list-command.js";
import type { OrgListConfig } from "../../lib/org-list.js";
import { fmtPct } from "./format.js";

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;
}

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",
Copy link
Copy Markdown
Contributor

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 fmtCrashFree which returns a plain "—" for missing values, while the ADOPTION and CRASHES columns in the same table use colorTag("muted", "—"). This causes mismatched styling within a single row — some dashes appear dimmed and others appear at full brightness. fmtCrashFree was designed for view.ts markdown 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)
Fix in Cursor Fix in Web

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) },
Expand All @@ -48,20 +67,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" +
Expand Down
83 changes: 79 additions & 4 deletions src/commands/release/view.ts
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";
Expand All @@ -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";
Expand All @@ -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);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing muted styling for null crash-free values in list

Low Severity

In the per-project health table, fmtCrashFree returns a plain em-dash ("—") for null/undefined values. This creates a visual inconsistency, as adjacent columns (e.g., ADOPTION, USERS) render their missing data placeholders as a muted em-dash. The CRASH-FREE column's dash appears unstyled, contrasting with the muted dashes in neighboring columns.

Fix in Cursor Fix in Web

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[] = [];

Expand Down Expand Up @@ -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" +
Expand Down Expand Up @@ -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}`
Expand Down
1 change: 1 addition & 0 deletions src/lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export {
getRelease,
listReleaseDeploys,
listReleasesPaginated,
type ReleaseSortValue,
setCommitsAuto,
setCommitsLocal,
setCommitsWithRefs,
Expand Down
Loading
Loading