Skip to content

Commit c133708

Browse files
authored
fix(error-reporting): silence SeerError on self-hosted (CLI-1WP) (#1651)
This PR addresses issue CLI-1WP, where `SeerError: Seer is not enabled for this organization.` was being reported to Sentry for self-hosted instances. **Problem:** When `sentry issue explain` or `sentry issue plan` is run against a self-hosted Sentry instance where Seer is disabled, the CLI receives a 403 HTTP response. This is converted into a `SeerError('not_enabled')`. Because `SeerError` extends `CliError` (not `ApiError`), and `classifySilenced()` had no specific handling for `SeerError`, these expected feature-gate conditions were being captured as Sentry issues, polluting the issue tracker. For self-hosted users, Seer cannot be enabled or trialed, making these reports unactionable noise. **Solution:** 1. Added `"seer_unavailable_self_hosted"` to the `SilenceReason` union type in `packages/cli/src/lib/error-reporting.ts`. 2. Modified `classifySilenced()` to return this new reason when a `SeerError` occurs and the CLI is *not* pointed at a SaaS (sentry.io) instance. This ensures that the error is silenced only for self-hosted environments. 3. Added a `seer_reason` attribute to the `cli.error.silenced` metric when a `SeerError` is silenced. This allows tracking the volume of these events without creating Sentry issues. 4. Ensured that `SeerError`s on SaaS instances continue to be captured, as this is an intentional signal for marketing dashboards. 5. Added unit tests to verify that `SeerError`s are silenced on self-hosted configurations and captured on SaaS, and that the correct metric attributes are emitted. This change prevents expected feature-gate conditions on self-hosted Sentry from being reported as CLI errors, while preserving the intended capture behavior for SaaS environments. <!-- SEER_FIXES_SENTRY_ISSUE --> Fixes [CLI-1WP](https://sentry.sentry.io/issues/7513541924/?seerDrawer=true) <!-- /SEER_FIXES_SENTRY_ISSUE --> <sub>`@sentry <feedback>`: Autofix iterates on these changes</sub> <sub>`@sentry stop iterating`: Autofix stops iterating on this run</sub> <sub>This PR was automatically generated by Sentry. You can [adjust this setting](https://sentry.sentry.io/settings/projects/cli/seer/) at any time.</sub> Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
1 parent 6f9d323 commit c133708

2 files changed

Lines changed: 71 additions & 1 deletion

File tree

‎packages/cli/src/lib/error-reporting.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
ValidationError,
4646
WizardError,
4747
} from "./errors.js";
48+
import { isSaaS } from "./sentry-urls.js";
4849

4950
// ---------------------------------------------------------------------------
5051
// Silencing
@@ -61,7 +62,8 @@ type SilenceReason =
6162
| "network_error"
6263
| "process_exit"
6364
| "user_validation"
64-
| "user_input_error";
65+
| "user_input_error"
66+
| "seer_unavailable_self_hosted";
6567

6668
/**
6769
* Classify whether an error should be silenced.
@@ -123,6 +125,12 @@ export function classifySilenced(error: unknown): SilenceReason | null {
123125
if (error instanceof ApiError && error.status > 400 && error.status < 500) {
124126
return "api_user_error";
125127
}
128+
// On SaaS, SeerError stays captured: it feeds the marketing dashboard for
129+
// Seer upsell/trial signal. Self-hosted instances cannot enable Seer or
130+
// start a trial, so there it is pure feature-gate noise (CLI-1WP).
131+
if (error instanceof SeerError && !isSaaS()) {
132+
return "seer_unavailable_self_hosted";
133+
}
126134
// A child process launched by `sentry local run` or `sentry monitor run`
127135
// that exits with a non-zero code throws `CliError("Process exited with
128136
// code N")`. These are expected user-script failures, not CLI bugs — no
@@ -153,6 +161,9 @@ function recordSilencedError(error: unknown, reason: SilenceReason): void {
153161
if (error instanceof AuthError) {
154162
attributes.auth_reason = error.reason;
155163
}
164+
if (error instanceof SeerError) {
165+
attributes.seer_reason = error.reason;
166+
}
156167

157168
// biome-ignore lint/plugin: grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing.
158169
try {

‎packages/cli/test/lib/error-reporting.test.ts‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,49 @@ describe("classifySilenced", () => {
345345
])("does NOT silence %s", (_label, err) => {
346346
expect(classifySilenced(err)).toBeNull();
347347
});
348+
349+
test.each([
350+
"not_enabled",
351+
"no_budget",
352+
"ai_disabled",
353+
] as const)("silences SeerError(%s) on self-hosted (CLI-1WP)", (reason) => {
354+
withSentryUrl("https://sentry.example.com", () => {
355+
expect(classifySilenced(new SeerError(reason, "my-org"))).toBe(
356+
"seer_unavailable_self_hosted"
357+
);
358+
});
359+
});
360+
361+
test("does NOT silence SeerError when SENTRY_URL points at SaaS", () => {
362+
withSentryUrl("https://sentry.io", () => {
363+
expect(classifySilenced(new SeerError("not_enabled"))).toBeNull();
364+
});
365+
});
348366
});
349367

368+
/**
369+
* Run `fn` with `SENTRY_URL` set to `url`, restoring the previous value after.
370+
* `SENTRY_HOST` is cleared for the duration since it takes precedence.
371+
*/
372+
function withSentryUrl(url: string, fn: () => void): void {
373+
const savedUrl = process.env.SENTRY_URL;
374+
const savedHost = process.env.SENTRY_HOST;
375+
process.env.SENTRY_URL = url;
376+
delete process.env.SENTRY_HOST;
377+
try {
378+
fn();
379+
} finally {
380+
if (savedUrl === undefined) {
381+
delete process.env.SENTRY_URL;
382+
} else {
383+
process.env.SENTRY_URL = savedUrl;
384+
}
385+
if (savedHost !== undefined) {
386+
process.env.SENTRY_HOST = savedHost;
387+
}
388+
}
389+
}
390+
350391
// ---------------------------------------------------------------------------
351392
// enrichEventWithGroupingTags
352393
// ---------------------------------------------------------------------------
@@ -571,6 +612,24 @@ describe("reportCliError integration", () => {
571612
expect(metricSpy).not.toHaveBeenCalled();
572613
});
573614

615+
test("silences SeerError on self-hosted and emits metric (CLI-1WP)", () => {
616+
withSentryUrl("https://sentry.example.com", () => {
617+
reportCliError(new SeerError("not_enabled", "my-org"));
618+
});
619+
expect(captureSpy).not.toHaveBeenCalled();
620+
expect(metricSpy).toHaveBeenCalledWith(
621+
"cli.error.silenced",
622+
1,
623+
expect.objectContaining({
624+
attributes: expect.objectContaining({
625+
error_class: "SeerError",
626+
reason: "seer_unavailable_self_hosted",
627+
seer_reason: "not_enabled",
628+
}),
629+
})
630+
);
631+
});
632+
574633
test("silences AuthError(invalid) and emits metric", () => {
575634
reportCliError(new AuthError("invalid"));
576635
expect(captureSpy).not.toHaveBeenCalled();

0 commit comments

Comments
 (0)