All files / src/commands/issue explain.ts

6.66% Statements 1/15
0% Branches 0/4
0% Functions 0/1
6.66% Lines 1/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119                                                                  10x                                                                                                                                                                          
/**
 * sentry issue explain
 *
 * Get root cause analysis for a Sentry issue using Seer AI.
 */
 
import type { SentryContext } from "../../context.js";
import { buildCommand } from "../../lib/command.js";
import { ApiError } from "../../lib/errors.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import {
  formatRootCauseList,
  handleSeerApiError,
} from "../../lib/formatters/seer.js";
import {
  applyFreshFlag,
  FRESH_ALIASES,
  FRESH_FLAG,
} from "../../lib/list-command.js";
import { extractRootCauses } from "../../types/seer.js";
import {
  ensureRootCauseAnalysis,
  issueIdPositional,
  resolveOrgAndIssueId,
} from "./utils.js";
 
type ExplainFlags = {
  readonly json: boolean;
  readonly force: boolean;
  readonly fresh: boolean;
  readonly fields?: string[];
};
 
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" +
      "Issue formats:\n" +
      "  @latest          - Most recent unresolved issue\n" +
      "  @most_frequent   - Issue with highest event frequency\n" +
      "  <org>/ID         - Explicit org: sentry/EXTENSION-7, sentry/cli-G\n" +
      "  <org>/@selector  - Selector with org: my-org/@latest\n" +
      "  <project>-suffix - Project + suffix: cli-G, spotlight-electron-4Y\n" +
      "  ID               - Short ID: CLI-G (searches across orgs)\n" +
      "  suffix           - Suffix only: G (requires DSN context)\n" +
      "  numeric          - Numeric ID: 123456789\n\n" +
      "Examples:\n" +
      "  sentry issue explain @latest\n" +
      "  sentry issue explain 123456789\n" +
      "  sentry issue explain sentry/EXTENSION-7\n" +
      "  sentry issue explain cli-G\n" +
      "  sentry issue explain 123456789 --json\n" +
      "  sentry issue explain 123456789 --force",
  },
  output: { human: formatRootCauseList },
  parameters: {
    positional: issueIdPositional,
    flags: {
      force: {
        kind: "boolean",
        brief: "Force new analysis even if one exists",
        default: false,
      },
      fresh: FRESH_FLAG,
    },
    aliases: FRESH_ALIASES,
  },
  async *func(this: SentryContext, flags: ExplainFlags, issueArg: string) {
    applyFreshFlag(flags);
    const { cwd } = this;
 
    // Declare org outside try block so it's accessible in catch for error messages
    let resolvedOrg: string | undefined;
 
    try {
      // Resolve org and issue ID
      const { org, issueId: numericId } = await resolveOrgAndIssueId({
        issueArg,
        cwd,
        command: "explain",
      });
      resolvedOrg = org;
 
      // Ensure root cause analysis exists (triggers if needed)
      const state = await ensureRootCauseAnalysis({
        org,
        issueId: numericId,
        json: flags.json,
        force: flags.force,
      });
 
      // 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."
        );
      }
 
      yield new CommandOutput(causes);
      return { hint: `To create a plan, run: sentry issue plan ${issueArg}` };
    } catch (error) {
      // Handle API errors with friendly messages
      if (error instanceof ApiError) {
        throw handleSeerApiError(error.status, error.detail, resolvedOrg);
      }
      throw error;
    }
  },
});