From 53d48023245144f793c8c3a0ccc166d4c0e7687e Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Tue, 10 Nov 2026 18:26:38 -0800 Subject: [PATCH 1/4] feat(autofix): Show why a single run stopped on the issue page A single run that stopped early showed "Seer failed to generate a root cause". The root cause card and sidebar preview now show the saved stop reason and next step instead, and the card shows them below the root cause when both exist. Refs AIML-3818 --- .../autofix/useExplorerAutofix.spec.tsx | 40 +++++++++++++++++++ .../events/autofix/useExplorerAutofix.tsx | 29 +++++++++++++- .../events/autofix/v3/autofixCards.spec.tsx | 22 ++++++++++ .../autofix/v3/autofixPreviews.spec.tsx | 15 +++++++ .../events/autofix/v3/autofixPreviews.tsx | 4 ++ .../events/autofix/v3/rootCauseCard.tsx | 13 ++++++ 6 files changed, 121 insertions(+), 2 deletions(-) diff --git a/static/app/components/events/autofix/useExplorerAutofix.spec.tsx b/static/app/components/events/autofix/useExplorerAutofix.spec.tsx index 12ba47b27518..8fd60c56a7fe 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.spec.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.spec.tsx @@ -13,6 +13,7 @@ import { getAutofixArtifactFromSection, getOrderedAutofixSections, getPollInterval, + getSingleRunStop, hideErroredPrIteration, isCodeChangesArtifact, isCodingAgentsArtifact, @@ -704,6 +705,45 @@ describe('getOrderedAutofixSections', () => { ); }); + it('keeps why the run stopped on the root cause section', () => { + const sections = getOrderedAutofixSections( + makeSingleRun([ + makeBlock({message: {metadata: {step: 'single_run'}}}), + makeBlock({ + artifacts: [ + { + key: 'single_run_stop', + reason: 'stop', + data: {reason: 'R', next_step: 'N'}, + }, + ], + }), + ]) + ); + + expect(getSingleRunStop(sections[0]!)).toEqual({reason: 'R', next_step: 'N'}); + }); + + it('drops the stop once the run has code changes', () => { + const sections = getOrderedAutofixSections( + makeSingleRun([ + makeBlock({message: {metadata: {step: 'single_run'}}}), + makeBlock({ + artifacts: [ + { + key: 'single_run_stop', + reason: 'stop', + data: {reason: 'R', next_step: 'N'}, + }, + ], + merged_file_patches: [makePatch('org/repo', 'a.py')], + }), + ]) + ); + + expect(getSingleRunStop(sections[0]!)).toBeNull(); + }); + it('has only a root cause section without patches', () => { const sections = getOrderedAutofixSections( makeSingleRun([ diff --git a/static/app/components/events/autofix/useExplorerAutofix.tsx b/static/app/components/events/autofix/useExplorerAutofix.tsx index 0efc40b49b02..f5754f1044d4 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.tsx @@ -79,6 +79,26 @@ export function isRootCauseArtifact( ); } +export interface SingleRunStop { + next_step: string; + reason: string; +} + +function isSingleRunStopArtifact(value: unknown): value is Artifact { + if (!isArtifact(value) || value.key !== SINGLE_RUN_STOP_KEY) { + return false; + } + const data = value.data; + if (data === null || typeof data !== 'object') { + return false; + } + return isString(data.reason) && isString(data.next_step); +} + +export function getSingleRunStop(section: AutofixSection): SingleRunStop | null { + return section.artifacts.findLast(isSingleRunStopArtifact)?.data ?? null; +} + export interface SolutionStep { description: string; title: string; @@ -474,6 +494,7 @@ function buildSection( */ export const SINGLE_RUN_FEATURE_ID = 'autofix_single_run'; const SINGLE_RUN_ROOT_CAUSE_KEY = 'single_run_root_cause'; +const SINGLE_RUN_STOP_KEY = 'single_run_stop'; export function isSingleRunAutofix(runState: ExplorerAutofixState | null): boolean { return runState?.feature_id === SINGLE_RUN_FEATURE_ID; @@ -503,12 +524,17 @@ function getSingleRunSections( } ); + const patches = mergeFilePatches(blocks); + const stops = patches.length + ? [] + : collectArtifacts(blocks).filter(isSingleRunStopArtifact); + const sections: AutofixSection[] = [ { index: 0, step: 'root_cause', blocks, - artifacts: rootCauses, + artifacts: [...rootCauses, ...stops], status: rootCauses.length ? 'completed' : runState.status === 'error' @@ -519,7 +545,6 @@ function getSingleRunSections( }, ]; - const patches = mergeFilePatches(blocks); if (patches.length) { sections.push({ index: 0, diff --git a/static/app/components/events/autofix/v3/autofixCards.spec.tsx b/static/app/components/events/autofix/v3/autofixCards.spec.tsx index a6acc909ed43..06564d3b0f73 100644 --- a/static/app/components/events/autofix/v3/autofixCards.spec.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.spec.tsx @@ -419,6 +419,28 @@ describe('ArtifactCard', () => { expect(screen.getByText('Click button')).toBeInTheDocument(); }); + it('shows why a single run stopped instead of the failure text', () => { + const stop = { + key: 'single_run_stop', + reason: 'stop', + data: {reason: 'R', next_step: 'N'}, + }; + + render( + + ); + + expect(screen.getByText('R')).toBeInTheDocument(); + expect(screen.getByText('N')).toBeInTheDocument(); + expect( + screen.queryByText(/Seer failed to generate a root cause/) + ).not.toBeInTheDocument(); + }); + it('renders card shell when artifact data is null', () => { const artifact = makeRootCauseArtifact(null); diff --git a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx index 5f590b720fc5..cefa80454fd5 100644 --- a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx +++ b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx @@ -55,6 +55,21 @@ describe('RootCausePreview', () => { expect(screen.getByText('Finding the root cause…')).toBeInTheDocument(); }); + it('shows why a single run stopped instead of the failure text', () => { + const stop = { + key: 'single_run_stop', + reason: 'stop', + data: {reason: 'R', next_step: 'N'}, + }; + + render(); + + expect(screen.getByText('R')).toBeInTheDocument(); + expect( + screen.queryByText(/Seer failed to generate a root cause/) + ).not.toBeInTheDocument(); + }); + it('handles null data', () => { const artifact: Artifact = { key: 'root-cause', diff --git a/static/app/components/events/autofix/v3/autofixPreviews.tsx b/static/app/components/events/autofix/v3/autofixPreviews.tsx index 62b9e87a5516..4f9e019f4db8 100644 --- a/static/app/components/events/autofix/v3/autofixPreviews.tsx +++ b/static/app/components/events/autofix/v3/autofixPreviews.tsx @@ -21,6 +21,7 @@ import { import { collectPatches, getAutofixArtifactFromSection, + getSingleRunStop, isCodeChangesArtifact, isCodingAgentsArtifact, isPullRequestsArtifact, @@ -42,6 +43,7 @@ export function RootCausePreview({section}: ArtifactPreviewProps) { const sectionArtifact = getAutofixArtifactFromSection(section); return isRootCauseArtifact(sectionArtifact) ? sectionArtifact : null; }, [section]); + const stop = getSingleRunStop(section); return ( } title={t('Root Cause')}> @@ -52,6 +54,8 @@ export function RootCausePreview({section}: ArtifactPreviewProps) { ) : artifact?.data ? ( + ) : stop ? ( + ) : ( {t( diff --git a/static/app/components/events/autofix/v3/rootCauseCard.tsx b/static/app/components/events/autofix/v3/rootCauseCard.tsx index 4a93eb360d8e..98eb63a914c4 100644 --- a/static/app/components/events/autofix/v3/rootCauseCard.tsx +++ b/static/app/components/events/autofix/v3/rootCauseCard.tsx @@ -8,6 +8,7 @@ import {Text} from '@sentry/scraps/text'; import { getAutofixArtifactFromSection, + getSingleRunStop, isRootCauseArtifact, isSingleRunAutofix, type AutofixSection, @@ -40,6 +41,15 @@ export function RootCauseCard({autofix, groupId, section}: RootCauseCardProps) { const sectionArtifact = getAutofixArtifactFromSection(section); return isRootCauseArtifact(sectionArtifact) ? sectionArtifact : null; }, [section]); + const stop = getSingleRunStop(section); + const stopDetails = stop ? ( + + {t('Why Seer stopped')} + + {t('Next step')} + + + ) : null; const {copy} = useCopyToClipboard(); const openSingleRunChat = useOpenSingleRunChat(); @@ -137,7 +147,10 @@ export function RootCauseCard({autofix, groupId, section}: RootCauseCardProps) { )} + {stopDetails} + ) : stopDetails ? ( + stopDetails ) : ( From 95506f2ac4ea6a8d9029dc375742ce5871ab787d Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Wed, 11 Nov 2026 11:48:53 -0800 Subject: [PATCH 2/4] ref(autofix): Read the single-run stop artifact as stop --- .../app/components/events/autofix/useExplorerAutofix.spec.tsx | 4 ++-- static/app/components/events/autofix/useExplorerAutofix.tsx | 2 +- static/app/components/events/autofix/v3/autofixCards.spec.tsx | 2 +- .../app/components/events/autofix/v3/autofixPreviews.spec.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/static/app/components/events/autofix/useExplorerAutofix.spec.tsx b/static/app/components/events/autofix/useExplorerAutofix.spec.tsx index 8fd60c56a7fe..2441f263586b 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.spec.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.spec.tsx @@ -712,7 +712,7 @@ describe('getOrderedAutofixSections', () => { makeBlock({ artifacts: [ { - key: 'single_run_stop', + key: 'stop', reason: 'stop', data: {reason: 'R', next_step: 'N'}, }, @@ -731,7 +731,7 @@ describe('getOrderedAutofixSections', () => { makeBlock({ artifacts: [ { - key: 'single_run_stop', + key: 'stop', reason: 'stop', data: {reason: 'R', next_step: 'N'}, }, diff --git a/static/app/components/events/autofix/useExplorerAutofix.tsx b/static/app/components/events/autofix/useExplorerAutofix.tsx index f5754f1044d4..b34d8a720620 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.tsx @@ -494,7 +494,7 @@ function buildSection( */ export const SINGLE_RUN_FEATURE_ID = 'autofix_single_run'; const SINGLE_RUN_ROOT_CAUSE_KEY = 'single_run_root_cause'; -const SINGLE_RUN_STOP_KEY = 'single_run_stop'; +const SINGLE_RUN_STOP_KEY = 'stop'; export function isSingleRunAutofix(runState: ExplorerAutofixState | null): boolean { return runState?.feature_id === SINGLE_RUN_FEATURE_ID; diff --git a/static/app/components/events/autofix/v3/autofixCards.spec.tsx b/static/app/components/events/autofix/v3/autofixCards.spec.tsx index 06564d3b0f73..872789ecb58f 100644 --- a/static/app/components/events/autofix/v3/autofixCards.spec.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.spec.tsx @@ -421,7 +421,7 @@ describe('ArtifactCard', () => { it('shows why a single run stopped instead of the failure text', () => { const stop = { - key: 'single_run_stop', + key: 'stop', reason: 'stop', data: {reason: 'R', next_step: 'N'}, }; diff --git a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx index cefa80454fd5..f841025b2481 100644 --- a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx +++ b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx @@ -57,7 +57,7 @@ describe('RootCausePreview', () => { it('shows why a single run stopped instead of the failure text', () => { const stop = { - key: 'single_run_stop', + key: 'stop', reason: 'stop', data: {reason: 'R', next_step: 'N'}, }; From 520d44335889f04c8e41af66bb9614f6473d2738 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Wed, 11 Nov 2026 14:24:24 -0800 Subject: [PATCH 3/4] ref(autofix): Collapse the stop artifact guard into one expression --- .../events/autofix/useExplorerAutofix.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/static/app/components/events/autofix/useExplorerAutofix.tsx b/static/app/components/events/autofix/useExplorerAutofix.tsx index b34d8a720620..c7cff276959e 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.tsx @@ -85,14 +85,12 @@ export interface SingleRunStop { } function isSingleRunStopArtifact(value: unknown): value is Artifact { - if (!isArtifact(value) || value.key !== SINGLE_RUN_STOP_KEY) { - return false; - } - const data = value.data; - if (data === null || typeof data !== 'object') { - return false; - } - return isString(data.reason) && isString(data.next_step); + return ( + isArtifact(value) && + value.key === SINGLE_RUN_STOP_KEY && + isString(value.data?.reason) && + isString(value.data?.next_step) + ); } export function getSingleRunStop(section: AutofixSection): SingleRunStop | null { From 090e1f33b02a213ef36a6641a9cb3f5d330f7ecc Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Wed, 11 Nov 2026 14:38:35 -0800 Subject: [PATCH 4/4] ref(autofix): Rename the stop artifact's next_step to suggestion --- .../components/events/autofix/useExplorerAutofix.spec.tsx | 6 +++--- static/app/components/events/autofix/useExplorerAutofix.tsx | 4 ++-- .../app/components/events/autofix/v3/autofixCards.spec.tsx | 2 +- .../components/events/autofix/v3/autofixPreviews.spec.tsx | 2 +- static/app/components/events/autofix/v3/rootCauseCard.tsx | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/static/app/components/events/autofix/useExplorerAutofix.spec.tsx b/static/app/components/events/autofix/useExplorerAutofix.spec.tsx index 2441f263586b..685e49a9b910 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.spec.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.spec.tsx @@ -714,14 +714,14 @@ describe('getOrderedAutofixSections', () => { { key: 'stop', reason: 'stop', - data: {reason: 'R', next_step: 'N'}, + data: {reason: 'R', suggestion: 'N'}, }, ], }), ]) ); - expect(getSingleRunStop(sections[0]!)).toEqual({reason: 'R', next_step: 'N'}); + expect(getSingleRunStop(sections[0]!)).toEqual({reason: 'R', suggestion: 'N'}); }); it('drops the stop once the run has code changes', () => { @@ -733,7 +733,7 @@ describe('getOrderedAutofixSections', () => { { key: 'stop', reason: 'stop', - data: {reason: 'R', next_step: 'N'}, + data: {reason: 'R', suggestion: 'N'}, }, ], merged_file_patches: [makePatch('org/repo', 'a.py')], diff --git a/static/app/components/events/autofix/useExplorerAutofix.tsx b/static/app/components/events/autofix/useExplorerAutofix.tsx index c7cff276959e..eb6ab3d5774a 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.tsx @@ -80,8 +80,8 @@ export function isRootCauseArtifact( } export interface SingleRunStop { - next_step: string; reason: string; + suggestion: string; } function isSingleRunStopArtifact(value: unknown): value is Artifact { @@ -89,7 +89,7 @@ function isSingleRunStopArtifact(value: unknown): value is Artifact { const stop = { key: 'stop', reason: 'stop', - data: {reason: 'R', next_step: 'N'}, + data: {reason: 'R', suggestion: 'N'}, }; render( diff --git a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx index f841025b2481..1555f379c7ca 100644 --- a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx +++ b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx @@ -59,7 +59,7 @@ describe('RootCausePreview', () => { const stop = { key: 'stop', reason: 'stop', - data: {reason: 'R', next_step: 'N'}, + data: {reason: 'R', suggestion: 'N'}, }; render(); diff --git a/static/app/components/events/autofix/v3/rootCauseCard.tsx b/static/app/components/events/autofix/v3/rootCauseCard.tsx index 98eb63a914c4..f75615a3d1a2 100644 --- a/static/app/components/events/autofix/v3/rootCauseCard.tsx +++ b/static/app/components/events/autofix/v3/rootCauseCard.tsx @@ -46,8 +46,8 @@ export function RootCauseCard({autofix, groupId, section}: RootCauseCardProps) { {t('Why Seer stopped')} - {t('Next step')} - + {t('Suggestion')} + ) : null;