Skip to content

Commit 5472658

Browse files
committed
refactor(formatters): extract shared number formatters, add sort aliases and session sparklines
- Extract compactFormatter, formatNumber, fmtPct, fmtCount, appendUnitSuffix, formatWithUnit, formatCompactWithUnit into src/lib/formatters/numbers.ts - Update dashboard.ts to import from shared module (no behavior change) - Delete src/commands/release/format.ts — replaced by shared module - Add sort aliases: stable_sessions/cfs → crash_free_sessions, stable_users/cfu → crash_free_users - Add SESSIONS sparkline column to release list table using health stats time-series data (same [timestamp, count] format as issue stats) - Drop COMMITS column from list to make room for sparkline
1 parent c3e442e commit 5472658

File tree

7 files changed

+204
-84
lines changed

7 files changed

+204
-84
lines changed

plugins/sentry-cli/skills/sentry-cli/references/release.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ List releases with adoption and health metrics
1717

1818
**Flags:**
1919
- `-n, --limit <value> - Maximum number of releases to list - (default: "25")`
20-
- `-s, --sort <value> - Sort order: date, sessions, users, crash_free_sessions, crash_free_users - (default: "date")`
20+
- `-s, --sort <value> - Sort: date, sessions, users, crash_free_sessions (cfs), crash_free_users (cfu) - (default: "date")`
2121
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
2222
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`
2323

src/commands/release/format.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

src/commands/release/list.ts

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import {
1313
} from "../../lib/api-client.js";
1414
import { parseOrgProjectArg } from "../../lib/arg-parsing.js";
1515
import { escapeMarkdownCell } from "../../lib/formatters/markdown.js";
16+
import { fmtPct } from "../../lib/formatters/numbers.js";
1617
import { CommandOutput } from "../../lib/formatters/output.js";
18+
import { sparkline } from "../../lib/formatters/sparkline.js";
1719
import { type Column, formatTable } from "../../lib/formatters/table.js";
1820
import { formatRelativeTime } from "../../lib/formatters/time-utils.js";
1921
import {
@@ -28,7 +30,6 @@ import {
2830
type ListResult,
2931
type OrgListConfig,
3032
} from "../../lib/org-list.js";
31-
import { fmtPct } from "./format.js";
3233

3334
export const PAGINATION_KEY = "release-list";
3435

@@ -43,20 +44,40 @@ const VALID_SORT_VALUES: ReleaseSortValue[] = [
4344
"crash_free_users",
4445
];
4546

47+
/**
48+
* Short aliases for sort values.
49+
*
50+
* Accepted alongside the canonical API values for convenience:
51+
* - `stable_sessions` / `cfs` → `crash_free_sessions`
52+
* - `stable_users` / `cfu` → `crash_free_users`
53+
*/
54+
const SORT_ALIASES: Record<string, ReleaseSortValue> = {
55+
stable_sessions: "crash_free_sessions",
56+
stable_users: "crash_free_users",
57+
cfs: "crash_free_sessions",
58+
cfu: "crash_free_users",
59+
};
60+
4661
const DEFAULT_SORT: ReleaseSortValue = "date";
4762

4863
/**
4964
* Parse and validate the `--sort` flag value.
5065
*
51-
* @throws Error when value is not one of the accepted sort keys
66+
* Accepts canonical API values and short aliases.
67+
* @throws Error when value is not recognized
5268
*/
5369
function parseSortFlag(value: string): ReleaseSortValue {
5470
if (VALID_SORT_VALUES.includes(value as ReleaseSortValue)) {
5571
return value as ReleaseSortValue;
5672
}
57-
throw new Error(
58-
`Invalid sort value. Must be one of: ${VALID_SORT_VALUES.join(", ")}`
73+
const alias = SORT_ALIASES[value];
74+
if (alias) {
75+
return alias;
76+
}
77+
const allAccepted = [...VALID_SORT_VALUES, ...Object.keys(SORT_ALIASES)].join(
78+
", "
5979
);
80+
throw new Error(`Invalid sort value. Must be one of: ${allAccepted}`);
6081
}
6182

6283
/**
@@ -69,6 +90,29 @@ function getHealthData(release: OrgReleaseResponse) {
6990
return release.projects?.find((p) => p.healthData?.hasHealthData)?.healthData;
7091
}
7192

93+
/**
94+
* Extract session time-series data points from health stats.
95+
*
96+
* The `stats` object follows the same `{ "<period>": [[ts, count], ...] }`
97+
* shape as issue stats. Takes the first available key.
98+
*/
99+
function extractSessionPoints(stats?: Record<string, unknown>): number[] {
100+
if (!stats) {
101+
return [];
102+
}
103+
const key = Object.keys(stats)[0];
104+
if (!key) {
105+
return [];
106+
}
107+
const buckets = stats[key];
108+
if (!Array.isArray(buckets)) {
109+
return [];
110+
}
111+
return buckets.map((b: unknown) =>
112+
Array.isArray(b) && b.length >= 2 ? Number(b[1]) || 0 : 0
113+
);
114+
}
115+
72116
const RELEASE_COLUMNS: Column<ReleaseWithOrg>[] = [
73117
{ header: "ORG", value: (r) => r.orgSlug || "" },
74118
{
@@ -89,12 +133,24 @@ const RELEASE_COLUMNS: Column<ReleaseWithOrg>[] = [
89133
value: (r) => fmtPct(getHealthData(r)?.crashFreeSessions),
90134
align: "right",
91135
},
136+
{
137+
header: "SESSIONS",
138+
value: (r) => {
139+
const health = getHealthData(r);
140+
if (!health) {
141+
return "";
142+
}
143+
const points = extractSessionPoints(
144+
health.stats as Record<string, unknown> | undefined
145+
);
146+
return points.length > 0 ? sparkline(points) : "";
147+
},
148+
},
92149
{
93150
header: "ISSUES",
94151
value: (r) => String(r.newGroups ?? 0),
95152
align: "right",
96153
},
97-
{ header: "COMMITS", value: (r) => String(r.commitCount ?? 0) },
98154
{ header: "DEPLOYS", value: (r) => String(r.deployCount ?? 0) },
99155
];
100156

@@ -168,8 +224,8 @@ export const listCommand = buildListCommand("release", {
168224
" date # by creation date (default)\n" +
169225
" sessions # by total sessions\n" +
170226
" users # by total users\n" +
171-
" crash_free_sessions # by crash-free session rate\n" +
172-
" crash_free_users # by crash-free user rate\n\n" +
227+
" crash_free_sessions # by crash-free session rate (aliases: stable_sessions, cfs)\n" +
228+
" crash_free_users # by crash-free user rate (aliases: stable_users, cfu)\n\n" +
173229
"Target specification:\n" +
174230
" sentry release list # auto-detect from DSN or config\n" +
175231
" sentry release list <org>/ # list all releases in org (paginated)\n" +
@@ -199,7 +255,7 @@ export const listCommand = buildListCommand("release", {
199255
kind: "parsed" as const,
200256
parse: parseSortFlag,
201257
brief:
202-
"Sort order: date, sessions, users, crash_free_sessions, crash_free_users",
258+
"Sort: date, sessions, users, crash_free_sessions (cfs), crash_free_users (cfu)",
203259
default: DEFAULT_SORT,
204260
},
205261
},

src/commands/release/view.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
renderMarkdown,
2020
safeCodeSpan,
2121
} from "../../lib/formatters/markdown.js";
22+
import { fmtCount, fmtPct } from "../../lib/formatters/numbers.js";
2223
import { CommandOutput } from "../../lib/formatters/output.js";
2324
import { formatRelativeTime } from "../../lib/formatters/time-utils.js";
2425
import {
@@ -27,7 +28,6 @@ import {
2728
FRESH_FLAG,
2829
} from "../../lib/list-command.js";
2930
import { resolveOrg } from "../../lib/resolve-target.js";
30-
import { fmtCount, fmtPct } from "./format.js";
3131
import { parseReleaseArg } from "./parse.js";
3232

3333
/** Format a crash-free rate with color coding (green ≥ 99, yellow ≥ 95, red < 95). */

src/lib/formatters/dashboard.ts

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -330,24 +330,15 @@ function calcGlyphWidth(formatted: string, glyphW: number): number {
330330
}
331331

332332
// ---------------------------------------------------------------------------
333-
// Number formatting
333+
// Number formatting — shared helpers live in numbers.ts, dashboard-only
334+
// helpers remain here.
334335
// ---------------------------------------------------------------------------
335336

336-
const compactFormatter = new Intl.NumberFormat("en-US", {
337-
notation: "compact",
338-
maximumFractionDigits: 1,
339-
});
340-
341-
const standardFormatter = new Intl.NumberFormat("en-US", {
342-
maximumFractionDigits: 2,
343-
});
344-
345-
function formatNumber(value: number): string {
346-
if (Math.abs(value) >= 1_000_000) {
347-
return compactFormatter.format(value);
348-
}
349-
return standardFormatter.format(value);
350-
}
337+
import {
338+
compactFormatter,
339+
formatCompactWithUnit,
340+
formatWithUnit,
341+
} from "./numbers.js";
351342

352343
/**
353344
* Format a value as a short Y-axis tick label (max ~4 chars).
@@ -376,32 +367,6 @@ function formatBigNumberValue(value: number): string {
376367
return Math.round(value).toString();
377368
}
378369

379-
/** Append unit suffix to a pre-formatted number string. */
380-
function appendUnitSuffix(formatted: string, unit?: string | null): string {
381-
if (!unit || unit === "none" || unit === "null") {
382-
return formatted;
383-
}
384-
if (unit === "millisecond") {
385-
return `${formatted}ms`;
386-
}
387-
if (unit === "second") {
388-
return `${formatted}s`;
389-
}
390-
if (unit === "byte") {
391-
return `${formatted}B`;
392-
}
393-
return `${formatted} ${unit}`;
394-
}
395-
396-
function formatWithUnit(value: number, unit?: string | null): string {
397-
return appendUnitSuffix(formatNumber(value), unit);
398-
}
399-
400-
/** Format a number with unit using compact notation (K/M/B). */
401-
function formatCompactWithUnit(value: number, unit?: string | null): string {
402-
return appendUnitSuffix(compactFormatter.format(Math.round(value)), unit);
403-
}
404-
405370
// ---------------------------------------------------------------------------
406371
// Sort helper: descending by value, "Other" always last
407372
// ---------------------------------------------------------------------------

src/lib/formatters/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from "./human.js";
1010
export * from "./json.js";
1111
export * from "./log.js";
1212
export * from "./markdown.js";
13+
export * from "./numbers.js";
1314
export * from "./output.js";
1415
export * from "./seer.js";
1516
export * from "./sparkline.js";

src/lib/formatters/numbers.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* Shared number formatting utilities.
3+
*
4+
* Provides compact notation (K/M/B), percentage formatting, and unit
5+
* suffixing used across dashboard, release, and other command formatters.
6+
*
7+
* Uses `Intl.NumberFormat` for locale-aware compact notation.
8+
*/
9+
10+
/**
11+
* Compact notation formatter: 52000 → "52K", 1.2M, etc.
12+
* One fractional digit maximum.
13+
*/
14+
export const compactFormatter = new Intl.NumberFormat("en-US", {
15+
notation: "compact",
16+
maximumFractionDigits: 1,
17+
});
18+
19+
/**
20+
* Standard notation formatter with thousands separators.
21+
* Two fractional digits maximum: 1234.5 → "1,234.5".
22+
*/
23+
export const standardFormatter = new Intl.NumberFormat("en-US", {
24+
maximumFractionDigits: 2,
25+
});
26+
27+
/**
28+
* Format a number with standard notation, switching to compact above 1M.
29+
*
30+
* - Below 1M: standard grouping (e.g., "52,000", "1,234.5")
31+
* - At or above 1M: compact (e.g., "1.2M", "52M")
32+
*
33+
* @example formatNumber(1234) // "1,234"
34+
* @example formatNumber(1500000) // "1.5M"
35+
*/
36+
export function formatNumber(value: number): string {
37+
if (Math.abs(value) >= 1_000_000) {
38+
return compactFormatter.format(value);
39+
}
40+
return standardFormatter.format(value);
41+
}
42+
43+
/**
44+
* Format a number in compact notation (always uses K/M/B suffixes).
45+
*
46+
* @example formatCompactCount(500) // "500"
47+
* @example formatCompactCount(52000) // "52K"
48+
* @example formatCompactCount(1200000) // "1.2M"
49+
*/
50+
export function formatCompactCount(value: number): string {
51+
return compactFormatter.format(value);
52+
}
53+
54+
/**
55+
* Append a unit suffix to a pre-formatted number string.
56+
*
57+
* Handles common Sentry unit names: "millisecond" → "ms",
58+
* "second" → "s", "byte" → "B". Unknown units are appended with a space.
59+
* Returns the number unchanged for "none"/"null"/empty units.
60+
*/
61+
export function appendUnitSuffix(
62+
formatted: string,
63+
unit?: string | null
64+
): string {
65+
if (!unit || unit === "none" || unit === "null") {
66+
return formatted;
67+
}
68+
if (unit === "millisecond") {
69+
return `${formatted}ms`;
70+
}
71+
if (unit === "second") {
72+
return `${formatted}s`;
73+
}
74+
if (unit === "byte") {
75+
return `${formatted}B`;
76+
}
77+
return `${formatted} ${unit}`;
78+
}
79+
80+
/**
81+
* Format a number with its unit, using standard/compact notation.
82+
*
83+
* @example formatWithUnit(1234, "millisecond") // "1,234ms"
84+
* @example formatWithUnit(1500000, "byte") // "1.5MB"
85+
*/
86+
export function formatWithUnit(value: number, unit?: string | null): string {
87+
return appendUnitSuffix(formatNumber(value), unit);
88+
}
89+
90+
/**
91+
* Format a number with its unit, always using compact notation.
92+
*
93+
* @example formatCompactWithUnit(52000, "byte") // "52KB"
94+
*/
95+
export function formatCompactWithUnit(
96+
value: number,
97+
unit?: string | null
98+
): string {
99+
return appendUnitSuffix(compactFormatter.format(Math.round(value)), unit);
100+
}
101+
102+
/**
103+
* Format a percentage value with one decimal place, or "—" when absent.
104+
*
105+
* @example fmtPct(42.3) // "42.3%"
106+
* @example fmtPct(null) // "—"
107+
*/
108+
export function fmtPct(value: number | null | undefined): string {
109+
if (value === null || value === undefined) {
110+
return "—";
111+
}
112+
return `${value.toFixed(1)}%`;
113+
}
114+
115+
/**
116+
* Format an integer count in compact notation, or "—" when absent.
117+
*
118+
* Values below 1000 are shown as-is. Above that, uses K/M/B suffixes.
119+
*
120+
* @example fmtCount(500) // "500"
121+
* @example fmtCount(52000) // "52K"
122+
* @example fmtCount(1200000) // "1.2M"
123+
* @example fmtCount(null) // "—"
124+
*/
125+
export function fmtCount(value: number | null | undefined): string {
126+
if (value === null || value === undefined) {
127+
return "—";
128+
}
129+
return compactFormatter.format(value);
130+
}

0 commit comments

Comments
 (0)