Skip to content

Commit 3ec9dd9

Browse files
fix(issue-list): auto-paginate --limit beyond 100 (#274)
Fixes #268. ## Problem `sentry issue list --limit N` where N > 100 silently returned only 100 results. The Sentry API server-side caps `per_page`/`limit` at 100 with no error or warning, so users requesting `--limit 500` always got 100. ## Changes **`src/lib/api-client.ts`** - Add `listIssuesAllPages()`: auto-paginates through the issues endpoint using cursor-based pagination up to the requested limit. Uses `Math.min(limit, 100)` as page size, bounded by `MAX_PAGINATION_PAGES` (50 pages). **`src/commands/issue/list.ts`** - Switch `fetchIssuesForTarget` from `listIssues` to `listIssuesAllPages` — transparently handles multi-page fetching for non-org-all modes. - Add explicit `--limit` validation: cap at 1000 for non-org-all (with error pointing to `<org>/` + `--cursor` for larger sets), cap at 100 for org-all (where `--limit` is page size). - Raise default `--limit` from 10 → 25. - Update help text to document auto-pagination semantics. ## Behaviour | Invocation | Before | After | |---|---|---| | `--limit 500` | silently returns 100 | fetches 5 pages, returns 500 | | `--limit 5000` | silently returns 100 | `ValidationError`: cannot exceed 1000 | | `--limit 200` in `<org>/` mode | API caps to 100 | `ValidationError`: cannot exceed 100 | --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 1ff4115 commit 3ec9dd9

File tree

3 files changed

+170
-66
lines changed

3 files changed

+170
-66
lines changed

plugins/sentry-cli/skills/sentry-cli/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ List issues in a project
201201

202202
**Flags:**
203203
- `-q, --query <value> - Search query (Sentry search syntax)`
204-
- `-n, --limit <value> - Maximum number of issues to list - (default: "10")`
204+
- `-n, --limit <value> - Maximum number of issues to list - (default: "25")`
205205
- `-s, --sort <value> - Sort by: date, new, freq, user - (default: "date")`
206206
- `--json - Output JSON`
207207
- `-c, --cursor <value> - Pagination cursor — only for <org>/ mode (use "last" to continue)`
@@ -591,7 +591,7 @@ List issues in a project
591591

592592
**Flags:**
593593
- `-q, --query <value> - Search query (Sentry search syntax)`
594-
- `-n, --limit <value> - Maximum number of issues to list - (default: "10")`
594+
- `-n, --limit <value> - Maximum number of issues to list - (default: "25")`
595595
- `-s, --sort <value> - Sort by: date, new, freq, user - (default: "date")`
596596
- `--json - Output JSON`
597597
- `-c, --cursor <value> - Pagination cursor — only for <org>/ mode (use "last" to continue)`

src/commands/issue/list.ts

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
import type { SentryContext } from "../../context.js";
99
import { buildOrgAwareAliases } from "../../lib/alias.js";
1010
import {
11+
API_MAX_PER_PAGE,
1112
findProjectsBySlug,
12-
listIssues,
13+
type IssuesPage,
14+
listIssuesAllPages,
1315
listIssuesPaginated,
1416
listProjects,
1517
} from "../../lib/api-client.js";
@@ -26,7 +28,12 @@ import {
2628
setProjectAliases,
2729
} from "../../lib/db/project-aliases.js";
2830
import { createDsnFingerprint } from "../../lib/dsn/index.js";
29-
import { ApiError, AuthError, ContextError } from "../../lib/errors.js";
31+
import {
32+
ApiError,
33+
AuthError,
34+
ContextError,
35+
ValidationError,
36+
} from "../../lib/errors.js";
3037
import {
3138
divider,
3239
type FormatShortIdOptions,
@@ -76,6 +83,12 @@ const VALID_SORT_VALUES: SortValue[] = ["date", "new", "freq", "user"];
7683
/** Usage hint for ContextError messages */
7784
const USAGE_HINT = "sentry issue list <org>/<project>";
7885

86+
/**
87+
* Maximum --limit value (user-facing ceiling for practical CLI response times).
88+
* Auto-pagination can theoretically fetch more, but 1000 keeps responses reasonable.
89+
*/
90+
const MAX_LIMIT = 1000;
91+
7992
function parseSort(value: string): SortValue {
8093
if (!VALID_SORT_VALUES.includes(value as SortValue)) {
8194
throw new Error(
@@ -378,7 +391,11 @@ async function fetchIssuesForTarget(
378391
options: { query?: string; limit: number; sort: SortValue }
379392
): Promise<FetchResult> {
380393
try {
381-
const issues = await listIssues(target.org, target.project, options);
394+
const { issues } = await listIssuesAllPages(
395+
target.org,
396+
target.project,
397+
options
398+
);
382399
return { success: true, data: { target, issues } };
383400
} catch (error) {
384401
// Auth errors should propagate - user needs to authenticate
@@ -406,6 +423,38 @@ function nextPageHint(org: string, flags: ListFlags): string {
406423
return parts.length > 0 ? `${base} ${parts.join(" ")}` : base;
407424
}
408425

426+
/**
427+
* Fetch org-wide issues, auto-paginating from the start or resuming from a cursor.
428+
*
429+
* When `cursor` is provided (--cursor resume), fetches a single page to keep the
430+
* cursor chain intact. Otherwise auto-paginates up to the requested limit.
431+
*/
432+
async function fetchOrgAllIssues(
433+
org: string,
434+
flags: Pick<ListFlags, "query" | "limit" | "sort">,
435+
cursor: string | undefined
436+
): Promise<IssuesPage> {
437+
// When resuming with --cursor, fetch a single page so the cursor chain stays intact.
438+
if (cursor) {
439+
const perPage = Math.min(flags.limit, API_MAX_PER_PAGE);
440+
const response = await listIssuesPaginated(org, "", {
441+
query: flags.query,
442+
cursor,
443+
perPage,
444+
sort: flags.sort,
445+
});
446+
return { issues: response.data, nextCursor: response.nextCursor };
447+
}
448+
449+
// No cursor — auto-paginate from the beginning via the shared helper.
450+
const { issues, nextCursor } = await listIssuesAllPages(org, "", {
451+
query: flags.query,
452+
limit: flags.limit,
453+
sort: flags.sort,
454+
});
455+
return { issues, nextCursor };
456+
}
457+
409458
/** Options for {@link handleOrgAllIssues}. */
410459
type OrgAllIssuesOptions = {
411460
stdout: Writer;
@@ -431,30 +480,25 @@ async function handleOrgAllIssues(options: OrgAllIssuesOptions): Promise<void> {
431480

432481
setContext([org], []);
433482

434-
const response = await listIssuesPaginated(org, "", {
435-
query: flags.query,
436-
cursor,
437-
perPage: flags.limit,
438-
sort: flags.sort,
439-
});
483+
const { issues, nextCursor } = await fetchOrgAllIssues(org, flags, cursor);
440484

441-
if (response.nextCursor) {
442-
setPaginationCursor(PAGINATION_KEY, contextKey, response.nextCursor);
485+
if (nextCursor) {
486+
setPaginationCursor(PAGINATION_KEY, contextKey, nextCursor);
443487
} else {
444488
clearPaginationCursor(PAGINATION_KEY, contextKey);
445489
}
446490

447-
const hasMore = !!response.nextCursor;
491+
const hasMore = !!nextCursor;
448492

449493
if (flags.json) {
450494
const output = hasMore
451-
? { data: response.data, nextCursor: response.nextCursor, hasMore: true }
452-
: { data: response.data, hasMore: false };
495+
? { data: issues, nextCursor, hasMore: true }
496+
: { data: issues, hasMore: false };
453497
writeJson(stdout, output);
454498
return;
455499
}
456500

457-
if (response.data.length === 0) {
501+
if (issues.length === 0) {
458502
if (hasMore) {
459503
stdout.write(
460504
`No issues on this page. Try the next page: ${nextPageHint(org, flags)}\n`
@@ -469,7 +513,7 @@ async function handleOrgAllIssues(options: OrgAllIssuesOptions): Promise<void> {
469513
// column is needed to identify which project each issue belongs to.
470514
writeListHeader(stdout, `Issues in ${org}`, true);
471515
const termWidth = process.stdout.columns || 80;
472-
const issuesWithOpts = response.data.map((issue) => ({
516+
const issuesWithOpts = issues.map((issue) => ({
473517
issue,
474518
formatOptions: {
475519
projectSlug: issue.project?.slug ?? "",
@@ -479,10 +523,10 @@ async function handleOrgAllIssues(options: OrgAllIssuesOptions): Promise<void> {
479523
writeIssueRows(stdout, issuesWithOpts, termWidth);
480524

481525
if (hasMore) {
482-
stdout.write(`\nShowing ${response.data.length} issues (more available)\n`);
526+
stdout.write(`\nShowing ${issues.length} issues (more available)\n`);
483527
stdout.write(`Next page: ${nextPageHint(org, flags)}\n`);
484528
} else {
485-
stdout.write(`\nShowing ${response.data.length} issues\n`);
529+
stdout.write(`\nShowing ${issues.length} issues\n`);
486530
}
487531
}
488532

@@ -664,7 +708,11 @@ export const listCommand = buildCommand({
664708
" sentry issue list <org>/ # all projects in org (trailing / required)\n" +
665709
" sentry issue list <project> # find project across all orgs\n\n" +
666710
`${targetPatternExplanation()}\n\n` +
667-
"In monorepos with multiple Sentry projects, shows issues from all detected projects.",
711+
"In monorepos with multiple Sentry projects, shows issues from all detected projects.\n\n" +
712+
"The --limit flag specifies the number of results to fetch per project (max 1000). " +
713+
"When the limit exceeds the API page size (100), multiple requests are made " +
714+
"automatically. Use --cursor to paginate through larger result sets. " +
715+
"Note: --cursor resumes from a single page to keep the cursor chain intact.",
668716
},
669717
parameters: {
670718
positional: LIST_TARGET_POSITIONAL,
@@ -675,7 +723,7 @@ export const listCommand = buildCommand({
675723
brief: "Search query (Sentry search syntax)",
676724
optional: true,
677725
},
678-
limit: buildListLimitFlag("issues", "10"),
726+
limit: buildListLimitFlag("issues", "25"),
679727
sort: {
680728
kind: "parsed",
681729
parse: parseSort,
@@ -703,6 +751,20 @@ export const listCommand = buildCommand({
703751

704752
const parsed = parseOrgProjectArg(target);
705753

754+
// Validate --limit range. Auto-pagination handles the API's 100-per-page
755+
// cap transparently, but we cap the total at MAX_LIMIT for practical CLI
756+
// response times. Use --cursor for paginating through larger result sets.
757+
if (flags.limit < 1) {
758+
throw new ValidationError("--limit must be at least 1.", "limit");
759+
}
760+
if (flags.limit > MAX_LIMIT) {
761+
throw new ValidationError(
762+
`--limit cannot exceed ${MAX_LIMIT}. ` +
763+
"Use --cursor to paginate through larger result sets.",
764+
"limit"
765+
);
766+
}
767+
706768
// biome-ignore lint/suspicious/noExplicitAny: shared handler accepts any mode variant
707769
const resolveAndHandle: ModeHandler<any> = (ctx) =>
708770
handleResolvedTargets({ ...ctx, flags, stderr, setContext });

0 commit comments

Comments
 (0)