From 9cc5b596de1719a09677e7e508ca8f9efcdd4116 Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:39:06 +0000 Subject: [PATCH 1/3] fix(error-reporting): silence ResolutionError to prevent crash reports --- src/lib/error-reporting.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/error-reporting.ts b/src/lib/error-reporting.ts index 03cc87c28..aca0cbc88 100644 --- a/src/lib/error-reporting.ts +++ b/src/lib/error-reporting.ts @@ -42,7 +42,7 @@ import { * Reasons an error may be silenced (not sent to Sentry as an issue). * Exposed as the `reason` attribute on the `cli.error.silenced` metric. */ -type SilenceReason = "output_error" | "auth_expected" | "api_user_error"; +type SilenceReason = "output_error" | "auth_expected" | "api_user_error" | "user_input_error"; /** * Classify whether an error should be silenced. @@ -54,6 +54,9 @@ export function classifySilenced(error: unknown): SilenceReason | null { if (error instanceof OutputError) { return "output_error"; } + if (error instanceof ResolutionError) { + return "user_input_error"; + } if ( error instanceof AuthError && (error.reason === "not_authenticated" || error.reason === "expired") From c1da4e3e3d7e086c3f33b402f7dc8786bfc00638 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" <286517962+jared-outpost[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 01:52:56 +0000 Subject: [PATCH 2/3] fix: update tests and docs for ResolutionError silencing --- src/lib/error-reporting.ts | 12 ++++++---- test/lib/error-reporting.test.ts | 39 ++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/lib/error-reporting.ts b/src/lib/error-reporting.ts index aca0cbc88..fae6a429f 100644 --- a/src/lib/error-reporting.ts +++ b/src/lib/error-reporting.ts @@ -3,9 +3,9 @@ * * Provides two things: * - * 1. **Silencing rules** — `OutputError`, expected-state `AuthError`, and - * 401–499 `ApiError` are not sent to Sentry as issues. A - * `cli.error.silenced` metric preserves volume + user/org context. + * 1. **Silencing rules** — `OutputError`, `ResolutionError`, expected-state + * `AuthError`, and 401–499 `ApiError` are not sent to Sentry as issues. + * A `cli.error.silenced` metric preserves volume + user/org context. * * 2. **Grouping tags** — enriches every error event with `cli_error.*` tags * that Sentry's server-side fingerprint rules use for stable grouping. @@ -42,7 +42,11 @@ import { * Reasons an error may be silenced (not sent to Sentry as an issue). * Exposed as the `reason` attribute on the `cli.error.silenced` metric. */ -type SilenceReason = "output_error" | "auth_expected" | "api_user_error" | "user_input_error"; +type SilenceReason = + | "output_error" + | "auth_expected" + | "api_user_error" + | "user_input_error"; /** * Classify whether an error should be silenced. diff --git a/test/lib/error-reporting.test.ts b/test/lib/error-reporting.test.ts index 731d3c072..769272afd 100644 --- a/test/lib/error-reporting.test.ts +++ b/test/lib/error-reporting.test.ts @@ -2,7 +2,7 @@ * Unit tests for the central error reporting helper. * * Covers: - * - Silencing rules (OutputError / expected AuthError / 401–499 ApiError) + * - Silencing rules (OutputError / ResolutionError / expected AuthError / 401–499 ApiError) * - Grouping tag extraction (extractResourceKind) * - Tag enrichment in beforeSend (enrichEventWithGroupingTags) * - End-to-end behavior of reportCliError (metric emission + capture) @@ -244,12 +244,16 @@ describe("classifySilenced", () => { expect(classifySilenced(new ApiError("x", status))).toBeNull(); }); + test("silences ResolutionError as user_input_error", () => { + expect( + classifySilenced( + new ResolutionError("Project 'x'", "not found", "sentry issue list") + ) + ).toBe("user_input_error"); + }); + test.each([ ["ContextError", new ContextError("Organization", "sentry org view ")], - [ - "ResolutionError", - new ResolutionError("Project 'x'", "not found", "sentry issue list"), - ], ["ValidationError", new ValidationError("bad")], ["SeerError", new SeerError("not_enabled")], ["ConfigError", new ConfigError("bad")], @@ -447,14 +451,25 @@ describe("reportCliError integration", () => { expect(traceErr["cli_error.kind"]).not.toBe(eventErr["cli_error.kind"]); }); - test("captures ResolutionError", () => { - const err = new ResolutionError( - "Project 'x'", - "not found", - "sentry issue list /x" + test("silences ResolutionError and emits metric", () => { + reportCliError( + new ResolutionError( + "Project 'x'", + "not found", + "sentry issue list /x" + ) + ); + expect(captureSpy).not.toHaveBeenCalled(); + expect(metricSpy).toHaveBeenCalledWith( + "cli.error.silenced", + 1, + expect.objectContaining({ + attributes: expect.objectContaining({ + error_class: "ResolutionError", + reason: "user_input_error", + }), + }) ); - reportCliError(err); - expect(captureSpy).toHaveBeenCalledWith(err); }); test("captures SeerError (marketing dashboard)", () => { From 3b1abea95e605915a6633b5780786d55204a13ea Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Tue, 23 Jun 2026 22:33:15 +0000 Subject: [PATCH 3/3] fix(error-reporting): make ResolutionError silencing opt-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Silencing the entire ResolutionError class was too broad — it suppressed project/issue/replay/log/trace lookup failures that we still want captured for observability, and stopped them counting as crashed sessions. Add an opt-in `expected` flag (default false) and only silence instances that set it. Mark the event-not-found sites in event/view.ts as expected (the originally intended case). Preserve resource_kind on the silenced metric so sub-grouping context isn't lost. --- src/commands/event/view.ts | 10 +++++++-- src/lib/error-reporting.ts | 20 ++++++++++++++---- src/lib/errors.ts | 18 +++++++++++++++- test/lib/error-reporting.test.ts | 35 +++++++++++++++++++++++++++----- 4 files changed, 71 insertions(+), 12 deletions(-) diff --git a/src/commands/event/view.ts b/src/commands/event/view.ts index 51c0b123d..9009dec72 100644 --- a/src/commands/event/view.ts +++ b/src/commands/event/view.ts @@ -501,7 +501,11 @@ export async function resolveOrgAllTarget( throw new ResolutionError( `Event ${eventId} in organization "${org}"`, "not found", - `sentry event view ${org}/ ${eventId}` + `sentry event view ${org}/ ${eventId}`, + [], + // Looking up an event ID that doesn't exist is an expected user miss, + // not a CLI bug — silence it from Sentry issues. + { expected: true } ); } return { @@ -700,7 +704,9 @@ export async function fetchEventWithContext( `Event '${eventId}'`, `not found in ${org}/${project}`, `sentry event view ${org}/ ${eventId}`, - suggestions + suggestions, + // Expected user miss: a specific event ID that doesn't exist. + { expected: true } ); } throw error; diff --git a/src/lib/error-reporting.ts b/src/lib/error-reporting.ts index fae6a429f..6584a0636 100644 --- a/src/lib/error-reporting.ts +++ b/src/lib/error-reporting.ts @@ -3,9 +3,11 @@ * * Provides two things: * - * 1. **Silencing rules** — `OutputError`, `ResolutionError`, expected-state - * `AuthError`, and 401–499 `ApiError` are not sent to Sentry as issues. - * A `cli.error.silenced` metric preserves volume + user/org context. + * 1. **Silencing rules** — `OutputError`, opt-in `expected` `ResolutionError`, + * expected-state `AuthError`, and 401–499 `ApiError` are not sent to Sentry + * as issues. A `cli.error.silenced` metric preserves volume + user/org + * context. Resolution failures are silenced only when constructed with + * `{ expected: true }`; the class as a whole stays captured. * * 2. **Grouping tags** — enriches every error event with `cli_error.*` tags * that Sentry's server-side fingerprint rules use for stable grouping. @@ -58,7 +60,11 @@ export function classifySilenced(error: unknown): SilenceReason | null { if (error instanceof OutputError) { return "output_error"; } - if (error instanceof ResolutionError) { + // Only silence resolution failures that explicitly opt in via `expected` + // (e.g. a looked-up event/issue ID that genuinely doesn't exist). The class + // as a whole stays captured — project/issue/replay/log/trace lookup failures + // remain observable for product/CLI telemetry. + if (error instanceof ResolutionError && error.expected) { return "user_input_error"; } if ( @@ -85,6 +91,12 @@ function recordSilencedError(error: unknown, reason: SilenceReason): void { if (error instanceof AuthError) { attributes.auth_reason = error.reason; } + // Preserve the resource "kind" so silenced resolution misses keep some + // sub-grouping context (e.g. "Event not found" vs "Issue not found") in the + // metric instead of collapsing to a single error_class=ResolutionError tag. + if (error instanceof ResolutionError) { + attributes.resource_kind = extractResourceKind(error.resource); + } try { Sentry.metrics.distribution("cli.error.silenced", 1, { attributes }); diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 8427cf14c..b4408dd6f 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -474,18 +474,33 @@ export class ContextError extends CliError { * @param headline - Short phrase describing the failure (e.g., "not found", "is ambiguous", "could not be resolved") * @param hint - Primary usage example or suggestion (shown under "Try:") * @param suggestions - Additional help bullets shown under "Or:" (defaults to empty) + * @param options - Optional behavior overrides + * @param options.expected - When `true`, marks this failure as an expected, + * user-driven miss (e.g. a looked-up event/issue ID that genuinely doesn't + * exist) so it is silenced rather than captured as a Sentry issue. Defaults + * to `false`: by default resolution failures are still captured for + * product/CLI observability. Silencing is opt-in per call site, never + * class-wide — see `classifySilenced` in `error-reporting.ts`. */ export class ResolutionError extends CliError { readonly resource: string; readonly headline: string; readonly hint: string; readonly suggestions: string[]; + /** + * Whether this resolution failure is an expected user-driven miss that + * should be silenced (no Sentry issue, no crashed session). Opt-in per + * call site; defaults to `false` so failures remain observable. + */ + readonly expected: boolean; + // biome-ignore lint/nursery/useMaxParams: established 4-param shape; options is a defaulted extension constructor( resource: string, headline: string, hint: string, - suggestions: string[] = [] + suggestions: string[] = [], + options: { expected?: boolean } = {} ) { super( buildResolutionMessage(resource, headline, hint, suggestions), @@ -496,6 +511,7 @@ export class ResolutionError extends CliError { this.headline = headline; this.hint = hint; this.suggestions = suggestions; + this.expected = options.expected ?? false; } override format(): string { diff --git a/test/lib/error-reporting.test.ts b/test/lib/error-reporting.test.ts index 769272afd..bc14e7a4f 100644 --- a/test/lib/error-reporting.test.ts +++ b/test/lib/error-reporting.test.ts @@ -244,16 +244,27 @@ describe("classifySilenced", () => { expect(classifySilenced(new ApiError("x", status))).toBeNull(); }); - test("silences ResolutionError as user_input_error", () => { + test("silences ResolutionError only when expected:true", () => { expect( classifySilenced( - new ResolutionError("Project 'x'", "not found", "sentry issue list") + new ResolutionError( + "Event 'abc'", + "not found", + "sentry event view / abc", + [], + { expected: true } + ) ) ).toBe("user_input_error"); }); test.each([ ["ContextError", new ContextError("Organization", "sentry org view ")], + [ + // Plain (non-expected) resolution failures stay captured for observability. + "ResolutionError", + new ResolutionError("Project 'x'", "not found", "sentry issue list"), + ], ["ValidationError", new ValidationError("bad")], ["SeerError", new SeerError("not_enabled")], ["ConfigError", new ConfigError("bad")], @@ -451,12 +462,25 @@ describe("reportCliError integration", () => { expect(traceErr["cli_error.kind"]).not.toBe(eventErr["cli_error.kind"]); }); - test("silences ResolutionError and emits metric", () => { + test("captures plain ResolutionError (not expected)", () => { + const err = new ResolutionError( + "Project 'x'", + "not found", + "sentry issue list /x" + ); + reportCliError(err); + expect(captureSpy).toHaveBeenCalledWith(err); + expect(metricSpy).not.toHaveBeenCalled(); + }); + + test("silences expected ResolutionError and emits metric with resource_kind", () => { reportCliError( new ResolutionError( - "Project 'x'", + "Event 'abc' in organization \"my-org\"", "not found", - "sentry issue list /x" + "sentry event view my-org/ abc", + [], + { expected: true } ) ); expect(captureSpy).not.toHaveBeenCalled(); @@ -467,6 +491,7 @@ describe("reportCliError integration", () => { attributes: expect.objectContaining({ error_class: "ResolutionError", reason: "user_input_error", + resource_kind: "Event", }), }) );