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
1 change: 1 addition & 0 deletions apps/cli-docs/src/content/docs/exit-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ react to failure categories without parsing stderr.
| 41 | Seer No Budget | Seer requires a paid plan |
| 42 | AI Disabled | AI features disabled by organization admin |
| 43 | Seer Out Of Credits | Organization is out of Seer credits |
| 44 | Seer Setup Required | Seer needs a GitHub or GitLab integration, or repositories connected to the project |
| 50 | Upgrade Error | CLI upgrade operation failed |
| 51 | Device Flow Error | OAuth device authorization flow failed |
| 60 | Output Error | Command produced output but the operation failed |
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,7 @@ CliError (base, exitCode=1)
├── ResolutionError (exitCode=23 — value provided but not found)
├── ValidationError (exitCode=21 — input validation)
├── DeviceFlowError (exitCode=51 — OAuth flow)
├── SeerError (exitCode=40–43 by reason — 'not_enabled' | 'no_budget' | 'ai_disabled' | 'out_of_credits')
├── SeerError (exitCode=40–44 by reason — 'not_enabled' | 'no_budget' | 'ai_disabled' | 'out_of_credits' | 'scm_integration_required' | 'repos_not_linked')
├── TimeoutError (exitCode=31 — operation timed out)
└── UpgradeError (exitCode=50 — upgrade failures)
```
Expand Down
19 changes: 18 additions & 1 deletion packages/cli/src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
buildBillingUrl,
buildOrgSettingsUrl,
buildSeerSettingsUrl,
buildSourceCodeIntegrationsUrl,
} from "./sentry-urls.js";
import { getLoginCommand, getUrlLoginCommand, sentry } from "./app-identity.js";

Expand Down Expand Up @@ -85,6 +86,8 @@ export const EXIT = {
SEER_AI_DISABLED: 42,
/** Organization is out of Seer credits */
SEER_OUT_OF_CREDITS: 43,
/** Seer needs a GitHub/GitLab integration or repositories connected to the project */
SEER_SETUP_REQUIRED: 44,

// 50–59: Operations
/** CLI upgrade failed */
Expand Down Expand Up @@ -765,7 +768,9 @@ export type SeerErrorReason =
| "not_enabled"
| "no_budget"
| "ai_disabled"
| "out_of_credits";
| "out_of_credits"
| "scm_integration_required"
| "repos_not_linked";

/**
* Seer-specific errors with actionable suggestions.
Expand All @@ -783,12 +788,17 @@ export class SeerError extends CliError {
no_budget: "Seer requires a paid plan.",
ai_disabled: "AI features are disabled for this organization.",
out_of_credits: "Your organization is out of Seer credits.",
scm_integration_required:
"Seer needs a GitHub or GitLab integration for this organization.",
repos_not_linked: "Seer needs repositories connected to this project.",
};
const exitCodes: Record<SeerErrorReason, number> = {
not_enabled: EXIT.SEER_NOT_ENABLED,
no_budget: EXIT.SEER_NO_BUDGET,
ai_disabled: EXIT.SEER_AI_DISABLED,
out_of_credits: EXIT.SEER_OUT_OF_CREDITS,
scm_integration_required: EXIT.SEER_SETUP_REQUIRED,
repos_not_linked: EXIT.SEER_SETUP_REQUIRED,
};
super(messages[reason], exitCodes[reason]);
this.name = "SeerError";
Expand Down Expand Up @@ -817,6 +827,9 @@ export class SeerError extends CliError {
// Sentry doesn't say whether auto-reload is off or has run through its
// top-ups, so the advice covers both.
out_of_credits: `Someone with billing access can adjust Seer spend and auto-reload in Sentry:\n ${buildBillingUrl(this.orgSlug, "seer")}`,
// Setup gaps are fixed in Sentry settings, not with a trial.
scm_integration_required: `Connect GitHub or GitLab to Sentry:\n ${buildSourceCodeIntegrationsUrl(this.orgSlug)}`,
repos_not_linked: `Connect the project's repositories in Seer settings:\n ${buildSeerSettingsUrl(this.orgSlug)}`,
};
return `${this.message}\n\n${suggestions[this.reason]}`;
}
Expand All @@ -831,6 +844,10 @@ export class SeerError extends CliError {
// out_of_credits orgs already have Seer, so a trial is not the fix
out_of_credits:
"Someone with billing access can adjust Seer spend and auto-reload in your organization's billing settings.",
scm_integration_required:
"Connect GitHub or GitLab in your organization's integration settings.",
repos_not_linked:
"Connect the project's repositories in your organization's Seer settings.",
};
return `${this.message}\n\n${fallbackSuggestions[this.reason]}`;
}
Expand Down
22 changes: 18 additions & 4 deletions packages/cli/src/lib/formatters/seer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
RootCause,
SolutionArtifact,
} from "../../types/seer.js";
import { ApiError, SeerError } from "../errors.js";
import { ApiError, SeerError, type SeerErrorReason } from "../errors.js";
import { cyan } from "./colors.js";
import { filterFields } from "./json.js";
import { renderMarkdown } from "./markdown.js";
Expand Down Expand Up @@ -272,6 +272,15 @@ export function jsonTransformIssueExplain(
*/
const OUT_OF_CREDITS_CODE = "seer_out_of_credits";

/**
* `code`s Sentry sends with a 409 when it refuses to start a run because the
* org has no GitHub/GitLab integration or the project has no linked repos.
*/
const SETUP_REQUIRED_REASONS: readonly SeerErrorReason[] = [
Comment thread
sentry-junior[bot] marked this conversation as resolved.
Resolved
"scm_integration_required",
"repos_not_linked",
];

/**
* Create a SeerError from an API error status code and detail.
*
Expand Down Expand Up @@ -304,15 +313,20 @@ export function createSeerError(
// (could be permission denied, rate limiting, etc.)
return null;
}
if (status === 409) {
const reason = SETUP_REQUIRED_REASONS.find((r) => r === code);
// Any other 409 (e.g. a run already in flight) keeps its API detail.
return reason ? new SeerError(reason, orgSlug) : null;
}
return null;
}

/**
* Convert an API error to a Seer-specific error or an ApiError.
*
* Returns a SeerError for known Seer issues (402/403 with specific detail),
* or preserves the error as an ApiError for other failures. Previously
* returned a plain Error which lost the status code and endpoint — causing
* Returns a SeerError for known Seer issues (402/403 with specific detail, or
* a 409 setup code), or preserves the error as an ApiError for other failures.
* Previously returned a plain Error which lost the status code and endpoint — causing
* CLI-N (84 users) where unrecognized 403s became untyped errors.
*
* @param status - HTTP status code
Expand Down
24 changes: 23 additions & 1 deletion packages/cli/test/commands/issue/explain.func.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ vi.mock("../../../src/commands/issue/utils.js", async (importOriginal) => {
import { explainCommand } from "../../../src/commands/issue/explain.js";
// oxlint-disable-next-line sentry-cli/no-namespace-import -- needed for spyOn mocking
import * as issueUtils from "../../../src/commands/issue/utils.js";
import { ApiError, ContextError } from "../../../src/lib/errors.js";
import { ApiError, ContextError, EXIT } from "../../../src/lib/errors.js";
import type { IssueArgument } from "../../../src/lib/issue-arguments.js";
import type { AutofixState, RootCause } from "../../../src/types/seer.js";

Expand Down Expand Up @@ -190,6 +190,28 @@ describe("issue explain multiple IDs", () => {
).rejects.toMatchObject({ reason: "out_of_credits", orgSlug: "test-org" });
});

test("throws repos_not_linked when Sentry refuses the analysis with a 409", async () => {
resolveSpy.mockResolvedValue({ org: "test-org", issueId: "1" });
analyzeSpy.mockRejectedValue(
new ApiError(
"Failed to start root cause analysis: 409 Conflict",
409,
"Seer Autofix requires repositories to be connected to this project before a new run can be started.",
undefined,
{ code: "repos_not_linked" },
),
);

const func = await explainCommand.loader();
await expect(
func.call(createMockContext().context, EXPLAIN_FLAGS, "IOS-1"),
).rejects.toMatchObject({
reason: "repos_not_linked",
orgSlug: "test-org",
exitCode: EXIT.SEER_SETUP_REQUIRED,
});
});

test("fails to parse when no issue ID is provided", async () => {
const func = await explainCommand.loader();
await expect(
Expand Down
28 changes: 28 additions & 0 deletions packages/cli/test/lib/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,28 @@ describe("SeerError", () => {
);
});

test("format() links source code integrations for scm_integration_required", () => {
const err = new SeerError("scm_integration_required", "my-org");
const formatted = err.format();
expect(formatted).toContain(
"Seer needs a GitHub or GitLab integration for this organization.",
);
expect(formatted).toContain(
"https://my-org.sentry.io/settings/integrations/?category=source%20code%20management",
);
expect(formatted).not.toContain("trial");
});

test("format() links Seer settings for repos_not_linked", () => {
const err = new SeerError("repos_not_linked", "my-org");
const formatted = err.format();
expect(formatted).toContain(
"Seer needs repositories connected to this project.",
);
expect(formatted).toContain("https://my-org.sentry.io/settings/seer/");
expect(formatted).not.toContain("trial");
});

test("format() shows fallback message without orgSlug", () => {
const err = new SeerError("not_enabled");
const formatted = err.format();
Expand Down Expand Up @@ -789,6 +811,12 @@ describe("exit codes", () => {
expect(new SeerError("out_of_credits").exitCode).toBe(
EXIT.SEER_OUT_OF_CREDITS,
);
expect(new SeerError("scm_integration_required").exitCode).toBe(
EXIT.SEER_SETUP_REQUIRED,
);
expect(new SeerError("repos_not_linked").exitCode).toBe(
EXIT.SEER_SETUP_REQUIRED,
);
});

test("TimeoutError has exit code TIMEOUT", () => {
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/test/lib/formatters/seer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,32 @@ describe("createSeerError", () => {
expect(error?.orgSlug).toBe("my-org");
});

test.each(["scm_integration_required", "repos_not_linked"] as const)(
"returns %s for a 409 with that setup code",
(code) => {
const error = createSeerError(
409,
"Seer Autofix requires …",
"my-org",
code,
);
expect(error).toBeInstanceOf(SeerError);
expect(error?.reason).toBe(code);
expect(error?.orgSlug).toBe("my-org");
},
);

test("returns null for a 409 without a setup code", () => {
expect(
createSeerError(
409,
"A step is already running for this autofix run",
"my-org",
"run_in_flight",
),
).toBeNull();
});

test("returns out_of_credits for a 402 with the out-of-credits code", () => {
const error = createSeerError(
402,
Expand Down
27 changes: 27 additions & 0 deletions packages/mcp-core/src/internal/tool-helpers/seer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,33 @@ export function getSeerPaymentRequiredMessage(responseBody: unknown): string {
return SEER_NO_QUOTA_MESSAGE;
}

const SEER_SETUP_REQUIRED_CODES = new Set([
"scm_integration_required",
"repos_not_linked",
]);

/**
* Explain a 409 from starting a run when Seer is not set up for the issue's
* project. Returns null for any other 409, such as a run already in flight.
*/
export function getSeerSetupRequiredMessage(
responseBody: unknown,
): string | null {
if (
!isPlainObject(responseBody) ||
typeof responseBody.code !== "string" ||
!SEER_SETUP_REQUIRED_CODES.has(responseBody.code) ||
typeof responseBody.detail !== "string"
) {
return null;
}
return [
responseBody.detail,
"",
"Retrying won't help until someone sets this up in Sentry. Investigate without Seer by reading the issue and its events.",
].join("\n");
}

/**
* Explain why a failed run stopped, when Seer gives a reason we can explain.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/src/skillDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@
"tools": [
{
"name": "analyze_issue_with_seer",
"description": "Use Seer to analyze production errors and get detailed root cause analysis with specific code fixes.\n\nUse this tool when:\n- The user explicitly asks for root cause analysis, Seer analysis, or help fixing/debugging an issue\n- You are unable to accurately determine the root cause from the issue details alone\n\nDo NOT call this tool as an automatic follow-up to get_sentry_resource.\n\nWhat this tool provides:\n- Root cause analysis with code-level explanations\n- Specific file locations and line numbers where errors occur\n- Concrete code fixes you can apply\n- Step-by-step implementation guidance\n\nThis tool automatically:\n1. Checks if analysis already exists (instant results)\n2. Starts new AI analysis if needed (~2-5 minutes)\n3. Returns complete fix recommendations\n\n<examples>\n### User: \"Run Seer on this issue\"\n\n```\nanalyze_issue_with_seer(issueUrl='https://my-org.sentry.io/issues/PROJECT-1Z43')\n```\n\n### User: \"Analyze this issue and suggest a fix\"\n\n```\nanalyze_issue_with_seer(organizationSlug='my-organization', issueId='ERROR-456')\n```\n</examples>\n\n<hints>\n- Only use when the user explicitly requests analysis or you cannot determine the root cause from issue details alone\n- Seer Autofix does not support metric alert issues (issueCategory: metric); use get_issue_details and search_metrics or search_traces instead\n- If the user provides an issueUrl, extract it and use that parameter alone\n- The analysis includes actual code snippets and fixes, not just error descriptions\n- Results are cached - subsequent calls return instantly\n</hints>",
"description": "Use Seer to analyze production errors and get detailed root cause analysis with specific code fixes.\n\nUse this tool when:\n- The user explicitly asks for root cause analysis, Seer analysis, or help fixing/debugging an issue\n- You are unable to accurately determine the root cause from the issue details alone\n\nDo NOT call this tool as an automatic follow-up to get_sentry_resource.\n\nWhat this tool provides:\n- Root cause analysis with code-level explanations\n- Specific file locations and line numbers where errors occur\n- Concrete code fixes you can apply\n- Step-by-step implementation guidance\n\nThis tool automatically:\n1. Checks if analysis already exists (instant results)\n2. Starts new AI analysis if needed (~2-5 minutes)\n3. Returns complete fix recommendations\n\n<examples>\n### User: \"Run Seer on this issue\"\n\n```\nanalyze_issue_with_seer(issueUrl='https://my-org.sentry.io/issues/PROJECT-1Z43')\n```\n\n### User: \"Analyze this issue and suggest a fix\"\n\n```\nanalyze_issue_with_seer(organizationSlug='my-organization', issueId='ERROR-456')\n```\n</examples>\n\n<hints>\n- Only use when the user explicitly requests analysis or you cannot determine the root cause from issue details alone\n- Seer Autofix does not support metric alert issues (issueCategory: metric); use get_issue_details and search_metrics or search_traces instead\n- Starting a new analysis requires a GitHub or GitLab integration and repositories connected to the issue's project; without them the tool reports that no analysis started\n- If the user provides an issueUrl, extract it and use that parameter alone\n- The analysis includes actual code snippets and fixes, not just error descriptions\n- Results are cached - subsequent calls return instantly\n</hints>",
"requiredScopes": []
},
{
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/src/toolDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
{
"name": "analyze_issue_with_seer",
"title": "Analyze Issue with Seer",
"description": "Use Seer to analyze production errors and get detailed root cause analysis with specific code fixes.\n\nUse this tool when:\n- The user explicitly asks for root cause analysis, Seer analysis, or help fixing/debugging an issue\n- You are unable to accurately determine the root cause from the issue details alone\n\nDo NOT call this tool as an automatic follow-up to get_sentry_resource.\n\nWhat this tool provides:\n- Root cause analysis with code-level explanations\n- Specific file locations and line numbers where errors occur\n- Concrete code fixes you can apply\n- Step-by-step implementation guidance\n\nThis tool automatically:\n1. Checks if analysis already exists (instant results)\n2. Starts new AI analysis if needed (~2-5 minutes)\n3. Returns complete fix recommendations\n\n<examples>\n### User: \"Run Seer on this issue\"\n\n```\nanalyze_issue_with_seer(issueUrl='https://my-org.sentry.io/issues/PROJECT-1Z43')\n```\n\n### User: \"Analyze this issue and suggest a fix\"\n\n```\nanalyze_issue_with_seer(organizationSlug='my-organization', issueId='ERROR-456')\n```\n</examples>\n\n<hints>\n- Only use when the user explicitly requests analysis or you cannot determine the root cause from issue details alone\n- Seer Autofix does not support metric alert issues (issueCategory: metric); use get_issue_details and search_metrics or search_traces instead\n- If the user provides an issueUrl, extract it and use that parameter alone\n- The analysis includes actual code snippets and fixes, not just error descriptions\n- Results are cached - subsequent calls return instantly\n</hints>",
"description": "Use Seer to analyze production errors and get detailed root cause analysis with specific code fixes.\n\nUse this tool when:\n- The user explicitly asks for root cause analysis, Seer analysis, or help fixing/debugging an issue\n- You are unable to accurately determine the root cause from the issue details alone\n\nDo NOT call this tool as an automatic follow-up to get_sentry_resource.\n\nWhat this tool provides:\n- Root cause analysis with code-level explanations\n- Specific file locations and line numbers where errors occur\n- Concrete code fixes you can apply\n- Step-by-step implementation guidance\n\nThis tool automatically:\n1. Checks if analysis already exists (instant results)\n2. Starts new AI analysis if needed (~2-5 minutes)\n3. Returns complete fix recommendations\n\n<examples>\n### User: \"Run Seer on this issue\"\n\n```\nanalyze_issue_with_seer(issueUrl='https://my-org.sentry.io/issues/PROJECT-1Z43')\n```\n\n### User: \"Analyze this issue and suggest a fix\"\n\n```\nanalyze_issue_with_seer(organizationSlug='my-organization', issueId='ERROR-456')\n```\n</examples>\n\n<hints>\n- Only use when the user explicitly requests analysis or you cannot determine the root cause from issue details alone\n- Seer Autofix does not support metric alert issues (issueCategory: metric); use get_issue_details and search_metrics or search_traces instead\n- Starting a new analysis requires a GitHub or GitLab integration and repositories connected to the issue's project; without them the tool reports that no analysis started\n- If the user provides an issueUrl, extract it and use that parameter alone\n- The analysis includes actual code snippets and fixes, not just error descriptions\n- Results are cached - subsequent calls return instantly\n</hints>",
"inputSchema": {
"type": "object",
"properties": {
Expand Down
Loading
Loading