Skip to content

Commit c796235

Browse files
committed
refactor: move handleSeerCommandError to shared utils
Move the error handling helper from plan.ts to utils.ts so both explain.ts and plan.ts use the same function. This ensures outcome classification stays consistent if the logic is updated.
1 parent dcb2862 commit c796235

File tree

3 files changed

+51
-64
lines changed

3 files changed

+51
-64
lines changed

src/commands/issue/explain.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,18 @@
66

77
import type { SentryContext } from "../../context.js";
88
import { buildCommand } from "../../lib/command.js";
9-
import { ApiError } from "../../lib/errors.js";
109
import { CommandOutput } from "../../lib/formatters/output.js";
11-
import {
12-
formatRootCauseList,
13-
handleSeerApiError,
14-
} from "../../lib/formatters/seer.js";
10+
import { formatRootCauseList } from "../../lib/formatters/seer.js";
1511
import {
1612
applyFreshFlag,
1713
FRESH_ALIASES,
1814
FRESH_FLAG,
1915
} from "../../lib/list-command.js";
20-
import { classifySeerError, recordSeerOutcome } from "../../lib/telemetry.js";
16+
import { recordSeerOutcome } from "../../lib/telemetry.js";
2117
import { extractRootCauses } from "../../types/seer.js";
2218
import {
2319
ensureRootCauseAnalysis,
20+
handleSeerCommandError,
2421
issueIdPositional,
2522
resolveOrgAndIssueId,
2623
} from "./utils.js";
@@ -113,22 +110,7 @@ export const explainCommand = buildCommand({
113110
recordSeerOutcome("success");
114111
return { hint: `To create a plan, run: sentry issue plan ${issueArg}` };
115112
} catch (error) {
116-
if (!recorded) {
117-
// Handle API errors with friendly messages
118-
if (error instanceof ApiError) {
119-
const mapped = handleSeerApiError(
120-
error.status,
121-
error.detail,
122-
resolvedOrg
123-
);
124-
recordSeerOutcome(classifySeerError(mapped));
125-
throw mapped;
126-
}
127-
recordSeerOutcome(classifySeerError(error));
128-
} else if (error instanceof ApiError) {
129-
throw handleSeerApiError(error.status, error.detail, resolvedOrg);
130-
}
131-
throw error;
113+
handleSeerCommandError(error, recorded, resolvedOrg);
132114
}
133115
},
134116
});

