Skip to content

Commit 027bf80

Browse files
committed
fix(issue-list): propagate ApiError instead of plain Error on fetch failures
When all project fetches fail, re-throw the original ApiError (with status, detail, endpoint) instead of a plain Error. This lets the telemetry layer in PR #251 correctly classify 4xx errors as client errors and suppress them from Sentry exceptions. Also: - Classify more HTTP status codes (404, 429, other 4xx) instead of lumping everything into "unknown" - Include partial failure info in JSON output when some projects fail - Warn on stderr about partial failures in human output - Add tests for error propagation and partial failure handling
1 parent 618671d commit 027bf80

File tree

2 files changed

+390
-39
lines changed

2 files changed

+390
-39
lines changed

src/commands/issue/list.ts

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ const VALID_SORT_VALUES: SortValue[] = ["date", "new", "freq", "user"];
5252
/** Usage hint for ContextError messages */
5353
const USAGE_HINT = "sentry issue list <org>/<project>";
5454

55-
/** Error type classification for fetch failures */
56-
type FetchErrorType = "permission" | "network" | "unknown";
57-
5855
function parseSort(value: string): SortValue {
5956
if (!VALID_SORT_VALUES.includes(value as SortValue)) {
6057
throw new Error(
@@ -241,7 +238,7 @@ function getComparator(
241238

242239
type 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 */
247244
type 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
*/
355352
async 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

Comments
 (0)