From 169c8e2f30f216619a686ec60c4028a7f661f58d Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 10 Jul 2026 17:32:09 -0400 Subject: [PATCH 1/5] fix(seer): Read sentry_run_id instead of the deprecated run_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The autofix API is migrating run identifiers from a legacy numeric run_id to a UUID sentry_run_id, and will eventually drop run_id entirely. The CLI was reading run_id directly off autofix state and forwarding it to the continue-run endpoint, which would silently break once the numeric field disappears. Add getAutofixRunId() to prefer sentry_run_id (falling back to run_id for older responses), and use it everywhere the CLI reads a run identifier. The continue-run request now sends the ID under whichever body field matches its type — sentry_run_id (UUID field) or run_id (integer field) — since the server validates them as distinct, differently-typed fields and a UUID under run_id would be rejected. Verified against the live API: GET/POST responses return both fields as documented, and a real continue-run POST with a UUID sentry_run_id returns 202. Co-Authored-By: Claude Sonnet 5 --- src/commands/issue/plan.ts | 14 ++++++-- src/lib/api/seer.ts | 32 ++++++++++++----- src/types/seer.ts | 23 +++++++++++- test/lib/api-client.seer.test.ts | 60 ++++++++++++++++++++++++++++++-- test/types/seer.test.ts | 22 ++++++++++++ 5 files changed, 137 insertions(+), 14 deletions(-) diff --git a/src/commands/issue/plan.ts b/src/commands/issue/plan.ts index 7ce8959f6..df82d32ea 100644 --- a/src/commands/issue/plan.ts +++ b/src/commands/issue/plan.ts @@ -26,6 +26,7 @@ import { extractNoSolutionReason, extractRootCauses, extractSolution, + getAutofixRunId, type SolutionArtifact, } from "../../types/seer.js"; import { @@ -54,7 +55,8 @@ type NoSolutionContext = { /** Return type for issue plan — includes state metadata and solution data */ type PlanData = { - run_id: number; + /** The autofix run ID (UUID from state.sentry_run_id, falling back to the legacy numeric state.run_id). */ + run_id: string | number | undefined; status: string; /** The solution data (without the artifact wrapper). Null when no solution is available. */ solution: SolutionArtifact["data"] | null; @@ -141,7 +143,7 @@ function buildNoSolutionContext( function buildPlanData(state: AutofixState): PlanData { const solution = extractSolution(state); const data: PlanData = { - run_id: state.run_id, + run_id: getAutofixRunId(state), status: state.status, solution: solution?.data ?? null, }; @@ -235,7 +237,13 @@ export const planCommand = buildCommand({ } } - await triggerSolutionPlanning(org, numericId, state.run_id); + const runId = getAutofixRunId(state); + if (runId === undefined) { + throw new Error( + "Autofix state is missing a run ID. Check the issue in Sentry web UI." + ); + } + await triggerSolutionPlanning(org, numericId, runId); // Poll until solution is ready or terminal const finalState = await pollAutofixState({ diff --git a/src/lib/api/seer.ts b/src/lib/api/seer.ts index d0437f473..3063e65f3 100644 --- a/src/lib/api/seer.ts +++ b/src/lib/api/seer.ts @@ -54,16 +54,21 @@ function normalizeAgentStatus(status: string): string { * * @param orgSlug - The organization slug * @param issueId - The numeric Sentry issue ID - * @returns The trigger response with run_id + * @returns The trigger response with `sentry_run_id` (current UUID, null for + * very old runs) and the legacy `run_id` (numeric), which is slated for + * removal — treat it as optional, not guaranteed to be present * @throws {ApiError} On API errors (402 = no budget, 403 = not enabled) */ export async function triggerRootCauseAnalysis( orgSlug: string, issueId: string -): Promise<{ run_id: number }> { +): Promise<{ run_id?: number; sentry_run_id: string | null }> { const regionUrl = await resolveOrgRegion(orgSlug); - const { data } = await apiRequestToRegion<{ run_id: number }>( + const { data } = await apiRequestToRegion<{ + run_id?: number; + sentry_run_id: string | null; + }>( regionUrl, `/organizations/${orgSlug}/issues/${issueId}/autofix/`, { @@ -113,21 +118,32 @@ export async function getAutofixState( * Trigger solution planning for an existing autofix run. * * Posts to the agent-based autofix endpoint with `step: "solution"` and - * the existing `run_id`. The agent continues from root cause analysis - * to generating a solution plan. + * the existing run ID (from {@link getAutofixRunId}). The agent continues + * from root cause analysis to generating a solution plan. + * + * The request body has two distinct, separately-validated fields: `run_id` + * (an integer field, deprecated) and `sentry_run_id` (a UUID field, takes + * precedence when both are given). A UUID string sent under `run_id` fails + * server-side validation, so the value must go under the field matching its + * type — string runIds (current `sentry_run_id`s) go under `sentry_run_id`, + * numeric runIds (legacy `run_id`s) go under `run_id`. * * @param orgSlug - The organization slug * @param issueId - The numeric Sentry issue ID - * @param runId - The autofix run ID + * @param runId - The autofix run ID (see {@link getAutofixRunId}) — a UUID + * string for current runs, or a legacy number for older ones * @returns The response from the API */ export async function triggerSolutionPlanning( orgSlug: string, issueId: string, - runId: number + runId: string | number ): Promise { const regionUrl = await resolveOrgRegion(orgSlug); + const runIdBodyField = + typeof runId === "string" ? { sentry_run_id: runId } : { run_id: runId }; + const { data } = await apiRequestToRegion( regionUrl, `/organizations/${orgSlug}/issues/${issueId}/autofix/`, @@ -136,7 +152,7 @@ export async function triggerSolutionPlanning( params: EXPLORER_MODE_PARAMS, body: { step: "solution", - run_id: runId, + ...runIdBodyField, referrer: "api.cli", }, } diff --git a/src/types/seer.ts b/src/types/seer.ts index d7803714d..faac8d26f 100644 --- a/src/types/seer.ts +++ b/src/types/seer.ts @@ -172,7 +172,14 @@ export type SolutionArtifact = z.infer; export const AutofixStateSchema = z .object({ - run_id: z.number(), + /** Legacy numeric run identifier. Deprecated in favor of {@link sentry_run_id}, kept for older API responses. */ + run_id: z.number().optional(), + /** + * Current run identifier (UUID string). Preferred over the legacy `run_id` + * field. The API returns this as explicit `null` (not an omitted key) for + * legacy runs predating SeerRun mirroring. + */ + sentry_run_id: z.string().nullable().optional(), status: z.string(), updated_at: z.string().optional(), request: z @@ -242,6 +249,20 @@ export function isTerminalStatus(status: string): boolean { return TERMINAL_STATUSES.includes(status as AutofixStatus); } +/** + * Get the run identifier from autofix state, preferring the current + * `sentry_run_id` field (a UUID string) over the deprecated numeric + * `run_id` field. + * + * @param state - The autofix state + * @returns The run ID, or undefined if neither field is present + */ +export function getAutofixRunId( + state: AutofixState +): string | number | undefined { + return state.sentry_run_id ?? state.run_id; +} + /** Container that may hold root cause analysis data (legacy format) */ type WithCauses = { key: string; causes?: RootCause[] }; diff --git a/test/lib/api-client.seer.test.ts b/test/lib/api-client.seer.test.ts index afcbcfe3f..ba5ade22b 100644 --- a/test/lib/api-client.seer.test.ts +++ b/test/lib/api-client.seer.test.ts @@ -110,7 +110,7 @@ describe("getAutofixState", () => { return new Response( JSON.stringify({ autofix: { - run_id: 12_345, + sentry_run_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", status: "processing", blocks: [], updated_at: "2025-01-01T00:00:00Z", @@ -125,7 +125,7 @@ describe("getAutofixState", () => { const result = await getAutofixState("test-org", "123456789"); - expect(result?.run_id).toBe(12_345); + expect(result?.sentry_run_id).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); expect(result?.status).toBe("PROCESSING"); expect(capturedRequest?.method).toBe("GET"); expect(capturedRequest?.url).toContain( @@ -134,6 +134,29 @@ describe("getAutofixState", () => { expect(capturedRequest?.url).toContain("mode=explorer"); }); + test("still parses legacy run_id when sentry_run_id is absent", async () => { + globalThis.fetch = async () => + new Response( + JSON.stringify({ + autofix: { + run_id: 12_345, + status: "processing", + blocks: [], + updated_at: "2025-01-01T00:00:00Z", + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + + const result = await getAutofixState("test-org", "123456789"); + + expect(result?.run_id).toBe(12_345); + expect(result?.sentry_run_id).toBeUndefined(); + }); + test("normalizes agent status values to uppercase", async () => { globalThis.fetch = async () => new Response( @@ -274,4 +297,37 @@ describe("triggerSolutionPlanning", () => { referrer: "api.cli", }); }); + + test("sends a UUID run ID under the sentry_run_id body field, not run_id", async () => { + let capturedBody: unknown; + + globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + capturedBody = await new Request(input, init).json(); + + return new Response( + JSON.stringify({ + run_id: 12_345, + sentry_run_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + }), + { + status: 202, + headers: { "Content-Type": "application/json" }, + } + ); + }; + + await triggerSolutionPlanning( + "test-org", + "123456789", + "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + ); + + // sentry_run_id is a UUIDField server-side; run_id is an IntegerField. + // Sending the UUID under run_id would fail server-side validation. + expect(capturedBody).toEqual({ + step: "solution", + sentry_run_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + referrer: "api.cli", + }); + }); }); diff --git a/test/types/seer.test.ts b/test/types/seer.test.ts index f202b4145..45fe3947e 100644 --- a/test/types/seer.test.ts +++ b/test/types/seer.test.ts @@ -11,6 +11,7 @@ import { extractNoSolutionReason, extractRootCauses, extractSolution, + getAutofixRunId, isTerminalStatus, type RootCause, TERMINAL_STATUSES, @@ -49,6 +50,27 @@ describe("isTerminalStatus", () => { }); }); +describe("getAutofixRunId", () => { + test("prefers sentry_run_id over legacy run_id", () => { + const state: AutofixState = { + sentry_run_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + run_id: 123, + status: "COMPLETED", + }; + expect(getAutofixRunId(state)).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + }); + + test("falls back to legacy run_id when sentry_run_id is absent", () => { + const state: AutofixState = { run_id: 123, status: "COMPLETED" }; + expect(getAutofixRunId(state)).toBe(123); + }); + + test("returns undefined when neither field is present", () => { + const state: AutofixState = { status: "COMPLETED" }; + expect(getAutofixRunId(state)).toBeUndefined(); + }); +}); + describe("extractRootCauses", () => { test("extracts causes from root_cause_analysis step", () => { const state: AutofixState = { From e9665a71e95d9335eaef0754f3bd6624b0973014 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 10 Jul 2026 17:46:38 -0400 Subject: [PATCH 2/5] fix(seer): Make requireAutofixRunId throw instead of returning undefined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getAutofixRunId returned undefined when neither run_id nor sentry_run_id was present, and issue plan's buildPlanData let that flow straight into the CLI's JSON output as run_id: undefined. The API dual-writes both fields on every non-null autofix state, so a missing run ID there is a malformed response, not a legitimate in-progress state — it shouldn't be passed through silently. Renamed to requireAutofixRunId and made it throw, so both call sites (the plan output and the continue-run request) treat a missing run ID as the same invariant violation instead of one silently swallowing it. Co-Authored-By: Claude Sonnet 5 --- src/commands/issue/plan.ts | 14 ++++---------- src/lib/api/seer.ts | 24 ++++++++++-------------- src/types/seer.ts | 19 ++++++++++++++----- test/types/seer.test.ts | 14 ++++++++------ 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/commands/issue/plan.ts b/src/commands/issue/plan.ts index df82d32ea..598352a06 100644 --- a/src/commands/issue/plan.ts +++ b/src/commands/issue/plan.ts @@ -26,7 +26,7 @@ import { extractNoSolutionReason, extractRootCauses, extractSolution, - getAutofixRunId, + requireAutofixRunId, type SolutionArtifact, } from "../../types/seer.js"; import { @@ -56,7 +56,7 @@ type NoSolutionContext = { /** Return type for issue plan — includes state metadata and solution data */ type PlanData = { /** The autofix run ID (UUID from state.sentry_run_id, falling back to the legacy numeric state.run_id). */ - run_id: string | number | undefined; + run_id: string | number; status: string; /** The solution data (without the artifact wrapper). Null when no solution is available. */ solution: SolutionArtifact["data"] | null; @@ -143,7 +143,7 @@ function buildNoSolutionContext( function buildPlanData(state: AutofixState): PlanData { const solution = extractSolution(state); const data: PlanData = { - run_id: getAutofixRunId(state), + run_id: requireAutofixRunId(state), status: state.status, solution: solution?.data ?? null, }; @@ -237,13 +237,7 @@ export const planCommand = buildCommand({ } } - const runId = getAutofixRunId(state); - if (runId === undefined) { - throw new Error( - "Autofix state is missing a run ID. Check the issue in Sentry web UI." - ); - } - await triggerSolutionPlanning(org, numericId, runId); + await triggerSolutionPlanning(org, numericId, requireAutofixRunId(state)); // Poll until solution is ready or terminal const finalState = await pollAutofixState({ diff --git a/src/lib/api/seer.ts b/src/lib/api/seer.ts index 3063e65f3..79c11ff44 100644 --- a/src/lib/api/seer.ts +++ b/src/lib/api/seer.ts @@ -68,18 +68,14 @@ export async function triggerRootCauseAnalysis( const { data } = await apiRequestToRegion<{ run_id?: number; sentry_run_id: string | null; - }>( - regionUrl, - `/organizations/${orgSlug}/issues/${issueId}/autofix/`, - { - method: "POST", - params: EXPLORER_MODE_PARAMS, - body: { - step: "root_cause", - referrer: "api.cli", - }, - } - ); + }>(regionUrl, `/organizations/${orgSlug}/issues/${issueId}/autofix/`, { + method: "POST", + params: EXPLORER_MODE_PARAMS, + body: { + step: "root_cause", + referrer: "api.cli", + }, + }); return data; } @@ -118,7 +114,7 @@ export async function getAutofixState( * Trigger solution planning for an existing autofix run. * * Posts to the agent-based autofix endpoint with `step: "solution"` and - * the existing run ID (from {@link getAutofixRunId}). The agent continues + * the existing run ID (from {@link requireAutofixRunId}). The agent continues * from root cause analysis to generating a solution plan. * * The request body has two distinct, separately-validated fields: `run_id` @@ -130,7 +126,7 @@ export async function getAutofixState( * * @param orgSlug - The organization slug * @param issueId - The numeric Sentry issue ID - * @param runId - The autofix run ID (see {@link getAutofixRunId}) — a UUID + * @param runId - The autofix run ID (see {@link requireAutofixRunId}) — a UUID * string for current runs, or a legacy number for older ones * @returns The response from the API */ diff --git a/src/types/seer.ts b/src/types/seer.ts index faac8d26f..4b7132d4e 100644 --- a/src/types/seer.ts +++ b/src/types/seer.ts @@ -254,13 +254,22 @@ export function isTerminalStatus(status: string): boolean { * `sentry_run_id` field (a UUID string) over the deprecated numeric * `run_id` field. * + * The API dual-writes both fields on every non-null autofix state, so a + * missing run ID here means the response is malformed, not a legitimate + * in-progress state — callers should treat it as an error, not a value to + * pass through. + * * @param state - The autofix state - * @returns The run ID, or undefined if neither field is present + * @throws {Error} If neither `sentry_run_id` nor `run_id` is present */ -export function getAutofixRunId( - state: AutofixState -): string | number | undefined { - return state.sentry_run_id ?? state.run_id; +export function requireAutofixRunId(state: AutofixState): string | number { + const runId = state.sentry_run_id ?? state.run_id; + if (runId === undefined) { + throw new Error( + "Autofix state is missing a run ID (no sentry_run_id or run_id). Check the issue in Sentry web UI." + ); + } + return runId; } /** Container that may hold root cause analysis data (legacy format) */ diff --git a/test/types/seer.test.ts b/test/types/seer.test.ts index 45fe3947e..afe0a663c 100644 --- a/test/types/seer.test.ts +++ b/test/types/seer.test.ts @@ -11,9 +11,9 @@ import { extractNoSolutionReason, extractRootCauses, extractSolution, - getAutofixRunId, isTerminalStatus, type RootCause, + requireAutofixRunId, TERMINAL_STATUSES, } from "../../src/types/seer.js"; @@ -50,24 +50,26 @@ describe("isTerminalStatus", () => { }); }); -describe("getAutofixRunId", () => { +describe("requireAutofixRunId", () => { test("prefers sentry_run_id over legacy run_id", () => { const state: AutofixState = { sentry_run_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", run_id: 123, status: "COMPLETED", }; - expect(getAutofixRunId(state)).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + expect(requireAutofixRunId(state)).toBe( + "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + ); }); test("falls back to legacy run_id when sentry_run_id is absent", () => { const state: AutofixState = { run_id: 123, status: "COMPLETED" }; - expect(getAutofixRunId(state)).toBe(123); + expect(requireAutofixRunId(state)).toBe(123); }); - test("returns undefined when neither field is present", () => { + test("throws when neither field is present", () => { const state: AutofixState = { status: "COMPLETED" }; - expect(getAutofixRunId(state)).toBeUndefined(); + expect(() => requireAutofixRunId(state)).toThrow(/missing a run ID/); }); }); From 4d59830ebbcc0416959ca60502c0a32d09a07131 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 10 Jul 2026 18:11:56 -0400 Subject: [PATCH 3/5] docs(seer): Fix inaccurate null-sentry_run_id comment triggerRootCauseAnalysis only ever kicks off a brand-new run, so sentry_run_id can't be null for the legacy-run reason that applies to the GET/continue-run paths. Drop the misleading claim. Co-Authored-By: Claude Sonnet 5 --- src/lib/api/seer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/api/seer.ts b/src/lib/api/seer.ts index 79c11ff44..f99f5115b 100644 --- a/src/lib/api/seer.ts +++ b/src/lib/api/seer.ts @@ -54,9 +54,9 @@ function normalizeAgentStatus(status: string): string { * * @param orgSlug - The organization slug * @param issueId - The numeric Sentry issue ID - * @returns The trigger response with `sentry_run_id` (current UUID, null for - * very old runs) and the legacy `run_id` (numeric), which is slated for - * removal — treat it as optional, not guaranteed to be present + * @returns The trigger response with `sentry_run_id` (current UUID) and the + * legacy `run_id` (numeric), which is slated for removal — treat it as + * optional, not guaranteed to be present * @throws {ApiError} On API errors (402 = no budget, 403 = not enabled) */ export async function triggerRootCauseAnalysis( From f312387577cbdffd1a12a52e14e3110fda2a172a Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 10 Jul 2026 18:23:31 -0400 Subject: [PATCH 4/5] style(seer): Trim verbose comments to the non-obvious bits Cut prose that restated what the code/types already say, keeping only the parts that explain non-obvious server behavior (why run_id vs sentry_run_id must go under different body keys, why a missing run ID means malformed state). Co-Authored-By: Claude Sonnet 5 --- src/commands/issue/plan.ts | 1 - src/lib/api/seer.ts | 18 ++++-------------- src/types/seer.ts | 23 ++++------------------- 3 files changed, 8 insertions(+), 34 deletions(-) diff --git a/src/commands/issue/plan.ts b/src/commands/issue/plan.ts index 598352a06..f5595fed0 100644 --- a/src/commands/issue/plan.ts +++ b/src/commands/issue/plan.ts @@ -55,7 +55,6 @@ type NoSolutionContext = { /** Return type for issue plan — includes state metadata and solution data */ type PlanData = { - /** The autofix run ID (UUID from state.sentry_run_id, falling back to the legacy numeric state.run_id). */ run_id: string | number; status: string; /** The solution data (without the artifact wrapper). Null when no solution is available. */ diff --git a/src/lib/api/seer.ts b/src/lib/api/seer.ts index f99f5115b..505ad6a4a 100644 --- a/src/lib/api/seer.ts +++ b/src/lib/api/seer.ts @@ -54,9 +54,7 @@ function normalizeAgentStatus(status: string): string { * * @param orgSlug - The organization slug * @param issueId - The numeric Sentry issue ID - * @returns The trigger response with `sentry_run_id` (current UUID) and the - * legacy `run_id` (numeric), which is slated for removal — treat it as - * optional, not guaranteed to be present + * @returns The trigger response with `sentry_run_id` and the legacy `run_id` * @throws {ApiError} On API errors (402 = no budget, 403 = not enabled) */ export async function triggerRootCauseAnalysis( @@ -114,20 +112,11 @@ export async function getAutofixState( * Trigger solution planning for an existing autofix run. * * Posts to the agent-based autofix endpoint with `step: "solution"` and - * the existing run ID (from {@link requireAutofixRunId}). The agent continues - * from root cause analysis to generating a solution plan. - * - * The request body has two distinct, separately-validated fields: `run_id` - * (an integer field, deprecated) and `sentry_run_id` (a UUID field, takes - * precedence when both are given). A UUID string sent under `run_id` fails - * server-side validation, so the value must go under the field matching its - * type — string runIds (current `sentry_run_id`s) go under `sentry_run_id`, - * numeric runIds (legacy `run_id`s) go under `run_id`. + * the existing run ID (from {@link requireAutofixRunId}). * * @param orgSlug - The organization slug * @param issueId - The numeric Sentry issue ID - * @param runId - The autofix run ID (see {@link requireAutofixRunId}) — a UUID - * string for current runs, or a legacy number for older ones + * @param runId - The autofix run ID (see {@link requireAutofixRunId}) * @returns The response from the API */ export async function triggerSolutionPlanning( @@ -137,6 +126,7 @@ export async function triggerSolutionPlanning( ): Promise { const regionUrl = await resolveOrgRegion(orgSlug); + // A UUID sent under run_id (an IntegerField) fails server-side validation. const runIdBodyField = typeof runId === "string" ? { sentry_run_id: runId } : { run_id: runId }; diff --git a/src/types/seer.ts b/src/types/seer.ts index 4b7132d4e..9d20c3401 100644 --- a/src/types/seer.ts +++ b/src/types/seer.ts @@ -172,13 +172,9 @@ export type SolutionArtifact = z.infer; export const AutofixStateSchema = z .object({ - /** Legacy numeric run identifier. Deprecated in favor of {@link sentry_run_id}, kept for older API responses. */ + /** @deprecated Use sentry_run_id instead. */ run_id: z.number().optional(), - /** - * Current run identifier (UUID string). Preferred over the legacy `run_id` - * field. The API returns this as explicit `null` (not an omitted key) for - * legacy runs predating SeerRun mirroring. - */ + /** Null for legacy runs predating this field. */ sentry_run_id: z.string().nullable().optional(), status: z.string(), updated_at: z.string().optional(), @@ -249,19 +245,8 @@ export function isTerminalStatus(status: string): boolean { return TERMINAL_STATUSES.includes(status as AutofixStatus); } -/** - * Get the run identifier from autofix state, preferring the current - * `sentry_run_id` field (a UUID string) over the deprecated numeric - * `run_id` field. - * - * The API dual-writes both fields on every non-null autofix state, so a - * missing run ID here means the response is malformed, not a legitimate - * in-progress state — callers should treat it as an error, not a value to - * pass through. - * - * @param state - The autofix state - * @throws {Error} If neither `sentry_run_id` nor `run_id` is present - */ +// Both fields are always present on a non-null autofix state, so a missing +// run ID means a malformed response, not an in-progress state. export function requireAutofixRunId(state: AutofixState): string | number { const runId = state.sentry_run_id ?? state.run_id; if (runId === undefined) { From 7835dbb7e8aa82fcde8bd315eef704d4216fb79f Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 10 Jul 2026 18:28:37 -0400 Subject: [PATCH 5/5] fix(seer): Correct requireAutofixRunId invariant and error message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment claimed sentry_run_id is always present, but it's null for legacy runs too — the real guarantee is that at least one of the two fields is present. Also drop the 'check the issue in Sentry web UI' hint from the error: this only fires on a malformed API response, and there's nothing to see in the web UI that explains a missing run ID. Co-Authored-By: Claude Sonnet 5 --- src/types/seer.ts | 6 +++--- test/types/seer.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types/seer.ts b/src/types/seer.ts index 9d20c3401..f83e6ca41 100644 --- a/src/types/seer.ts +++ b/src/types/seer.ts @@ -245,13 +245,13 @@ export function isTerminalStatus(status: string): boolean { return TERMINAL_STATUSES.includes(status as AutofixStatus); } -// Both fields are always present on a non-null autofix state, so a missing -// run ID means a malformed response, not an in-progress state. +// At least one of sentry_run_id/run_id is always present on a non-null +// autofix state, so a missing run ID means a malformed response. export function requireAutofixRunId(state: AutofixState): string | number { const runId = state.sentry_run_id ?? state.run_id; if (runId === undefined) { throw new Error( - "Autofix state is missing a run ID (no sentry_run_id or run_id). Check the issue in Sentry web UI." + "Unexpected autofix response: missing both sentry_run_id and run_id." ); } return runId; diff --git a/test/types/seer.test.ts b/test/types/seer.test.ts index afe0a663c..96db67ec8 100644 --- a/test/types/seer.test.ts +++ b/test/types/seer.test.ts @@ -69,7 +69,7 @@ describe("requireAutofixRunId", () => { test("throws when neither field is present", () => { const state: AutofixState = { status: "COMPLETED" }; - expect(() => requireAutofixRunId(state)).toThrow(/missing a run ID/); + expect(() => requireAutofixRunId(state)).toThrow(/missing both/); }); });