Skip to content

Commit 3c7d085

Browse files
betegoncursoragent
andauthored
fix(issue): stop collapsing stats on piped output and JSON defaults (#1221)
## Summary Fixes #1219. `sentry issue list` was sending `collapse=stats` whenever stdout was piped or `--json` was used. On the Sentry API that flag skips all seen-stats Snuba queries — not just sparklines — so piped tables showed `—`/`?`/`0` for SEEN/AGE/EVENTS/USERS and JSON omitted basic fields by default. Now stats are only collapsed when `--json --fields` explicitly omits count/timestamp/stats fields. Human output (including piped/agent usage) always requests seen-stats data; the TREND column is still hidden on narrow terminals as a display-only concern. ## Test plan - [x] `pnpm exec vitest run test/commands/issue/list.test.ts -t "collapse"` - [ ] `sentry issue list <org>/<project> -q "is:unresolved" | head` — SEEN/AGE/EVENTS/USERS populated - [ ] `sentry issue list <org>/<project> --json` — includes `count`, `userCount`, `firstSeen`, `lastSeen`, `stats` Fixes #1219 Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b40da6b commit 3c7d085

5 files changed

Lines changed: 156 additions & 205 deletions

File tree

‎.lore.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* **check:stale-refs: generic toolchain consistency scanner derived from package.json**: \`script/check-stale-references.ts\`: reads \`packageManager\` from \`package.json\` (e.g., \`pnpm@10.11.0\`), derives stale PMs dynamically, and scans dev-facing docs/scripts for stale \`\<pm> run\`, \`\<pm> remove\`, \`\<pm> add -d\` commands and \`requires \<pm>\`/\`\<pm> installed\` prerequisite prose. Excludes: user-facing install instructions (fenced code blocks with \`install -g\`/\`add -g\`), the check script itself, and \`node\_modules/\`. Added to CI lint job. \*\*Generic\*\*: if project migrates from pnpm to yarn, changing \`packageManager\` in \`package.json\` auto-flags all \`pnpm run\` references in dev docs — no manual pattern updates needed. Trap: script must exclude itself from scanning or its own JSDoc examples trigger false positives.
2727

2828
<!-- lore:019e6a6c-3c39-71fc-a5a1-fb56b9870039 -->
29-
* **collapse=lifetime in issue list: LIFETIME\_FIELDS, buildListApiOptions, and API gotcha**: \`src/commands/issue/list.ts\` \`LIFETIME\_FIELDS = new Set(\['count','userCount','firstSeen','lastSeen'])\` — fields stripped by \`collapse=lifetime\` on the list endpoint. \`buildListApiOptions(json, fields)\`: \`collapseLifetime\` only true when \`json && fields !== undefined && fields.length > 0 && !fields.some(f => LIFETIME\_FIELDS.has(f))\`. Human output NEVER collapses lifetime. \`buildIssueListCollapse()\` always starts with \`\['filtered','unhandled']\`, conditionally adds \`'lifetime'\` then \`'stats'\`. \`ISSUE\_DETAIL\_COLLAPSE\` safely includes \`'lifetime'\` — detail endpoint preserves top-level fields regardless. \`IssueViewOutputSchema\` in \`src/types/sentry.ts\` extends \`SentryIssueSchema\` with enrichment fields (\`event\`, \`org\`, \`replayIds\`, \`trace\`) added by \`jsonTransformIssueView\`. Wired via \`schema: IssueViewOutputSchema\` on output config in \`view.ts\`. NOTE: \`count\`/\`userCount\`/\`firstSeen\`/\`lastSeen\` always present on \`issue view\` (detail endpoint) — only potentially absent on \`issue list\` when collapse=lifetime is active.
29+
* **issue list collapse=stats/lifetime API gotcha: SEEN\_STATS\_FIELDS, LIFETIME\_FIELDS, buildListApiOptions**: On the Sentry list endpoint, \`collapse=stats\` skips \`_get_seen_stats()\` entirely — stripping top-level \`count\`, \`userCount\`, \`firstSeen\`, \`lastSeen\` and the sparkline \`stats\` object (not just TREND). \`src/commands/issue/list.ts\`: \`SEEN_STATS_FIELDS = new Set(\[...LIFETIME_FIELDS, 'stats'])\`; \`shouldCollapseForFields(fields, dependentFields)\` shared by stats/lifetime decisions; \`shouldCollapseStats(json, fields)\` never collapses in human mode; JSON only when \`--fields\` omits all seen-stats fields. \`buildListApiOptions(json, fields)\`: \`collapseLifetime = json && shouldCollapseForFields(fields, LIFETIME_FIELDS)\`. \`buildIssueListCollapse()\` always starts with \`\['filtered','unhandled']\`, conditionally adds \`'lifetime'\` then \`'stats'\`. \`willShowTrend()\` is display-only (hides TREND column on narrow/piped stdout). See #1219. NOTE: \`count\`/\`userCount\`/\`firstSeen\`/\`lastSeen\` always present on \`issue view\` (detail endpoint) — only potentially absent on \`issue list\` when collapse is active.
3030

3131
<!-- lore:019cbaa2-e4a2-76c0-8f64-917a97ae20c5 -->
3232
* **Consola chosen as CLI logger with Sentry createConsolaReporter integration**: Consola is the CLI logger with Sentry \`createConsolaReporter\` integration. Two reporters: FancyReporter (stderr) + Sentry structured logs. Level via \`SENTRY\_LOG\_LEVEL\`. \`buildCommand\` injects hidden \`--log-level\`/\`--verbose\` flags. \`withTag()\` creates independent instances; \`setLogLevel()\` propagates via registry. All user-facing output must use consola, not raw stderr. \`HandlerContext\` intentionally omits stderr. Telemetry opt-out priority: (1) \`SENTRY\_CLI\_NO\_TELEMETRY=1\`, (2) \`DO\_NOT\_TRACK=1\`, (3) \`metadata.defaults.telemetry\`, (4) default on. Shell completions set \`SENTRY\_CLI\_NO\_TELEMETRY=1\` in \`bin.ts\` before imports. Timing queued to \`completion\_telemetry\_queue\` SQLite table; normal runs drain via \`DELETE ... RETURNING\`. \`ENV\_VAR\_REGISTRY\` in \`src/lib/env-registry.ts\` is single source for all honored env vars; \`topLevel: true\` + \`briefDescription\` surfaces in \`--help\`. Add install-script-only vars with \`installOnly: true\`.

‎src/commands/issue/list.ts‎

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import {
4646
import {
4747
type IssueTableRow,
4848
shouldAutoCompact,
49-
willShowTrend,
5049
writeIssueTable,
5150
} from "../../lib/formatters/index.js";
5251
import {
@@ -189,27 +188,7 @@ type ListApiOptions = {
189188
};
190189

191190
/**
192-
* Determine whether stats data should be collapsed (skipped) in the API request.
193-
*
194-
* Stats power the TREND sparkline column, which is only shown when:
195-
* 1. Output is human (not `--json`) — JSON consumers don't render sparklines
196-
* 2. Terminal is wide enough — narrow terminals and non-TTY hide TREND
197-
*
198-
* Collapsing stats avoids expensive Snuba/ClickHouse aggregation queries,
199-
* saving 200-500ms per API request.
200-
*
201-
* @see {@link willShowTrend} for the terminal width threshold logic
202-
*/
203-
function shouldCollapseStats(json: boolean): boolean {
204-
if (json) {
205-
return true;
206-
}
207-
return !willShowTrend();
208-
}
209-
210-
/**
211-
* Fields that depend on the `lifetime` API data. When `collapse=lifetime`
212-
* is sent, the server omits these from the list response. See #969.
191+
* Fields stripped by `collapse=lifetime` on the list endpoint. See #969.
213192
*/
214193
const LIFETIME_FIELDS = new Set([
215194
"count",
@@ -218,29 +197,55 @@ const LIFETIME_FIELDS = new Set([
218197
"lastSeen",
219198
]);
220199

200+
/**
201+
* Fields populated by Snuba seen-stats queries on the list endpoint.
202+
*
203+
* On the Sentry API, `collapse=stats` skips `_get_seen_stats()` entirely,
204+
* stripping top-level count/timestamp fields and the sparkline `stats` object
205+
* — not just the TREND column data. See #1219.
206+
*/
207+
const SEEN_STATS_FIELDS = new Set([...LIFETIME_FIELDS, "stats"]);
208+
209+
/**
210+
* Whether collapse is safe for a `--fields` subset — true when explicit fields
211+
* were requested and none depend on the given API data.
212+
*/
213+
function shouldCollapseForFields(
214+
fields: string[] | undefined,
215+
dependentFields: ReadonlySet<string>
216+
): boolean {
217+
return (
218+
fields !== undefined &&
219+
fields.length > 0 &&
220+
!fields.some((f) => dependentFields.has(f))
221+
);
222+
}
223+
224+
/**
225+
* Determine whether stats data should be collapsed (skipped) in the API request.
226+
*
227+
* Collapsing stats saves ~200–500ms per Snuba query but also removes basic issue
228+
* metadata. Human output never collapses stats; JSON only opts out via `--fields`.
229+
*/
230+
function shouldCollapseStats(json: boolean, fields?: string[]): boolean {
231+
return json && shouldCollapseForFields(fields, SEEN_STATS_FIELDS);
232+
}
233+
221234
/**
222235
* Build the collapse and groupStatsPeriod options for issue list API calls.
223236
*
224237
* When stats are collapsed, groupStatsPeriod is omitted (undefined) since
225238
* the server won't compute stats anyway. This avoids wasted server-side
226239
* processing and makes the request intent explicit.
227240
*
228-
* Lifetime is only collapsed in JSON mode when explicit `--fields` are
229-
* provided and none of them are lifetime-dependent (`count`, `userCount`,
230-
* `firstSeen`, `lastSeen`). Human output always needs these for the
231-
* EVENTS, USERS, SEEN, and AGE columns.
241+
* Stats and lifetime are only collapsed in JSON mode with explicit `--fields`
242+
* that omit the corresponding dependent fields. Human output always requests
243+
* seen-stats data for the EVENTS, USERS, SEEN, and AGE columns.
232244
*/
233245
function buildListApiOptions(json: boolean, fields?: string[]): ListApiOptions {
234-
const collapseStats = shouldCollapseStats(json);
235-
// Collapse lifetime only when in JSON mode with explicit --fields that
236-
// don't include any lifetime-dependent field. Human output always needs
237-
// these (EVENTS, USERS, SEEN, AGE columns), and JSON without --fields
238-
// returns all fields.
246+
const collapseStats = shouldCollapseStats(json, fields);
239247
const collapseLifetime =
240-
json &&
241-
fields !== undefined &&
242-
fields.length > 0 &&
243-
!fields.some((f) => LIFETIME_FIELDS.has(f));
248+
json && shouldCollapseForFields(fields, LIFETIME_FIELDS);
244249
return {
245250
collapse: buildIssueListCollapse({
246251
shouldCollapseStats: collapseStats,

‎src/lib/api/issues.ts‎

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,22 @@ export type IssueCollapseField = NonNullable<
6060
* Build the `collapse` parameter for issue list API calls.
6161
*
6262
* Always collapses `filtered` and `unhandled` — the CLI never consumes
63-
* these in issue list views. Conditionally collapses `stats` when
64-
* sparklines won't be rendered (narrow terminal, non-TTY, or JSON),
65-
* and `lifetime` when the caller confirms the lifetime-dependent
66-
* top-level fields (`count`, `userCount`, `firstSeen`, `lastSeen`)
67-
* aren't needed.
63+
* these in issue list views. Conditionally collapses `stats` when the caller
64+
* confirms seen-stats fields aren't needed, and `lifetime` when the caller
65+
* confirms lifetime-dependent fields aren't needed.
66+
*
67+
* **Important:** `collapse=stats` skips Snuba seen-stats queries entirely,
68+
* stripping top-level `count`, `userCount`, `firstSeen`, `lastSeen` and the
69+
* sparkline `stats` object — not just the time-series. See #1219.
6870
*
6971
* **Important:** Despite being documented as removing only the `lifetime`
7072
* sub-object, `collapse=lifetime` also strips the top-level `count`,
7173
* `userCount`, `firstSeen`, and `lastSeen` fields from list responses.
7274
* Only collapse it when those fields are confirmed unnecessary. See #969.
7375
*
74-
* Matches the Sentry web UI's optimization: the initial page load sends
75-
* `collapse=stats,unhandled` to skip expensive Snuba queries, fetching
76-
* stats in a follow-up request only when needed.
77-
*
7876
* @param options - Context for determining what to collapse
7977
* @param options.shouldCollapseStats - Whether stats data can be skipped
80-
* (true when sparklines won't be shown: narrow terminal, non-TTY, --json)
78+
* (true only when `--json --fields` omits all seen-stats-dependent fields)
8179
* @param options.shouldCollapseLifetime - Whether lifetime data can be skipped.
8280
* Defaults to `false` because most output paths need `count`/`userCount`/
8381
* `firstSeen`/`lastSeen`. Only set to `true` when `--json --fields` omits

‎src/lib/formatters/human.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -375,9 +375,9 @@ export const TREND_MIN_TERM_WIDTH = 100;
375375
* Returns `true` when the terminal is wide enough (≥ {@link TREND_MIN_TERM_WIDTH}).
376376
* Non-TTY output defaults to 80 columns, which is below the threshold.
377377
*
378-
* Used by the issue list command to decide whether to request stats data
379-
* from the API — when TREND won't be shown, stats can be collapsed to
380-
* save 200-500ms per request.
378+
* Display-only — the issue list command always requests seen-stats data from
379+
* the API regardless of this value so SEEN/AGE/EVENTS/USERS stay populated
380+
* when output is piped. See #1219.
381381
*/
382382
export function willShowTrend(): boolean {
383383
const termWidth = process.stdout.columns || 80;

0 commit comments

Comments
 (0)