Skip to content

Commit 3166a37

Browse files
committed
fix(issue-list): auto-paginate --limit beyond 100 (#268)
When users passed --limit > 100, the Sentry API silently capped results at 100. The CLI now transparently fetches multiple pages to satisfy the requested limit. - Add listIssuesAllPages() in api-client.ts: cursor-paginates up to the requested limit using listIssuesPaginated, bounded by MAX_PAGINATION_PAGES - Switch fetchIssuesForTarget to use listIssuesAllPages instead of listIssues - Cap --limit at 1000 (non-org-all) and 100 (org-all page size) with a clear ValidationError pointing to cursor pagination for larger sets - Change default --limit from 10 to 25 - Update help text to document auto-pagination semantics
1 parent 5584a1d commit 3166a37

File tree

2 files changed

+104
-5
lines changed

2 files changed

+104
-5
lines changed

src/commands/issue/list.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { SentryContext } from "../../context.js";
99
import { buildOrgAwareAliases } from "../../lib/alias.js";
1010
import {
1111
findProjectsBySlug,
12-
listIssues,
12+
listIssuesAllPages,
1313
listIssuesPaginated,
1414
listProjects,
1515
} from "../../lib/api-client.js";
@@ -26,7 +26,12 @@ import {
2626
setProjectAliases,
2727
} from "../../lib/db/project-aliases.js";
2828
import { createDsnFingerprint } from "../../lib/dsn/index.js";
29-
import { ApiError, AuthError, ContextError } from "../../lib/errors.js";
29+
import {
30+
ApiError,
31+
AuthError,
32+
ContextError,
33+
ValidationError,
34+
} from "../../lib/errors.js";
3035
import {
3136
divider,
3237
type FormatShortIdOptions,
@@ -76,6 +81,15 @@ const VALID_SORT_VALUES: SortValue[] = ["date", "new", "freq", "user"];
7681
/** Usage hint for ContextError messages */
7782
const USAGE_HINT = "sentry issue list <org>/<project>";
7883

84+
/** Maximum --limit value for non-org-all modes (auto-pagination ceiling). */
85+
const MAX_LIMIT = 1000;
86+
87+
/**
88+
* Sentry API's max per_page for issues. In org-all mode --limit controls page
89+
* size, so it's capped here to avoid the API silently clamping it.
90+
*/
91+
const API_MAX_PER_PAGE = 100;
92+
7993
function parseSort(value: string): SortValue {
8094
if (!VALID_SORT_VALUES.includes(value as SortValue)) {
8195
throw new Error(
@@ -378,7 +392,11 @@ async function fetchIssuesForTarget(
378392
options: { query?: string; limit: number; sort: SortValue }
379393
): Promise<FetchResult> {
380394
try {
381-
const issues = await listIssues(target.org, target.project, options);
395+
const issues = await listIssuesAllPages(
396+
target.org,
397+
target.project,
398+
options
399+
);
382400
return { success: true, data: { target, issues } };
383401
} catch (error) {
384402
// Auth errors should propagate - user needs to authenticate
@@ -664,7 +682,11 @@ export const listCommand = buildCommand({
664682
" sentry issue list <org>/ # all projects in org (trailing / required)\n" +
665683
" sentry issue list <project> # find project across all orgs\n\n" +
666684
`${targetPatternExplanation()}\n\n` +
667-
"In monorepos with multiple Sentry projects, shows issues from all detected projects.",
685+
"In monorepos with multiple Sentry projects, shows issues from all detected projects.\n\n" +
686+
"The --limit flag specifies the total number of results to fetch (max 1000). " +
687+
"When the limit exceeds the API page size (100), multiple requests are made " +
688+
"automatically. In <org>/ mode, --limit controls the page size for cursor " +
689+
"pagination (max 100).",
668690
},
669691
parameters: {
670692
positional: LIST_TARGET_POSITIONAL,
@@ -675,7 +697,7 @@ export const listCommand = buildCommand({
675697
brief: "Search query (Sentry search syntax)",
676698
optional: true,
677699
},
678-
limit: buildListLimitFlag("issues", "10"),
700+
limit: buildListLimitFlag("issues", "25"),
679701
sort: {
680702
kind: "parsed",
681703
parse: parseSort,
@@ -703,6 +725,24 @@ export const listCommand = buildCommand({
703725

704726
const parsed = parseOrgProjectArg(target);
705727

728+
// Validate --limit range based on mode.
729+
// org-all: --limit is page size, cap at API max (100).
730+
// Others: --limit is total results, cap at MAX_LIMIT (1000).
731+
if (parsed.type === "org-all") {
732+
if (flags.limit > API_MAX_PER_PAGE) {
733+
throw new ValidationError(
734+
`--limit cannot exceed ${API_MAX_PER_PAGE} in <org>/ mode (page size for cursor pagination).`,
735+
"limit"
736+
);
737+
}
738+
} else if (flags.limit > MAX_LIMIT) {
739+
throw new ValidationError(
740+
`--limit cannot exceed ${MAX_LIMIT}. ` +
741+
"Use 'sentry issue list <org>/' with --cursor for larger result sets.",
742+
"limit"
743+
);
744+
}
745+
706746
// biome-ignore lint/suspicious/noExplicitAny: shared handler accepts any mode variant
707747
const resolveAndHandle: ModeHandler<any> = (ctx) =>
708748
handleResolvedTargets({ ...ctx, flags, stderr, setContext });

src/lib/api-client.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,65 @@ export function listIssuesPaginated(
10641064
);
10651065
}
10661066

1067+
/**
1068+
* Sentry API's maximum items per page for the issues endpoint.
1069+
* Requests above this are silently capped server-side.
1070+
*/
1071+
const ISSUES_MAX_PER_PAGE = 100;
1072+
1073+
/**
1074+
* Auto-paginate through issues up to the requested limit.
1075+
*
1076+
* The Sentry issues API caps `per_page` at 100 server-side. When the caller
1077+
* requests more than 100 issues, this function transparently fetches multiple
1078+
* pages using cursor-based pagination and returns the combined result.
1079+
*
1080+
* Safety-bounded by {@link MAX_PAGINATION_PAGES} to prevent runaway requests.
1081+
*
1082+
* @param orgSlug - Organization slug
1083+
* @param projectSlug - Project slug (empty string for org-wide)
1084+
* @param options - Query, sort, and limit options
1085+
* @returns Combined array of issues (up to `limit` items)
1086+
*/
1087+
export async function listIssuesAllPages(
1088+
orgSlug: string,
1089+
projectSlug: string,
1090+
options: {
1091+
query?: string;
1092+
limit: number;
1093+
sort?: "date" | "new" | "freq" | "user";
1094+
statsPeriod?: string;
1095+
}
1096+
): Promise<SentryIssue[]> {
1097+
const allResults: SentryIssue[] = [];
1098+
let cursor: string | undefined;
1099+
1100+
// Use the smaller of the requested limit and the API max as page size
1101+
const perPage = Math.min(options.limit, ISSUES_MAX_PER_PAGE);
1102+
1103+
for (let page = 0; page < MAX_PAGINATION_PAGES; page++) {
1104+
const response = await listIssuesPaginated(orgSlug, projectSlug, {
1105+
query: options.query,
1106+
cursor,
1107+
perPage,
1108+
sort: options.sort,
1109+
statsPeriod: options.statsPeriod,
1110+
});
1111+
1112+
allResults.push(...response.data);
1113+
1114+
// Stop if we've reached the requested limit or there are no more pages
1115+
if (allResults.length >= options.limit || !response.nextCursor) {
1116+
break;
1117+
}
1118+
1119+
cursor = response.nextCursor;
1120+
}
1121+
1122+
// Trim to exact limit (last page may overshoot)
1123+
return allResults.slice(0, options.limit);
1124+
}
1125+
10671126
/**
10681127
* Get a specific issue by numeric ID.
10691128
*/

0 commit comments

Comments
 (0)