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
148 changes: 148 additions & 0 deletions src/commands/issue/explain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* sentry issue explain
*
* Get root cause analysis for a Sentry issue using Seer AI.
*/

import { buildCommand } from "@stricli/core";
import type { SentryContext } from "../../context.js";
import {
getAutofixState,
triggerRootCauseAnalysis,
} from "../../lib/api-client.js";
import { ApiError } from "../../lib/errors.js";
import { writeJson } from "../../lib/formatters/index.js";
import {
formatAutofixError,
formatRootCauseList,
} from "../../lib/formatters/seer.js";
import { extractRootCauses } from "../../types/seer.js";
import { pollAutofixState, resolveOrgAndIssueId } from "./utils.js";

type ExplainFlags = {
readonly org?: string;
readonly json: boolean;
readonly force: boolean;
};

export const explainCommand = buildCommand({
docs: {
brief: "Analyze an issue's root cause using Seer AI",
fullDescription:
"Get a root cause analysis for a Sentry issue using Seer AI.\n\n" +
"This command analyzes the issue and provides:\n" +
" - Identified root causes\n" +
" - Reproduction steps\n" +
" - Relevant code locations\n\n" +
"The analysis may take a few minutes for new issues.\n" +
"Use --force to trigger a fresh analysis even if one already exists.\n\n" +
"Examples:\n" +
" sentry issue explain 123456789\n" +
" sentry issue explain MYPROJECT-ABC --org my-org\n" +
" sentry issue explain 123456789 --json\n" +
" sentry issue explain 123456789 --force",
},
parameters: {
positional: {
kind: "tuple",
parameters: [
{
brief: "Issue ID or short ID (e.g., MYPROJECT-ABC or 123456789)",
parse: String,
},
],
},
flags: {
org: {
kind: "parsed",
parse: String,
brief:
"Organization slug (required for short IDs if not auto-detected)",
optional: true,
},
json: {
kind: "boolean",
brief: "Output as JSON",
default: false,
},
force: {
kind: "boolean",
brief: "Force new analysis even if one exists",
default: false,
},
},
},
async func(
this: SentryContext,
flags: ExplainFlags,
issueId: string
): Promise<void> {
const { stdout, stderr, cwd } = this;

try {
// Resolve org and issue ID
const { org, issueId: numericId } = await resolveOrgAndIssueId(
issueId,
flags.org,
cwd,
`sentry issue explain ${issueId} --org <org-slug>`
);

// 1. Check for existing analysis (skip if --force)
let state = flags.force ? null : await getAutofixState(org, numericId);

// Handle error status, we are gonna retry the analysis
if (state?.status === "ERROR") {
stderr.write("Root cause analysis failed, retrying...\n");
state = null;
}

// 2. Trigger new analysis if none exists or forced
if (!state) {
if (!flags.json) {
let message = flags.force ? "Forcing fresh" : "Starting";

message += " root cause analysis, it can take several minutes...\n";
stderr.write(message);
}
await triggerRootCauseAnalysis(org, numericId);
}

// 3. Poll until complete (if not already completed)
if (!state || state.status !== "COMPLETED") {
state = await pollAutofixState({
orgSlug: org,
issueId: numericId,
stderr,
json: flags.json,
stopOnWaitingForUser: true,
});
}

// 4. Extract root causes from steps
const causes = extractRootCauses(state);
if (causes.length === 0) {
throw new Error(
"Analysis completed but no root causes found. " +
"The issue may not have enough context for root cause analysis."
);
}

// 5. Output results
if (flags.json) {
writeJson(stdout, causes);
return;
}

// Human-readable output
const lines = formatRootCauseList(causes, issueId);
stdout.write(`${lines.join("\n")}\n`);
} catch (error) {
// Handle API errors with friendly messages
if (error instanceof ApiError) {
throw new Error(formatAutofixError(error.status, error.detail));
}
throw error;
}
},
});
12 changes: 10 additions & 2 deletions src/commands/issue/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { buildRouteMap } from "@stricli/core";
import { explainCommand } from "./explain.js";
import { listCommand } from "./list.js";
import { planCommand } from "./plan.js";
import { viewCommand } from "./view.js";

export const issueRoute = buildRouteMap({
routes: {
list: listCommand,
explain: explainCommand,
plan: planCommand,
view: viewCommand,
},
docs: {
brief: "Manage Sentry issues",
fullDescription:
"View and manage issues from your Sentry projects. " +
"Use 'sentry issue list' to list issues and 'sentry issue view <id>' to view issue details.",
"View and manage issues from your Sentry projects.\n\n" +
"Commands:\n" +
" list List issues in a project\n" +
" view View details of a specific issue\n" +
" explain Analyze an issue using Seer AI\n" +
" plan Generate a solution plan using Seer AI",
hideRoute: {},
},
});
Loading
Loading