@@ -52,9 +52,6 @@ const VALID_SORT_VALUES: SortValue[] = ["date", "new", "freq", "user"];
5252/** Usage hint for ContextError messages */
5353const USAGE_HINT = "sentry issue list <org>/<project>" ;
5454
55- /** Error type classification for fetch failures */
56- type FetchErrorType = "permission" | "network" | "unknown" ;
57-
5855function parseSort ( value : string ) : SortValue {
5956 if ( ! VALID_SORT_VALUES . includes ( value as SortValue ) ) {
6057 throw new Error (
@@ -241,7 +238,7 @@ function getComparator(
241238
242239type FetchResult =
243240 | { success : true ; data : IssueListResult }
244- | { success : false ; errorType : FetchErrorType } ;
241+ | { success : false ; error : Error } ;
245242
246243/** Result of resolving targets from parsed argument */
247244type TargetResolutionResult = {
@@ -349,7 +346,7 @@ async function resolveTargetsFromParsedArg(
349346 *
350347 * @param target - Resolved org/project target
351348 * @param options - Query options (query, limit, sort)
352- * @returns Success with issues, or failure with error type classification
349+ * @returns Success with issues, or failure with the original error preserved
353350 * @throws {AuthError } When user is not authenticated
354351 */
355352async function fetchIssuesForTarget (
@@ -364,19 +361,11 @@ async function fetchIssuesForTarget(
364361 if ( error instanceof AuthError ) {
365362 throw error ;
366363 }
367- // Classify error type for better user messaging
368- // 401/403 are permission errors
369- if (
370- error instanceof ApiError &&
371- ( error . status === 401 || error . status === 403 )
372- ) {
373- return { success : false , errorType : "permission" } ;
374- }
375- // Network errors (fetch failures, timeouts)
376- if ( error instanceof TypeError && error . message . includes ( "fetch" ) ) {
377- return { success : false , errorType : "network" } ;
378- }
379- return { success : false , errorType : "unknown" } ;
364+
365+ return {
366+ success : false ,
367+ error : error instanceof Error ? error : new Error ( String ( error ) ) ,
368+ } ;
380369 }
381370}
382371
@@ -438,7 +427,7 @@ export const listCommand = buildCommand({
438427 flags : ListFlags ,
439428 target ?: string
440429 ) : Promise < void > {
441- const { stdout, cwd, setContext } = this ;
430+ const { stdout, stderr , cwd, setContext } = this ;
442431
443432 // Parse positional argument to determine resolution strategy
444433 const parsed = parseOrgProjectArg ( target ) ;
@@ -477,34 +466,36 @@ export const listCommand = buildCommand({
477466
478467 // Separate successful fetches from failures
479468 const validResults : IssueListResult [ ] = [ ] ;
480- const errorTypes = new Set < FetchErrorType > ( ) ;
469+ const failures : Error [ ] = [ ] ;
481470
482471 for ( const result of results ) {
483472 if ( result . success ) {
484473 validResults . push ( result . data ) ;
485474 } else {
486- errorTypes . add ( result . errorType ) ;
475+ failures . push ( result . error ) ;
487476 }
488477 }
489478
490- if ( validResults . length === 0 ) {
491- // Build error message based on what types of errors we saw
492- if ( errorTypes . has ( "permission" ) ) {
493- throw new Error (
494- `Failed to fetch issues from ${ targets . length } project(s).\n` +
495- "You don't have permission to access these projects.\n\n" +
496- "Try running 'sentry auth status' to verify your authentication."
479+ if ( validResults . length === 0 && failures . length > 0 ) {
480+ // Re-throw the first underlying error so telemetry can classify it
481+ // correctly (e.g., ApiError → isClientApiError → suppressed from exceptions).
482+ // Add context about how many projects failed.
483+ // biome-ignore lint/style/noNonNullAssertion: guarded by failures.length > 0
484+ const first = failures [ 0 ] ! ;
485+ const prefix = `Failed to fetch issues from ${ targets . length } project(s)` ;
486+
487+ // For ApiError, propagate the original so telemetry sees the status code
488+ if ( first instanceof ApiError ) {
489+ throw new ApiError (
490+ `${ prefix } : ${ first . message } ` ,
491+ first . status ,
492+ first . detail ,
493+ first . endpoint
497494 ) ;
498495 }
499- if ( errorTypes . has ( "network" ) ) {
500- throw new Error (
501- `Failed to fetch issues from ${ targets . length } project(s).\n` +
502- "Network connection failed. Check your internet connection."
503- ) ;
504- }
505- throw new Error (
506- `Failed to fetch issues from ${ targets . length } project(s).`
507- ) ;
496+
497+ // For other errors, add context to the message
498+ throw new Error ( `${ prefix } .\n${ first . message } ` ) ;
508499 }
509500
510501 // Determine display mode
@@ -539,13 +530,33 @@ export const listCommand = buildCommand({
539530 getComparator ( flags . sort ) ( a . issue , b . issue )
540531 ) ;
541532
542- // JSON output
533+ // JSON output — include partial failure info when some projects failed
543534 if ( flags . json ) {
544535 const allIssues = issuesWithOptions . map ( ( i ) => i . issue ) ;
545- writeJson ( stdout , allIssues ) ;
536+ if ( failures . length > 0 ) {
537+ writeJson ( stdout , {
538+ issues : allIssues ,
539+ errors : failures . map ( ( e ) =>
540+ e instanceof ApiError
541+ ? { status : e . status , message : e . message }
542+ : { message : e . message }
543+ ) ,
544+ } ) ;
545+ } else {
546+ writeJson ( stdout , allIssues ) ;
547+ }
546548 return ;
547549 }
548550
551+ // Warn on stderr about partial failures (human output only)
552+ if ( failures . length > 0 ) {
553+ stderr . write (
554+ muted (
555+ `\nNote: Failed to fetch issues from ${ failures . length } project(s). Showing results from ${ validResults . length } project(s).\n`
556+ )
557+ ) ;
558+ }
559+
549560 if ( issuesWithOptions . length === 0 ) {
550561 stdout . write ( "No issues found.\n" ) ;
551562 if ( footer ) {
0 commit comments