Skip to content
Closed
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
9 changes: 8 additions & 1 deletion packages/cli/src/lib/error-reporting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ type SilenceReason =
| "output_error"
| "auth_expected"
| "api_user_error"
| "network_error";
| "network_error"
| "feature_disabled";

/**
* Classify whether an error should be silenced.
Expand Down Expand Up @@ -94,6 +95,12 @@ export function classifySilenced(error: unknown): SilenceReason | null {
if (error instanceof ApiError && error.status > 400 && error.status < 500) {
return "api_user_error";
}
// SeerError means the Seer feature is not available for this org (not enabled,
// AI features disabled, or no budget). These are expected feature-gate
// conditions — not CLI bugs — so they should not surface as actionable issues.
if (error instanceof SeerError) {
return "feature_disabled";
}
// A 400 (Bad Request) signals a malformed request the CLI built — a code
// defect — so it is always captured. A user's unparseable `--query` is NOT a
// 400 here: it is converted to a ValidationError at the command boundary
Expand Down
23 changes: 19 additions & 4 deletions packages/cli/test/lib/error-reporting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,12 +304,19 @@ describe("classifySilenced", () => {
new ResolutionError("Project 'x'", "not found", "sentry issue list"),
],
["ValidationError", new ValidationError("bad")],
["SeerError", new SeerError("not_enabled")],
["ConfigError", new ConfigError("bad")],
["generic Error", new Error("boom")],
])("does NOT silence %s", (_label, err) => {
expect(classifySilenced(err)).toBeNull();
});

test.each([
["not_enabled", new SeerError("not_enabled")],
["no_budget", new SeerError("no_budget")],
["ai_disabled", new SeerError("ai_disabled")],
])("silences SeerError(%s) as feature_disabled", (_label, err) => {
expect(classifySilenced(err)).toBe("feature_disabled");
});
});

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -518,10 +525,18 @@ describe("reportCliError integration", () => {
expect(captureSpy).toHaveBeenCalledWith(err);
});

test("captures SeerError (marketing dashboard)", () => {
test("silences SeerError and emits metric", () => {
reportCliError(new SeerError("not_enabled", "my-org"));
expect(captureSpy).toHaveBeenCalled();
expect(metricSpy).not.toHaveBeenCalled();
expect(captureSpy).not.toHaveBeenCalled();
expect(metricSpy).toHaveBeenCalledWith(
"cli.error.silenced",
1,
expect.objectContaining({
attributes: expect.objectContaining({
reason: "feature_disabled",
}),
})
);
});

test("silences AuthError(invalid) and emits metric", () => {
Expand Down
Loading