src/commands/issue/plan.ts

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,16 @@
88
import type { SentryContext } from "../../context.js";
99
import { triggerSolutionPlanning } from "../../lib/api-client.js";
1010
import { buildCommand, numberParser } from "../../lib/command.js";
11-
import { ApiError, ValidationError } from "../../lib/errors.js";
11+
import { ValidationError } from "../../lib/errors.js";
1212
import { CommandOutput } from "../../lib/formatters/output.js";
13-
import {
14-
formatSolution,
15-
handleSeerApiError,
16-
} from "../../lib/formatters/seer.js";
13+
import { formatSolution } from "../../lib/formatters/seer.js";
1714
import {
1815
applyFreshFlag,
1916
FRESH_ALIASES,
2017
FRESH_FLAG,
2118
} from "../../lib/list-command.js";
2219
import { logger } from "../../lib/logger.js";
23-
import { classifySeerError, recordSeerOutcome } from "../../lib/telemetry.js";
20+
import { recordSeerOutcome } from "../../lib/telemetry.js";
2421
import {
2522
type AutofixState,
2623
extractRootCauses,
@@ -30,6 +27,7 @@ import {
3027
} from "../../types/seer.js";
3128
import {
3229
ensureRootCauseAnalysis,
30+
handleSeerCommandError,
3331
issueIdPositional,
3432
pollAutofixState,
3533
resolveOrgAndIssueId,
@@ -126,40 +124,6 @@ function buildPlanData(state: AutofixState): PlanData {
126124
};
127125
}
128126

129-
/**
130-
* Handle errors in Seer commands with outcome recording.
131-
*
132-
* Records the Seer outcome if not already recorded, maps API errors to
133-
* Seer-specific errors, and re-throws. Extracted from the catch block to
134-
* keep command function complexity under the Biome limit.
135-
*
136-
* @param error - The caught error
137-
* @param recorded - Whether outcome was already recorded before the error
138-
* @param resolvedOrg - Org slug for Seer error messages
139-
* @returns never — always throws
140-
*/
141-
function handleSeerCommandError(
142-
error: unknown,
143-
recorded: boolean,
144-
resolvedOrg: string | undefined
145-
): never {
146-
if (!recorded) {
147-
if (error instanceof ApiError) {
148-
const mapped = handleSeerApiError(
149-
error.status,
150-
error.detail,
151-
resolvedOrg
152-
);
153-
recordSeerOutcome(classifySeerError(mapped));
154-
throw mapped;
155-
}
156-
recordSeerOutcome(classifySeerError(error));
157-
} else if (error instanceof ApiError) {
158-
throw handleSeerApiError(error.status, error.detail, resolvedOrg);
159-
}
160-
throw error;
161-
}
162-
163127
export const planCommand = buildCommand({
164128
docs: {
165129
brief: "Generate a solution plan using Seer AI",

src/commands/issue/utils.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ import {
2929
ResolutionError,
3030
withAuthGuard,
3131
} from "../../lib/errors.js";
32-
import { getProgressMessage } from "../../lib/formatters/seer.js";
32+
import {
33+
getProgressMessage,
34+
handleSeerApiError,
35+
} from "../../lib/formatters/seer.js";
3336
import { expandToFullShortId, isShortSuffix } from "../../lib/issue-id.js";
3437
import { logger } from "../../lib/logger.js";
3538
import { poll } from "../../lib/polling.js";
@@ -41,7 +44,11 @@ import {
4144
} from "../../lib/resolve-target.js";
4245
import { parseSentryUrl } from "../../lib/sentry-url-parser.js";
4346
import { buildIssueUrl } from "../../lib/sentry-urls.js";
44-
import { setOrgProjectContext } from "../../lib/telemetry.js";
47+
import {
48+
classifySeerError,
49+
recordSeerOutcome,
50+
setOrgProjectContext,
51+
} from "../../lib/telemetry.js";
4552
import { isAllDigits } from "../../lib/utils.js";
4653
import type { SentryIssue } from "../../types/index.js";
4754
import { type AutofixState, isTerminalStatus } from "../../types/seer.js";
@@ -807,3 +814,37 @@ export async function pollAutofixState(
807814
initialMessage: "Waiting for analysis to start...",
808815
});
809816
}
817+
818+
/**
819+
* Handle errors in Seer commands with outcome recording.
820+
*
821+
* Records the Seer outcome if not already recorded, maps API errors to
822+
* Seer-specific errors, and re-throws. Shared between explain and plan
823+
* commands to keep outcome classification consistent.
824+
*
825+
* @param error - The caught error
826+
* @param recorded - Whether outcome was already recorded before the error
827+
* @param resolvedOrg - Org slug for Seer error messages
828+
* @returns never — always throws
829+
*/
830+
export function handleSeerCommandError(
831+
error: unknown,
832+
recorded: boolean,
833+
resolvedOrg: string | undefined
834+
): never {
835+
if (!recorded) {
836+
if (error instanceof ApiError) {
837+
const mapped = handleSeerApiError(
838+
error.status,
839+
error.detail,
840+
resolvedOrg
841+
);
842+
recordSeerOutcome(classifySeerError(mapped));
843+
throw mapped;
844+
}
845+
recordSeerOutcome(classifySeerError(error));
846+
} else if (error instanceof ApiError) {
847+
throw handleSeerApiError(error.status, error.detail, resolvedOrg);
848+
}
849+
throw error;
850+
}

0 commit comments

Comments
 (0)