Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions packages/cli/src/lib/error-reporting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ export function classifySilenced(error: unknown): SilenceReason | null {
return "output_error";
}
// A raw `TypeError: "fetch failed"` (CLI-16W) means the CLI could not reach
// Sentry at all (offline, DNS, connection refused/timeout). There is nothing
// actionable in a "user is offline" report, so drop it — same rationale as
// EPIPE/EBADF OS noise in `beforeSend`. Note: TLS cert errors are wrapped as
// Sentry at all (offline, DNS, connection refused/timeout); a raw
// `TypeError: "terminated"` (CLI-2AD) means the connection dropped while
// streaming the response body. There is nothing actionable in a "user is
// offline" report, so drop it — same rationale as EPIPE/EBADF OS noise in
// `beforeSend`. Note: TLS cert errors are wrapped as
// ApiError(status 0), NOT matched here, so they stay captured/actionable.
if (isNetworkError(error)) {
return "network_error";
Expand Down Expand Up @@ -116,6 +118,13 @@ export function classifySilenced(error: unknown): SilenceReason | null {
if (error instanceof ValidationError && error.field === "input") {
return "user_input_error";
}
// A ValidationError with field "directory" means the sourcemap directory
// the user passed is missing, not a directory, or unreadable. Pure
// user-input noise, not a CLI bug — the user sees the path and the
// reason (CLI-1FN).
if (error instanceof ValidationError && error.field === "directory") {
return "user_input_error";
}
// A ResolutionError means the user provided a value (event ID, project slug,
// etc.) that was looked up but not found. This is pure user-input noise, not
// a CLI bug — the user sees a clear "not found" message (CLI-RP).
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,12 @@ export function toSearchQueryError(
* `TypeError: "fetch failed"` for every such failure (the real errno is in
* `cause`).
*
* When the connection drops *after* response headers arrive (e.g. `read
* ETIMEDOUT` while streaming the body), undici instead aborts the body stream
* with `TypeError: "terminated"` (CLI-2AD). `sentry-client.ts` already retries
* finite GET bodies, so one that still escapes is the same class of
* environmental failure and is matched too.
*
* This reflects the user's environment, not a CLI bug, so it is treated as a
* user error (no upgrade nudge) and is not reported to Sentry — the same
* rationale as dropping EPIPE/EBADF OS noise in `beforeSend`.
Expand All @@ -922,7 +928,10 @@ export function toSearchQueryError(
* CLI bug" treatment for `status === 0` check it explicitly.
*/
export function isNetworkError(error: unknown): boolean {
return error instanceof TypeError && error.message === "fetch failed";
return (
error instanceof TypeError &&
(error.message === "fetch failed" || error.message === "terminated")
);
}

/**
Expand Down
19 changes: 19 additions & 0 deletions packages/cli/test/lib/error-reporting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,13 @@ describe("classifySilenced", () => {
);
});

test("silences a raw 'terminated' TypeError (mid-body connection drop)", () => {
const err = new TypeError("terminated", {
cause: new Error("read ETIMEDOUT"),
});
expect(classifySilenced(err)).toBe("network_error");
});

test("does NOT silence a TLS cert error wrapped as ApiError(0)", () => {
// status 0 is shared by network failures and TLS cert errors; the latter
// are actionable (missing CA) and must stay captured.
Expand Down Expand Up @@ -334,6 +341,18 @@ describe("classifySilenced", () => {
).toBe("user_input_error");
});

test("silences ValidationError with field 'directory' (bad sourcemap directory)", () => {
// A missing sourcemap upload/inject directory is user-input noise (CLI-1FN).
expect(
classifySilenced(
new ValidationError(
"Directory '/app/.next/static/chunks' does not exist.",
"directory",
),
),
).toBe("user_input_error");
});

test("silences ResolutionError (user provided a value that wasn't found)", () => {
expect(
classifySilenced(
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/test/lib/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,11 @@ describe("isUserError", () => {
new TypeError("fetch failed"),
true,
],
[
"raw terminated TypeError (mid-body network drop)",
new TypeError("terminated"),
true,
],
["ApiError 400", new ApiError("bad request", 400), false],
[
// A user's unparseable --query becomes a ValidationError at the command
Expand Down Expand Up @@ -565,6 +570,18 @@ describe("isNetworkError", () => {
expect(isNetworkError(new TypeError("fetch failed"))).toBe(true);
});

test("true for a raw 'terminated' TypeError (mid-body connection drop)", () => {
const err = new TypeError("terminated", {
cause: new Error("read ETIMEDOUT"),
});
expect(isNetworkError(err)).toBe(true);
expect(isNetworkError(new TypeError("terminated"))).toBe(true);
});

test("false for a plain Error with message 'terminated'", () => {
expect(isNetworkError(new Error("terminated"))).toBe(false);
});

test("false for ApiError status 0 (shared with TLS cert errors)", () => {
// status 0 is also used for TLS cert errors, which must stay actionable.
expect(isNetworkError(new ApiError("Network error", 0))).toBe(false);
Expand Down
Loading