Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
53 changes: 43 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 @@ -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",
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 +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" +
Expand Down
98 changes: 94 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 @@ -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)}%`;
}

/** 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.


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

Expand Down Expand Up @@ -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" +
Expand Down Expand Up @@ -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}`
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
48 changes: 45 additions & 3 deletions src/lib/api/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ import { listRepositoriesPaginated } from "./repositories.js";
* List releases in an organization with pagination control.
* Returns a single page of results with cursor metadata.
*
* When `health` is true, each release's `projects[].healthData` is populated
* with adoption percentages, crash-free rates, and session/user counts.
*
* @param orgSlug - Organization slug
* @param options - Pagination, query, and sort options
* @param options - Pagination, query, sort, and health options
* @returns Single page of releases with cursor metadata
*/
export async function listReleasesPaginated(
Expand All @@ -49,19 +52,22 @@ export async function listReleasesPaginated(
perPage?: number;
query?: string;
sort?: string;
/** Include per-project health/adoption data in the response. */
health?: boolean;
} = {}
): Promise<PaginatedResponse<OrgReleaseResponse[]>> {
const config = await getOrgSdkConfig(orgSlug);

const result = await listAnOrganization_sReleases({
...config,
path: { organization_id_or_slug: orgSlug },
// per_page and sort are supported at runtime but not in the OpenAPI spec
// per_page, sort, and health are supported at runtime but not in the OpenAPI spec
query: {
cursor: options.cursor,
per_page: options.perPage ?? 25,
query: options.query,
sort: options.sort,
health: options.health ? 1 : undefined,
} as { cursor?: string },
});

Expand All @@ -73,17 +79,38 @@ export async function listReleasesPaginated(
);
}

/** Sort options for the release list endpoint. */
export type ReleaseSortValue =
| "date"
| "sessions"
| "users"
| "crash_free_sessions"
| "crash_free_users";

/**
* Get a single release by version.
* Version is URL-encoded by the SDK.
*
* When `health` is true, each project in the response includes a
* `healthData` object with adoption percentages, crash-free rates,
* and session/user counts for the requested period.
*
* @param orgSlug - Organization slug
* @param version - Release version string (e.g., "1.0.0", "sentry-cli@0.24.0")
* @param options - Optional health and adoption query parameters
* @returns Full release detail
*/
export async function getRelease(
orgSlug: string,
version: string
version: string,
options?: {
/** Include per-project health/adoption data. */
health?: boolean;
/** Include adoption stage info (e.g., "adopted", "low_adoption"). */
adoptionStages?: boolean;
/** Period for health stats: "24h", "7d", "14d", etc. Defaults to "24h". */
healthStatsPeriod?: string;
}
): Promise<OrgReleaseResponse> {
const config = await getOrgSdkConfig(orgSlug);

Expand All @@ -93,6 +120,21 @@ export async function getRelease(
organization_id_or_slug: orgSlug,
version,
},
query: {
health: options?.health,
adoptionStages: options?.adoptionStages,
healthStatsPeriod: options?.healthStatsPeriod as
| "24h"
| "7d"
| "14d"
| "30d"
| "1h"
| "1d"
| "2d"
| "48h"
| "90d"
| undefined,
},
});

const data = unwrapResult(result, `Failed to get release '${version}'`);
Expand Down
Loading
Loading