Skip to content

Commit f29ea8b

Browse files
roagaandrewshie-sentry
authored andcommitted
feat(autofix): Better errored state (#92571)
If the run errors, we don't erase the whole run. And we let the user rethink to keep going. (there is a refresh button hidden behind spotlight in the first screenshot) <img width="867" alt="Screenshot 2025-05-30 at 11 23 56 AM" src="https://github.com/user-attachments/assets/b5126e82-1865-4577-bb7f-358e661b594b" /> <img width="850" alt="Screenshot 2025-05-30 at 11 09 15 AM" src="https://github.com/user-attachments/assets/f611d772-4489-4dd9-9734-7ee2ea2499f9" />
1 parent cc71486 commit f29ea8b

File tree

4 files changed

+159
-103
lines changed

4 files changed

+159
-103
lines changed

static/app/components/events/autofix/autofixInsightCards.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ function AutofixInsightCards({
567567
);
568568
}
569569

570-
function useUpdateInsightCard({groupId, runId}: {groupId: string; runId: string}) {
570+
export function useUpdateInsightCard({groupId, runId}: {groupId: string; runId: string}) {
571571
const api = useApi({persistInFlight: true});
572572
const queryClient = useQueryClient();
573573
const orgSlug = useOrganization().slug;

static/app/components/events/autofix/autofixOutputStream.tsx

Lines changed: 114 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ import styled from '@emotion/styled';
44
import {AnimatePresence, motion} from 'framer-motion';
55

66
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
7-
import {SeerLoadingIcon} from 'sentry/components/ai/SeerIcon';
7+
import {SeerLoadingIcon, SeerWaitingIcon} from 'sentry/components/ai/SeerIcon';
88
import {Button} from 'sentry/components/core/button';
99
import {TextArea} from 'sentry/components/core/textarea';
1010
import {Tooltip} from 'sentry/components/core/tooltip';
11+
import {useUpdateInsightCard} from 'sentry/components/events/autofix/autofixInsightCards';
1112
import {AutofixProgressBar} from 'sentry/components/events/autofix/autofixProgressBar';
1213
import {FlyingLinesEffect} from 'sentry/components/events/autofix/FlyingLinesEffect';
1314
import type {AutofixData} from 'sentry/components/events/autofix/types';
15+
import {AutofixStepType} from 'sentry/components/events/autofix/types';
1416
import {makeAutofixQueryKey} from 'sentry/components/events/autofix/useAutofix';
1517
import {useTypingAnimation} from 'sentry/components/events/autofix/useTypingAnimation';
18+
import {getAutofixRunErrorMessage} from 'sentry/components/events/autofix/utils';
19+
import {IconRefresh} from 'sentry/icons';
1620
import {t} from 'sentry/locale';
1721
import {space} from 'sentry/styles/space';
1822
import {singleLineRenderer} from 'sentry/utils/marked/marked';
@@ -102,6 +106,104 @@ interface Props {
102106
responseRequired?: boolean;
103107
}
104108

109+
interface ActiveLogDisplayProps {
110+
groupId: string;
111+
runId: string;
112+
activeLog?: string;
113+
autofixData?: AutofixData;
114+
isInitializingRun?: boolean;
115+
seerIconRef?: React.RefObject<HTMLDivElement | null>;
116+
}
117+
118+
function ActiveLogDisplay({
119+
activeLog = '',
120+
isInitializingRun = false,
121+
seerIconRef,
122+
autofixData,
123+
groupId,
124+
runId,
125+
}: ActiveLogDisplayProps) {
126+
const displayedActiveLog =
127+
useTypingAnimation({
128+
text: activeLog,
129+
speed: 200,
130+
enabled: !!activeLog,
131+
}) || '';
132+
133+
// special case for errored step
134+
const errorMessage = getAutofixRunErrorMessage(autofixData);
135+
const erroredStep = autofixData?.steps?.find(step => step.status === 'ERROR');
136+
const erroredStepIndex = erroredStep?.index ?? 0;
137+
let retainInsightCardIndex: number | null = null;
138+
if (
139+
erroredStep &&
140+
erroredStep.type === AutofixStepType.DEFAULT &&
141+
Array.isArray((erroredStep as any).insights)
142+
) {
143+
const insights = (erroredStep as any).insights;
144+
if (insights.length > 0) {
145+
retainInsightCardIndex = insights.length;
146+
}
147+
}
148+
149+
const {mutate: refreshStep, isPending: isRefreshing} = useUpdateInsightCard({
150+
groupId,
151+
runId,
152+
});
153+
154+
if (errorMessage) {
155+
return (
156+
<ActiveLogWrapper>
157+
<SeerIconContainer>
158+
<SeerWaitingIcon size="lg" />
159+
</SeerIconContainer>
160+
<ActiveLog>{errorMessage}</ActiveLog>
161+
<Button
162+
size="xs"
163+
borderless
164+
aria-label={t('Retry step')}
165+
title={t('Retry step')}
166+
onClick={() =>
167+
refreshStep({
168+
message: '',
169+
step_index: erroredStepIndex,
170+
retain_insight_card_index: retainInsightCardIndex,
171+
})
172+
}
173+
disabled={isRefreshing}
174+
style={{marginLeft: 'auto'}}
175+
>
176+
<IconRefresh size="sm" />
177+
</Button>
178+
</ActiveLogWrapper>
179+
);
180+
}
181+
if (activeLog) {
182+
return (
183+
<ActiveLogWrapper>
184+
<Tooltip
185+
title={t(
186+
"Seer is hard at work. Feel free to leave - it'll keep running in the background."
187+
)}
188+
>
189+
<SeerIconContainer ref={seerIconRef}>
190+
<StyledAnimatedSeerIcon size="lg" />
191+
{seerIconRef?.current && isInitializingRun && (
192+
<FlyingLinesEffect targetElement={seerIconRef.current} />
193+
)}
194+
</SeerIconContainer>
195+
</Tooltip>
196+
<ActiveLog
197+
dangerouslySetInnerHTML={{
198+
__html: singleLineRenderer(displayedActiveLog),
199+
}}
200+
/>
201+
</ActiveLogWrapper>
202+
);
203+
}
204+
return null;
205+
}
206+
105207
export function AutofixOutputStream({
106208
stream,
107209
activeLog = '',
@@ -118,12 +220,6 @@ export function AutofixOutputStream({
118220

119221
const isInitializingRun = activeLog === 'Ingesting Sentry data...';
120222

121-
const displayedActiveLog = useTypingAnimation({
122-
text: activeLog,
123-
speed: 200,
124-
enabled: !!activeLog,
125-
});
126-
127223
const orgSlug = useOrganization().slug;
128224

129225
const {mutate: send} = useMutation({
@@ -192,27 +288,16 @@ export function AutofixOutputStream({
192288
>
193289
<VerticalLine />
194290
<Container required={responseRequired}>
195-
{activeLog && (
196-
<ActiveLogWrapper>
197-
<Tooltip
198-
title={t(
199-
"Seer is hard at work. Feel free to leave - it'll keep running in the background."
200-
)}
201-
>
202-
<SeerIconContainer ref={seerIconRef}>
203-
<StyledAnimatedSeerIcon size="lg" />
204-
{seerIconRef.current && isInitializingRun && (
205-
<FlyingLinesEffect targetElement={seerIconRef.current} />
206-
)}
207-
</SeerIconContainer>
208-
</Tooltip>
209-
<ActiveLog
210-
dangerouslySetInnerHTML={{
211-
__html: singleLineRenderer(displayedActiveLog),
212-
}}
213-
/>
214-
</ActiveLogWrapper>
215-
)}
291+
{getAutofixRunErrorMessage(autofixData) || activeLog ? (
292+
<ActiveLogDisplay
293+
activeLog={activeLog}
294+
isInitializingRun={isInitializingRun}
295+
seerIconRef={seerIconRef}
296+
autofixData={autofixData}
297+
groupId={groupId}
298+
runId={runId}
299+
/>
300+
) : null}
216301
{autofixData && (
217302
<ProgressBarWrapper>
218303
<AutofixProgressBar autofixData={autofixData} />
@@ -326,7 +411,7 @@ const ActiveLogWrapper = styled('div')`
326411
const ActiveLog = styled('div')`
327412
flex-grow: 1;
328413
word-break: break-word;
329-
margin-top: ${space(0.5)};
414+
margin-top: ${space(0.25)};
330415
`;
331416

332417
const VerticalLine = styled('div')`

static/app/components/events/autofix/autofixSteps.tsx

Lines changed: 2 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ import {AutofixSolution} from 'sentry/components/events/autofix/autofixSolution'
1313
import {
1414
type AutofixData,
1515
type AutofixProgressItem,
16-
AutofixStatus,
1716
type AutofixStep,
1817
AutofixStepType,
1918
} from 'sentry/components/events/autofix/types';
20-
import {IconArrow} from 'sentry/icons';
2119
import {t} from 'sentry/locale';
2220
import {space} from 'sentry/styles/space';
2321
import testableTransition from 'sentry/utils/testableTransition';
@@ -150,55 +148,6 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) {
150148
return null;
151149
}
152150

153-
if (data.status === AutofixStatus.ERROR) {
154-
const errorStep = steps.find(step => step.status === AutofixStatus.ERROR);
155-
const errorMessage = errorStep?.completedMessage || t('Something went wrong.');
156-
157-
// sugar coat common errors
158-
let customErrorMessage = '';
159-
if (
160-
errorMessage.toLowerCase().includes('overloaded') ||
161-
errorMessage.toLowerCase().includes('no completion tokens') ||
162-
errorMessage.toLowerCase().includes('exhausted')
163-
) {
164-
customErrorMessage = t(
165-
'The robots are having a moment. Our LLM provider is overloaded - please try again soon.'
166-
);
167-
} else if (
168-
errorMessage.toLowerCase().includes('prompt') ||
169-
errorMessage.toLowerCase().includes('tokens')
170-
) {
171-
customErrorMessage = t(
172-
"Seer worked so hard that it couldn't fit all its findings in its own memory. Please try again."
173-
);
174-
} else if (errorMessage.toLowerCase().includes('iterations')) {
175-
customErrorMessage = t(
176-
'Seer was taking a ton of iterations, so we pulled the plug out of fear it might go rogue. Please try again.'
177-
);
178-
} else if (errorMessage.toLowerCase().includes('timeout')) {
179-
customErrorMessage = t(
180-
'Seer was taking way too long, so we pulled the plug to turn it off and on again. Please try again.'
181-
);
182-
} else {
183-
customErrorMessage = t(
184-
"Oops, Seer went kaput. We've dispatched Seer to fix Seer. In the meantime, try again?"
185-
);
186-
}
187-
188-
return (
189-
<ErrorContainer>
190-
<StyledArrow direction="down" size="sm" />
191-
<ErrorMessage>
192-
{customErrorMessage || (
193-
<Fragment>
194-
{t('Something went wrong with Autofix:')} {errorMessage}
195-
</Fragment>
196-
)}
197-
</ErrorMessage>
198-
</ErrorContainer>
199-
);
200-
}
201-
202151
const lastStep = steps[steps.length - 1];
203152
const logs: AutofixProgressItem[] = lastStep!.progress?.filter(isProgressLog) ?? [];
204153
const activeLog =
@@ -209,7 +158,7 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) {
209158
const isInitialMount = !isMountedRef.current;
210159

211160
return (
212-
<StepsContainer>
161+
<div>
213162
{steps.map((step, index) => {
214163
const previousDefaultStepIndex = steps
215164
.slice(0, index)
@@ -283,7 +232,7 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) {
283232
autofixData={data}
284233
/>
285234
)}
286-
</StepsContainer>
235+
</div>
287236
);
288237
}
289238

@@ -296,26 +245,6 @@ const StepMessage = styled('div')`
296245
text-align: left;
297246
`;
298247

299-
const ErrorMessage = styled('div')`
300-
font-size: ${p => p.theme.fontSizeMedium};
301-
color: ${p => p.theme.subText};
302-
`;
303-
304-
const StepsContainer = styled('div')``;
305-
306-
const ErrorContainer = styled('div')`
307-
margin-top: ${space(1)};
308-
display: flex;
309-
align-items: center;
310-
flex-direction: column;
311-
gap: ${space(1)};
312-
`;
313-
314-
const StyledArrow = styled(IconArrow)`
315-
color: ${p => p.theme.subText};
316-
opacity: 0.5;
317-
`;
318-
319248
const StepCard = styled('div')`
320249
overflow: hidden;
321250

static/app/components/events/autofix/utils.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
AutofixStatus,
88
AutofixStepType,
99
} from 'sentry/components/events/autofix/types';
10+
import {t} from 'sentry/locale';
1011
import type {Group} from 'sentry/types/group';
1112

1213
export function getRootCauseDescription(autofixData: AutofixData) {
@@ -187,3 +188,44 @@ export function getAutofixRunExists(group: Group) {
187188
export function isIssueQuickFixable(group: Group) {
188189
return group.seerFixabilityScore && group.seerFixabilityScore > 0.7;
189190
}
191+
192+
export function getAutofixRunErrorMessage(autofixData: AutofixData | undefined) {
193+
if (!autofixData || autofixData.status !== AutofixStatus.ERROR) {
194+
return null;
195+
}
196+
197+
const errorStep = autofixData.steps?.find(step => step.status === AutofixStatus.ERROR);
198+
const errorMessage = errorStep?.completedMessage || t('Something went wrong.');
199+
200+
let customErrorMessage = '';
201+
if (
202+
errorMessage.toLowerCase().includes('overloaded') ||
203+
errorMessage.toLowerCase().includes('no completion tokens') ||
204+
errorMessage.toLowerCase().includes('exhausted')
205+
) {
206+
customErrorMessage = t(
207+
'The robots are having a moment. Our LLM provider is overloaded - please try again soon.'
208+
);
209+
} else if (
210+
errorMessage.toLowerCase().includes('prompt') ||
211+
errorMessage.toLowerCase().includes('tokens')
212+
) {
213+
customErrorMessage = t(
214+
"Seer worked so hard that it couldn't fit all its findings in its own memory. Please try again."
215+
);
216+
} else if (errorMessage.toLowerCase().includes('iterations')) {
217+
customErrorMessage = t(
218+
'Seer was taking a ton of iterations, so we pulled the plug out of fear it might go rogue. Please try again.'
219+
);
220+
} else if (errorMessage.toLowerCase().includes('timeout')) {
221+
customErrorMessage = t(
222+
'Seer was taking way too long, so we pulled the plug to turn it off and on again. Please try again.'
223+
);
224+
} else {
225+
customErrorMessage = t(
226+
"Oops, Seer went kaput. We've dispatched Seer to fix Seer. In the meantime, try again?"
227+
);
228+
}
229+
230+
return customErrorMessage;
231+
}

0 commit comments

Comments
 (0)