From 56369201f13f214a1f99cb75d92c60df817b2fa6 Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Thu, 5 Feb 2026 14:26:19 +0530 Subject: [PATCH 1/5] fix: created a common function for explain --- src/commands/issue/explain.ts | 48 ++++++------------------ src/commands/issue/plan.ts | 69 +++++++++++------------------------ src/commands/issue/utils.ts | 69 +++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 85 deletions(-) diff --git a/src/commands/issue/explain.ts b/src/commands/issue/explain.ts index c1e0b3a3..d51482e7 100644 --- a/src/commands/issue/explain.ts +++ b/src/commands/issue/explain.ts @@ -6,10 +6,6 @@ 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 { writeFooter, writeJson } from "../../lib/formatters/index.js"; import { @@ -18,8 +14,8 @@ import { } from "../../lib/formatters/seer.js"; import { extractRootCauses } from "../../types/seer.js"; import { + ensureRootCauseAnalysis, issueIdPositional, - pollAutofixState, resolveOrgAndIssueId, } from "./utils.js"; @@ -86,38 +82,16 @@ export const explainCommand = buildCommand({ }); resolvedOrg = org; - // 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) { - const prefix = flags.force ? "Forcing fresh" : "Starting"; - stderr.write( - `${prefix} root cause analysis, it can take several minutes...\n` - ); - } - 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, - }); - } + // Ensure root cause analysis exists (triggers if needed) + const state = await ensureRootCauseAnalysis({ + org, + issueId: numericId, + stderr, + json: flags.json, + force: flags.force, + }); - // 4. Extract root causes from steps + // Extract root causes from steps const causes = extractRootCauses(state); if (causes.length === 0) { throw new Error( @@ -126,7 +100,7 @@ export const explainCommand = buildCommand({ ); } - // 5. Output results + // Output results if (flags.json) { writeJson(stdout, causes); return; diff --git a/src/commands/issue/plan.ts b/src/commands/issue/plan.ts index 2f240e00..d917b60a 100644 --- a/src/commands/issue/plan.ts +++ b/src/commands/issue/plan.ts @@ -2,15 +2,12 @@ * sentry issue plan * * Generate a solution plan for a Sentry issue using Seer AI. - * Requires that 'sentry issue explain' has been run first. + * Automatically runs root cause analysis if not already done. */ import { buildCommand, numberParser } from "@stricli/core"; import type { SentryContext } from "../../context.js"; -import { - getAutofixState, - triggerSolutionPlanning, -} from "../../lib/api-client.js"; +import { triggerSolutionPlanning } from "../../lib/api-client.js"; import { ApiError, ValidationError } from "../../lib/errors.js"; import { muted } from "../../lib/formatters/colors.js"; import { writeJson } from "../../lib/formatters/index.js"; @@ -25,6 +22,7 @@ import { type RootCause, } from "../../types/seer.js"; import { + ensureRootCauseAnalysis, issueIdPositional, pollAutofixState, resolveOrgAndIssueId, @@ -36,49 +34,20 @@ type PlanFlags = { }; /** - * Validate that an autofix run exists and has completed root cause analysis. + * Validate that the autofix state has root causes identified. * - * @param state - Current autofix state - * @param issueId - Issue ID for error messages - * @returns The validated state and root causes + * @param state - Current autofix state (already ensured to exist) + * @returns Array of root causes + * @throws {ValidationError} If no root causes found */ -function validateAutofixState( - state: AutofixState | null, - issueId: string -): { state: AutofixState; causes: RootCause[] } { - if (!state) { - throw new ValidationError( - `No root cause analysis found for issue ${issueId}.\n` + - `Run 'sentry issue explain ${issueId}' first.` - ); - } - - // Check if the autofix is in a state where we can continue - const validStatuses = ["COMPLETED", "WAITING_FOR_USER_RESPONSE"]; - if (!validStatuses.includes(state.status)) { - if (state.status === "PROCESSING") { - throw new ValidationError( - "Root cause analysis is still in progress. Please wait for it to complete." - ); - } - if (state.status === "ERROR") { - throw new ValidationError( - "Root cause analysis failed. Check the Sentry web UI for details." - ); - } - throw new ValidationError( - `Cannot create plan: autofix is in '${state.status}' state.` - ); - } - +function validateRootCauses(state: AutofixState): RootCause[] { const causes = extractRootCauses(state); if (causes.length === 0) { throw new ValidationError( "No root causes identified. Cannot create a plan without a root cause." ); } - - return { state, causes }; + return causes; } /** @@ -134,10 +103,9 @@ export const planCommand = buildCommand({ brief: "Generate a solution plan using Seer AI", fullDescription: "Generate a solution plan for a Sentry issue using Seer AI.\n\n" + - "This command requires that 'sentry issue explain' has been run first " + - "to identify the root cause. It will then generate a solution plan with " + - "specific implementation steps to fix the issue.\n\n" + - "If multiple root causes were identified, use --cause to specify which one.\n\n" + + "This command automatically runs root cause analysis if needed, then " + + "generates a solution plan with specific implementation steps to fix the issue.\n\n" + + "If multiple root causes are identified, use --cause to specify which one.\n\n" + "Issue formats:\n" + " /ID - Explicit org: sentry/EXTENSION-7, sentry/cli-G\n" + " -suffix - Project + suffix: cli-G, spotlight-electron-4Y\n" + @@ -187,11 +155,16 @@ export const planCommand = buildCommand({ }); resolvedOrg = org; - // Get current autofix state - const currentState = await getAutofixState(org, numericId); + // Ensure root cause analysis exists (runs explain if needed) + const state = await ensureRootCauseAnalysis({ + org, + issueId: numericId, + stderr, + json: flags.json, + }); - // Validate we have a completed root cause analysis - const { state, causes } = validateAutofixState(currentState, issueArg); + // Validate we have root causes + const causes = validateRootCauses(state); // Validate cause selection const causeId = validateCauseSelection(causes, flags.cause, issueArg); diff --git a/src/commands/issue/utils.ts b/src/commands/issue/utils.ts index 5cd861e4..aea80e1a 100644 --- a/src/commands/issue/utils.ts +++ b/src/commands/issue/utils.ts @@ -9,6 +9,7 @@ import { getAutofixState, getIssue, getIssueByShortId, + triggerRootCauseAnalysis, } from "../../lib/api-client.js"; import { parseIssueArg } from "../../lib/arg-parsing.js"; import { getProjectByAlias } from "../../lib/db/project-aliases.js"; @@ -340,6 +341,74 @@ type PollAutofixOptions = { stopOnWaitingForUser?: boolean; }; +type EnsureRootCauseOptions = { + /** Organization slug */ + org: string; + /** Numeric issue ID */ + issueId: string; + /** Writer for progress output */ + stderr: Writer; + /** Whether to suppress progress output (JSON mode) */ + json: boolean; + /** Force new analysis even if one exists */ + force?: boolean; +}; + +/** + * Ensure root cause analysis exists for an issue. + * + * If no analysis exists (or force is true), triggers a new analysis. + * If analysis is in progress, waits for it to complete. + * If analysis failed (ERROR status), retries automatically. + * + * @param options - Configuration options + * @returns The completed autofix state with root causes + */ +export async function ensureRootCauseAnalysis( + options: EnsureRootCauseOptions +): Promise { + const { org, issueId, stderr, json, force = false } = options; + + // 1. Check for existing analysis (skip if --force) + let state = force ? null : await getAutofixState(org, issueId); + + // Handle error status - we will retry the analysis + if (state?.status === "ERROR") { + if (!json) { + stderr.write("Previous analysis failed, retrying...\n"); + } + state = null; + } + + // 2. Trigger new analysis if none exists or forced + if (!state) { + if (!json) { + const prefix = force ? "Forcing fresh" : "Starting"; + stderr.write( + `${prefix} root cause analysis, it can take several minutes...\n` + ); + } + await triggerRootCauseAnalysis(org, issueId); + } + + // 3. Poll until complete (if not already completed) + if ( + !state || + (state.status !== "COMPLETED" && + state.status !== "WAITING_FOR_USER_RESPONSE") + ) { + state = await pollAutofixState({ + orgSlug: org, + issueId, + stderr, + json, + stopOnWaitingForUser: true, + }); + } + + return state; +} + /** * Check if polling should stop based on current state. * From a7e3c1e208e1d8894cfbd752b33955b707ea07e3 Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Thu, 5 Feb 2026 14:30:45 +0530 Subject: [PATCH 2/5] test: added the test for that new function --- test/commands/issue/utils.test.ts | 373 ++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) diff --git a/test/commands/issue/utils.test.ts b/test/commands/issue/utils.test.ts index 09cd5eff..27214f3d 100644 --- a/test/commands/issue/utils.test.ts +++ b/test/commands/issue/utils.test.ts @@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { buildCommandHint, + ensureRootCauseAnalysis, pollAutofixState, resolveOrgAndIssueId, } from "../../../src/commands/issue/utils.js"; @@ -845,3 +846,375 @@ describe("pollAutofixState", () => { expect(fetchCount).toBe(2); }); }); + +describe("ensureRootCauseAnalysis", () => { + const mockStderr = { + write: () => { + // Intentionally empty - suppress output in tests + }, + }; + + test("returns immediately when state is COMPLETED", async () => { + let fetchCount = 0; + + // @ts-expect-error - partial mock + globalThis.fetch = async () => { + fetchCount += 1; + return new Response( + JSON.stringify({ + autofix: { + run_id: 12_345, + status: "COMPLETED", + steps: [], + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + }; + + const result = await ensureRootCauseAnalysis({ + org: "test-org", + issueId: "123456789", + stderr: mockStderr, + json: true, + }); + + expect(result.status).toBe("COMPLETED"); + expect(fetchCount).toBe(1); // Only one fetch to check state + }); + + test("returns immediately when state is WAITING_FOR_USER_RESPONSE", async () => { + let fetchCount = 0; + + // @ts-expect-error - partial mock + globalThis.fetch = async () => { + fetchCount += 1; + return new Response( + JSON.stringify({ + autofix: { + run_id: 12_345, + status: "WAITING_FOR_USER_RESPONSE", + steps: [], + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + }; + + const result = await ensureRootCauseAnalysis({ + org: "test-org", + issueId: "123456789", + stderr: mockStderr, + json: true, + }); + + expect(result.status).toBe("WAITING_FOR_USER_RESPONSE"); + expect(fetchCount).toBe(1); + }); + + test("triggers new analysis when no state exists", async () => { + let fetchCount = 0; + let triggerCalled = false; + + // @ts-expect-error - partial mock + globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const req = new Request(input, init); + const url = req.url; + + fetchCount += 1; + + // First call: getAutofixState returns null + if (url.includes("/autofix/") && req.method === "GET") { + // After trigger, return COMPLETED + if (triggerCalled) { + return new Response( + JSON.stringify({ + autofix: { + run_id: 12_345, + status: "COMPLETED", + steps: [], + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + // Before trigger, return null + return new Response(JSON.stringify({ autofix: null }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + // Trigger RCA endpoint + if (url.includes("/autofix/") && req.method === "POST") { + triggerCalled = true; + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify({ detail: "Not found" }), { + status: 404, + }); + }; + + const result = await ensureRootCauseAnalysis({ + org: "test-org", + issueId: "123456789", + stderr: mockStderr, + json: true, + }); + + expect(result.status).toBe("COMPLETED"); + expect(triggerCalled).toBe(true); + }); + + test("retries when existing analysis has ERROR status", async () => { + let fetchCount = 0; + let triggerCalled = false; + + // @ts-expect-error - partial mock + globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const req = new Request(input, init); + const url = req.url; + + fetchCount += 1; + + // getAutofixState + if (url.includes("/autofix/") && req.method === "GET") { + // First call returns ERROR, subsequent calls return COMPLETED + if (!triggerCalled) { + return new Response( + JSON.stringify({ + autofix: { + run_id: 12_345, + status: "ERROR", + steps: [], + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + return new Response( + JSON.stringify({ + autofix: { + run_id: 12_346, + status: "COMPLETED", + steps: [], + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Trigger RCA endpoint + if (url.includes("/autofix/") && req.method === "POST") { + triggerCalled = true; + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify({ detail: "Not found" }), { + status: 404, + }); + }; + + const result = await ensureRootCauseAnalysis({ + org: "test-org", + issueId: "123456789", + stderr: mockStderr, + json: true, + }); + + expect(result.status).toBe("COMPLETED"); + expect(triggerCalled).toBe(true); // Should have retried + }); + + test("polls until complete when state is PROCESSING", async () => { + let fetchCount = 0; + + // @ts-expect-error - partial mock + globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const req = new Request(input, init); + + if (req.method === "GET") { + fetchCount += 1; + + // First call returns PROCESSING, second returns COMPLETED + if (fetchCount === 1) { + return new Response( + JSON.stringify({ + autofix: { + run_id: 12_345, + status: "PROCESSING", + steps: [], + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + return new Response( + JSON.stringify({ + autofix: { + run_id: 12_345, + status: "COMPLETED", + steps: [], + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + return new Response(JSON.stringify({ detail: "Not found" }), { + status: 404, + }); + }; + + const result = await ensureRootCauseAnalysis({ + org: "test-org", + issueId: "123456789", + stderr: mockStderr, + json: true, + }); + + expect(result.status).toBe("COMPLETED"); + expect(fetchCount).toBeGreaterThan(1); // Polled multiple times + }); + + test("forces new analysis when force flag is true", async () => { + let triggerCalled = false; + + // @ts-expect-error - partial mock + globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const req = new Request(input, init); + const url = req.url; + + // getAutofixState - would return COMPLETED, but force should skip this + if (url.includes("/autofix/") && req.method === "GET") { + // After trigger, return new COMPLETED state + return new Response( + JSON.stringify({ + autofix: { + run_id: triggerCalled ? 99_999 : 12_345, + status: "COMPLETED", + steps: [], + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + // Trigger RCA endpoint + if (url.includes("/autofix/") && req.method === "POST") { + triggerCalled = true; + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify({ detail: "Not found" }), { + status: 404, + }); + }; + + const result = await ensureRootCauseAnalysis({ + org: "test-org", + issueId: "123456789", + stderr: mockStderr, + json: true, + force: true, + }); + + expect(result.status).toBe("COMPLETED"); + expect(triggerCalled).toBe(true); // Should trigger even though state exists + }); + + test("writes progress messages to stderr when not in JSON mode", async () => { + let stderrOutput = ""; + let triggerCalled = false; + + // @ts-expect-error - partial mock + globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const req = new Request(input, init); + const url = req.url; + + if (url.includes("/autofix/") && req.method === "GET") { + if (triggerCalled) { + return new Response( + JSON.stringify({ + autofix: { + run_id: 12_345, + status: "COMPLETED", + steps: [], + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + return new Response(JSON.stringify({ autofix: null }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + if (url.includes("/autofix/") && req.method === "POST") { + triggerCalled = true; + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify({ detail: "Not found" }), { + status: 404, + }); + }; + + const stderrMock = { + write: (s: string) => { + stderrOutput += s; + }, + }; + + await ensureRootCauseAnalysis({ + org: "test-org", + issueId: "123456789", + stderr: stderrMock, + json: false, // Not JSON mode, should output progress + }); + + expect(stderrOutput).toContain("root cause analysis"); + }); +}); From c429b0849732bc19b9d10d09ee60343a2eba6b4a Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Thu, 5 Feb 2026 14:39:28 +0530 Subject: [PATCH 3/5] chore: minor change --- test/commands/issue/utils.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/commands/issue/utils.test.ts b/test/commands/issue/utils.test.ts index 27214f3d..ad3ac173 100644 --- a/test/commands/issue/utils.test.ts +++ b/test/commands/issue/utils.test.ts @@ -919,7 +919,6 @@ describe("ensureRootCauseAnalysis", () => { }); test("triggers new analysis when no state exists", async () => { - let fetchCount = 0; let triggerCalled = false; // @ts-expect-error - partial mock @@ -927,8 +926,6 @@ describe("ensureRootCauseAnalysis", () => { const req = new Request(input, init); const url = req.url; - fetchCount += 1; - // First call: getAutofixState returns null if (url.includes("/autofix/") && req.method === "GET") { // After trigger, return COMPLETED @@ -980,7 +977,6 @@ describe("ensureRootCauseAnalysis", () => { }); test("retries when existing analysis has ERROR status", async () => { - let fetchCount = 0; let triggerCalled = false; // @ts-expect-error - partial mock @@ -988,8 +984,6 @@ describe("ensureRootCauseAnalysis", () => { const req = new Request(input, init); const url = req.url; - fetchCount += 1; - // getAutofixState if (url.includes("/autofix/") && req.method === "GET") { // First call returns ERROR, subsequent calls return COMPLETED From 9ab286e97100186d40c734cd5e14d5a6cfa4e12f Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Thu, 5 Feb 2026 21:22:42 +0530 Subject: [PATCH 4/5] fix: checking for solution before requesting another one --- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 1 + src/commands/issue/plan.ts | 91 ++++++++++++++----- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index f527fdda..8757919d 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -256,6 +256,7 @@ Generate a solution plan using Seer AI **Flags:** - `--cause - Root cause ID to plan (required if multiple causes exist)` - `--json - Output as JSON` +- `--force - Force new plan even if one exists` **Examples:** diff --git a/src/commands/issue/plan.ts b/src/commands/issue/plan.ts index d917b60a..0e11ef63 100644 --- a/src/commands/issue/plan.ts +++ b/src/commands/issue/plan.ts @@ -15,11 +15,13 @@ import { formatSolution, handleSeerApiError, } from "../../lib/formatters/seer.js"; +import type { Writer } from "../../types/index.js"; import { type AutofixState, extractRootCauses, extractSolution, type RootCause, + type SolutionArtifact, } from "../../types/seer.js"; import { ensureRootCauseAnalysis, @@ -31,6 +33,7 @@ import { type PlanFlags = { readonly cause?: number; readonly json: boolean; + readonly force: boolean; }; /** @@ -98,6 +101,37 @@ function validateCauseSelection( return causeId; } +type OutputSolutionOptions = { + stdout: Writer; + stderr: Writer; + solution: SolutionArtifact | null; + state: AutofixState; + json: boolean; +}; + +/** + * Output a solution artifact to stdout. + */ +function outputSolution(options: OutputSolutionOptions): void { + const { stdout, stderr, solution, state, json } = options; + + if (json) { + writeJson(stdout, { + run_id: state.run_id, + status: state.status, + solution: solution?.data ?? null, + }); + return; + } + + if (solution) { + const lines = formatSolution(solution); + stdout.write(`${lines.join("\n")}\n`); + } else { + stderr.write("No solution found. Check the Sentry web UI for details.\n"); + } +} + export const planCommand = buildCommand({ docs: { brief: "Generate a solution plan using Seer AI", @@ -105,7 +139,8 @@ export const planCommand = buildCommand({ "Generate a solution plan for a Sentry issue using Seer AI.\n\n" + "This command automatically runs root cause analysis if needed, then " + "generates a solution plan with specific implementation steps to fix the issue.\n\n" + - "If multiple root causes are identified, use --cause to specify which one.\n\n" + + "If multiple root causes are identified, use --cause to specify which one.\n" + + "Use --force to regenerate a plan even if one already exists.\n\n" + "Issue formats:\n" + " /ID - Explicit org: sentry/EXTENSION-7, sentry/cli-G\n" + " -suffix - Project + suffix: cli-G, spotlight-electron-4Y\n" + @@ -118,7 +153,8 @@ export const planCommand = buildCommand({ "Examples:\n" + " sentry issue plan 123456789 --cause 0\n" + " sentry issue plan sentry/EXTENSION-7 --cause 1\n" + - " sentry issue plan cli-G --cause 0", + " sentry issue plan cli-G --cause 0\n" + + " sentry issue plan 123456789 --force", }, parameters: { positional: issueIdPositional, @@ -134,6 +170,11 @@ export const planCommand = buildCommand({ brief: "Output as JSON", default: false, }, + force: { + kind: "boolean", + brief: "Force new plan even if one exists", + default: false, + }, }, }, async func( @@ -170,6 +211,22 @@ export const planCommand = buildCommand({ const causeId = validateCauseSelection(causes, flags.cause, issueArg); const selectedCause = causes[causeId]; + // Check if solution already exists (skip if --force) + if (!flags.force) { + const existingSolution = extractSolution(state); + if (existingSolution) { + outputSolution({ + stdout, + stderr, + solution: existingSolution, + state, + json: flags.json, + }); + return; + } + } + + // No solution exists, trigger planning if (!flags.json) { stderr.write(`Creating plan for cause #${causeId}...\n`); if (selectedCause) { @@ -177,7 +234,6 @@ export const planCommand = buildCommand({ } } - // Trigger solution planning to generate implementation steps await triggerSolutionPlanning(org, numericId, state.run_id); // Poll until PR is created @@ -201,28 +257,15 @@ export const planCommand = buildCommand({ throw new Error("Plan creation was cancelled."); } - // Extract solution artifact + // Extract and output solution const solution = extractSolution(finalState); - - // Output results - if (flags.json) { - writeJson(stdout, { - run_id: finalState.run_id, - status: finalState.status, - solution: solution?.data ?? null, - }); - return; - } - - // Human-readable output - if (solution) { - const lines = formatSolution(solution); - stdout.write(`${lines.join("\n")}\n`); - } else { - stderr.write( - "No solution found. Check the Sentry web UI for details.\n" - ); - } + outputSolution({ + stdout, + stderr, + solution, + state: finalState, + json: flags.json, + }); } catch (error) { // Handle API errors with friendly messages if (error instanceof ApiError) { From a16d7722006b5a76710dc47915afee32611f13c0 Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Thu, 5 Feb 2026 21:31:44 +0530 Subject: [PATCH 5/5] fix: update timeout duration in issue planning and polling functions from 3 minutes to 6 minutes --- src/commands/issue/plan.ts | 2 +- src/commands/issue/utils.ts | 8 ++++---- src/lib/polling.ts | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/commands/issue/plan.ts b/src/commands/issue/plan.ts index 0e11ef63..e251a32b 100644 --- a/src/commands/issue/plan.ts +++ b/src/commands/issue/plan.ts @@ -243,7 +243,7 @@ export const planCommand = buildCommand({ stderr, json: flags.json, timeoutMessage: - "Plan creation timed out after 3 minutes. Try again or check the issue in Sentry web UI.", + "Plan creation timed out after 6 minutes. Try again or check the issue in Sentry web UI.", }); // Handle errors diff --git a/src/commands/issue/utils.ts b/src/commands/issue/utils.ts index aea80e1a..7678477f 100644 --- a/src/commands/issue/utils.ts +++ b/src/commands/issue/utils.ts @@ -63,8 +63,8 @@ export function buildCommandHint(command: string, issueId: string): string { return `sentry issue ${command} /${issueId}`; } -/** Default timeout in milliseconds (3 minutes) */ -const DEFAULT_TIMEOUT_MS = 180_000; +/** Default timeout in milliseconds (6 minutes) */ +const DEFAULT_TIMEOUT_MS = 360_000; /** * Result of resolving an issue ID - includes full issue object. @@ -333,7 +333,7 @@ type PollAutofixOptions = { json: boolean; /** Polling interval in milliseconds (default: 1000) */ pollIntervalMs?: number; - /** Maximum time to wait in milliseconds (default: 180000 = 3 minutes) */ + /** Maximum time to wait in milliseconds (default: 360000 = 6 minutes) */ timeoutMs?: number; /** Custom timeout error message */ timeoutMessage?: string; @@ -447,7 +447,7 @@ export async function pollAutofixState( json, pollIntervalMs, timeoutMs = DEFAULT_TIMEOUT_MS, - timeoutMessage = "Operation timed out after 3 minutes. Try again or check the issue in Sentry web UI.", + timeoutMessage = "Operation timed out after 6 minutes. Try again or check the issue in Sentry web UI.", stopOnWaitingForUser = false, } = options; diff --git a/src/lib/polling.ts b/src/lib/polling.ts index 50926297..9bc6e6b4 100644 --- a/src/lib/polling.ts +++ b/src/lib/polling.ts @@ -17,8 +17,8 @@ const DEFAULT_POLL_INTERVAL_MS = 1000; /** Animation interval for spinner updates (independent of polling) */ const ANIMATION_INTERVAL_MS = 80; -/** Default timeout in milliseconds (3 minutes) */ -const DEFAULT_TIMEOUT_MS = 180_000; +/** Default timeout in milliseconds (6 minutes) */ +const DEFAULT_TIMEOUT_MS = 360_000; /** * Options for the generic poll function. @@ -36,7 +36,7 @@ export type PollOptions = { json?: boolean; /** Poll interval in ms (default: 1000) */ pollIntervalMs?: number; - /** Timeout in ms (default: 180000 / 3 min) */ + /** Timeout in ms (default: 360000 / 6 min) */ timeoutMs?: number; /** Custom timeout message */ timeoutMessage?: string; @@ -64,8 +64,8 @@ export type PollOptions = { * getProgressMessage: (state) => state.message ?? "Processing...", * stderr: process.stderr, * json: false, - * timeoutMs: 180_000, - * timeoutMessage: "Operation timed out after 3 minutes.", + * timeoutMs: 360_000, + * timeoutMessage: "Operation timed out after 6 minutes.", * }); * ``` */ @@ -78,7 +78,7 @@ export async function poll(options: PollOptions): Promise { json = false, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, timeoutMs = DEFAULT_TIMEOUT_MS, - timeoutMessage = "Operation timed out after 3 minutes. Try again or check the Sentry web UI.", + timeoutMessage = "Operation timed out after 6 minutes. Try again or check the Sentry web UI.", initialMessage = "Waiting for operation to start...", } = options;