88import type { SentryContext } from "../../context.js" ;
99import { buildOrgAwareAliases } from "../../lib/alias.js" ;
1010import {
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" ;
2830import { 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" ;
3037import {
3138 divider ,
3239 type FormatShortIdOptions ,
@@ -76,6 +83,12 @@ const VALID_SORT_VALUES: SortValue[] = ["date", "new", "freq", "user"];
7683/** Usage hint for ContextError messages */
7784const 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+
7992function 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}. */
410459type 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