Skip to content

Commit fa74a7f

Browse files
committed
feat(cli): pass error code as Sentry tag on captured exceptions
Adds the error code as an `error.code` tag to Sentry exceptions, matching the v1 SentryClient behavior. The code is resolved via resolveErrorCode in both reportError and TaskContextAdapter. Made-with: Cursor
1 parent 6e761d9 commit fa74a7f

5 files changed

Lines changed: 38 additions & 18 deletions

File tree

packages/cli/cli-v2/src/context/adapter/TaskContextAdapter.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type Finishable,
66
type InteractiveTaskContext,
77
type PosthogEvent,
8+
resolveErrorCode,
89
type Startable,
910
TaskAbortSignal,
1011
type TaskContext,
@@ -60,16 +61,21 @@ export class TaskContextAdapter implements TaskContext {
6061
}
6162

6263
public failWithoutThrowing(message?: string, error?: unknown, options?: { code?: CliErrorCode }): void {
64+
this.result = TaskResult.Failure;
65+
if (error instanceof TaskAbortSignal) {
66+
return;
67+
}
6368
const fullMessage = this.getFullErrorMessage(message, error);
6469
if (fullMessage != null) {
6570
this.logger.error(fullMessage);
6671
}
67-
this.result = TaskResult.Failure;
68-
reportError(this.context, error ?? new Error(message), options);
72+
73+
reportError(this.context, error, { ...options, message });
6974
}
7075

71-
public captureException(error: unknown, _code?: CliErrorCode): void {
72-
this.context.telemetry.captureException(error);
76+
public captureException(error: unknown, code?: CliErrorCode): void {
77+
const errorCode = resolveErrorCode(error, code) ?? "INTERNAL_ERROR";
78+
this.context.telemetry.captureException(error, { errorCode });
7379
}
7480

7581
private getFullErrorMessage(message?: string, error?: unknown): string | undefined {

packages/cli/cli-v2/src/context/withContext.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,20 @@ function handleError(context: Context, error: unknown): void {
114114
* Called from the top-level catch in withContext and from
115115
* TaskContextAdapter.failWithoutThrowing.
116116
*/
117-
<<<<<<< HEAD
118-
export function reportError(context: Context, error: unknown, options?: { code?: CliErrorCode }): void {
117+
export function reportError(
118+
context: Context,
119+
error: unknown,
120+
options?: { message?: string; code?: CliErrorCode }
121+
): void {
119122
if (error instanceof TaskAbortSignal) {
120123
return;
121124
}
122-
const code = resolveErrorCode(error, options?.code) ?? "INTERNAL_ERROR";
125+
const code = resolveErrorCode(error, options?.code);
126+
const capturable = error ?? new CliError({ message: options?.message ?? "", code });
123127
if (shouldReportToSentry(code)) {
124-
context.telemetry.captureException(error);
128+
context.telemetry.captureException(capturable, {
129+
errorCode: code
130+
});
125131
}
126132
context.telemetry.sendLifecycleEvent({
127133
status: "error",

packages/cli/cli-v2/src/telemetry/TelemetryClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,15 @@ export class TelemetryClient {
107107
* The caller is responsible for deciding which errors are worth reporting
108108
* (see `shouldReportToSentry` in withContext.ts).
109109
*/
110-
public captureException(error: unknown): void {
110+
public captureException(error: unknown, { errorCode }: { errorCode: string }): void {
111111
if (this.sentry === undefined) {
112112
return;
113113
}
114114
try {
115115
this.sentry.captureException(error, {
116116
captureContext: {
117117
user: { id: this.distinctId },
118-
tags: { ...this.baseTags, ...this.accumulatedTags }
118+
tags: { ...this.baseTags, ...this.accumulatedTags, "error.code": errorCode }
119119
}
120120
});
121121
} catch {

packages/cli/cli/src/telemetry/SentryClient.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,10 @@ export class SentryClient {
2929
return;
3030
}
3131
try {
32-
if (code != null) {
33-
Sentry.withScope((scope) => {
34-
scope.setTag("error.code", code);
35-
this.sentry?.captureException(error);
36-
});
37-
} else {
38-
this.sentry.captureException(error);
39-
}
32+
this.sentry.captureException(
33+
error,
34+
code != null ? { captureContext: { tags: { "error.code": code } } } : undefined
35+
);
4036
} catch {
4137
// no-op
4238
}

packages/cli/cli/src/telemetry/__test__/SentryClient.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ describe("SentryClient (cli-v1)", () => {
5151

5252
expect(mockSentryInit).toHaveBeenCalledOnce();
5353
expect(mockSentryCaptureException).toHaveBeenCalledOnce();
54+
expect(mockSentryCaptureException).toHaveBeenCalledWith(expect.any(Error), undefined);
55+
});
56+
57+
it("passes error.code tag via captureContext when code is provided", () => {
58+
const client = new SentryClient({ release: "cli@1.2.3" });
59+
const error = new Error("something broke");
60+
61+
client.captureException(error, "INTERNAL_ERROR");
62+
63+
expect(mockSentryCaptureException).toHaveBeenCalledWith(error, {
64+
captureContext: { tags: { "error.code": "INTERNAL_ERROR" } }
65+
});
5466
});
5567

5668
it("flushes the Sentry client", async () => {

0 commit comments

Comments
 (0)