Skip to content

Commit ff72324

Browse files
authored
feat(autofix): Add Autofix status to sidebar button (#85287)
Change the copy of the "Open Autofix" button to indicate the current status of the Autofix run. Also catches an edge case in the polling logic when the root cause step is complete but the solution step hasn't been triggered yet. Examples: <img width="352" alt="Screenshot 2025-02-14 at 1 40 23 PM" src="https://github.com/user-attachments/assets/a3de0930-78ba-4526-9e00-3da57c7ea984" /> <img width="336" alt="Screenshot 2025-02-14 at 1 42 11 PM" src="https://github.com/user-attachments/assets/2b1b5cf1-8661-4973-a6e9-a8e15575cdd8" /> <img width="360" alt="Screenshot 2025-02-14 at 1 42 36 PM" src="https://github.com/user-attachments/assets/68dba0e1-a6ee-4c19-b81c-a5c8330262d3" /> <img width="358" alt="Screenshot 2025-02-14 at 1 45 32 PM" src="https://github.com/user-attachments/assets/619126d5-2008-4ca4-bc1f-51fd8bd9cb1e" /> <img width="341" alt="Screenshot 2025-02-14 at 1 46 18 PM" src="https://github.com/user-attachments/assets/f6e8dafb-2623-4ef2-8c80-aae8d775431b" />
1 parent c8d050f commit ff72324

File tree

7 files changed

+251
-147
lines changed

7 files changed

+251
-147
lines changed

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

+23-4
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,30 @@ const makeErrorAutofixData = (errorMessage: string): AutofixResponse => {
7676
};
7777

7878
/** Will not poll when the autofix is in an error state or has completed */
79-
const isPolling = (autofixData?: AutofixData | null) =>
80-
!autofixData ||
81-
![AutofixStatus.ERROR, AutofixStatus.COMPLETED, AutofixStatus.CANCELLED].includes(
82-
autofixData.status
79+
const isPolling = (autofixData?: AutofixData | null) => {
80+
if (!autofixData?.steps) {
81+
return true;
82+
}
83+
84+
const hasSolutionStep = autofixData.steps.some(
85+
step => step.type === AutofixStepType.SOLUTION
86+
);
87+
88+
if (
89+
!hasSolutionStep &&
90+
![AutofixStatus.ERROR, AutofixStatus.CANCELLED].includes(autofixData.status)
91+
) {
92+
// we want to keep polling until we have a solution step because that's a stopping point
93+
// we need this explicit check in case we get a state for a fraction of a second where the root cause is complete and there is no step after it started
94+
return true;
95+
}
96+
return (
97+
!autofixData ||
98+
![AutofixStatus.ERROR, AutofixStatus.COMPLETED, AutofixStatus.CANCELLED].includes(
99+
autofixData.status
100+
)
83101
);
102+
};
84103

85104
export const useAutofixData = ({groupId}: {groupId: string}) => {
86105
const {data} = useApiQuery<AutofixResponse>(makeAutofixQueryKey(groupId), {

static/app/views/issueDetails/groupEventDetails/groupEventDetails.spec.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,12 @@ const mockGroupApis = (
336336
},
337337
},
338338
});
339+
MockApiClient.addMockResponse({
340+
url: `/issues/${group.id}/autofix/`,
341+
body: {
342+
steps: [],
343+
},
344+
});
339345
};
340346

341347
describe('groupEventDetails', () => {

static/app/views/issueDetails/streamline/hooks/useAiConfig.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const useAiConfig = (
4848

4949
const isSummaryEnabled = issueTypeConfig.issueSummary.enabled;
5050
const isAutofixEnabled = issueTypeConfig.autofix;
51-
const hasResources = issueTypeConfig.resources;
51+
const hasResources = !!issueTypeConfig.resources;
5252

5353
const hasGenAIConsent = autofixSetupData?.genAIConsent.ok ?? organization.genAIConsent;
5454

static/app/views/issueDetails/streamline/sidebar/sidebar.spec.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ describe('StreamlinedSidebar', function () {
6161
},
6262
});
6363

64+
MockApiClient.addMockResponse({
65+
url: `/issues/${group.id}/autofix/`,
66+
body: {
67+
steps: [],
68+
},
69+
});
70+
6471
mockFirstLastRelease = MockApiClient.addMockResponse({
6572
url: `/organizations/${organization.slug}/issues/${group.id}/first-last-release/`,
6673
method: 'GET',

static/app/views/issueDetails/streamline/sidebar/solutionsSection.spec.tsx

+13-48
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ describe('SolutionsSection', () => {
4343
},
4444
});
4545

46+
MockApiClient.addMockResponse({
47+
url: `/issues/${mockGroup.id}/autofix/`,
48+
body: {
49+
steps: [],
50+
},
51+
});
52+
4653
jest.mocked(getConfigForIssueType).mockReturnValue({
4754
issueSummary: {
4855
enabled: true,
@@ -190,7 +197,7 @@ describe('SolutionsSection', () => {
190197
});
191198

192199
describe('Solutions Hub button text', () => {
193-
it('shows "Set up Sentry AI" when AI needs setup', async () => {
200+
it('shows "Set Up Sentry AI" when AI needs setup', async () => {
194201
const customOrganization = OrganizationFixture({
195202
genAIConsent: false,
196203
hideAiFeatures: false,
@@ -221,10 +228,10 @@ describe('SolutionsSection', () => {
221228
screen.getByText('Explore potential root causes and solutions with Sentry AI.')
222229
).toBeInTheDocument();
223230

224-
expect(screen.getByRole('button', {name: 'Set up Sentry AI'})).toBeInTheDocument();
231+
expect(screen.getByRole('button', {name: 'Set Up Sentry AI'})).toBeInTheDocument();
225232
});
226233

227-
it('shows "Set up Autofix" when autofix needs setup', async () => {
234+
it('shows "Set Up Autofix" when autofix needs setup', async () => {
228235
MockApiClient.addMockResponse({
229236
url: `/issues/${mockGroup.id}/autofix/setup/`,
230237
body: {
@@ -252,52 +259,10 @@ describe('SolutionsSection', () => {
252259
expect(screen.queryByTestId('loading-placeholder')).not.toBeInTheDocument();
253260
});
254261

255-
expect(screen.getByRole('button', {name: 'Set up Autofix'})).toBeInTheDocument();
256-
});
257-
258-
it('shows "Open Resources & Autofix" when both are available', async () => {
259-
// Mock successful summary response
260-
MockApiClient.addMockResponse({
261-
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
262-
method: 'POST',
263-
body: {
264-
whatsWrong: 'Test summary',
265-
},
266-
});
267-
268-
// Mock successful autofix setup
269-
MockApiClient.addMockResponse({
270-
url: `/issues/${mockGroup.id}/autofix/setup/`,
271-
body: {
272-
genAIConsent: {ok: true},
273-
integration: {ok: true},
274-
githubWriteIntegration: {ok: true},
275-
},
276-
});
277-
278-
MockApiClient.addMockResponse({
279-
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/summarize/`,
280-
method: 'POST',
281-
body: {
282-
whatsWrong: 'Test summary',
283-
},
284-
});
285-
286-
render(
287-
<SolutionsSection event={mockEvent} group={mockGroup} project={mockProject} />,
288-
{
289-
organization,
290-
}
291-
);
292-
293-
await waitFor(() => {
294-
expect(
295-
screen.getByRole('button', {name: 'Open Resources & Autofix'})
296-
).toBeInTheDocument();
297-
});
262+
expect(screen.getByRole('button', {name: 'Set Up Autofix'})).toBeInTheDocument();
298263
});
299264

300-
it('shows "Open Autofix" when only autofix is available', async () => {
265+
it('shows "Find Root Cause" when autofix is available', async () => {
301266
// Mock successful autofix setup but disable resources
302267
MockApiClient.addMockResponse({
303268
url: `/issues/${mockGroup.id}/autofix/setup/`,
@@ -329,7 +294,7 @@ describe('SolutionsSection', () => {
329294
);
330295

331296
await waitFor(() => {
332-
expect(screen.getByRole('button', {name: 'Open Autofix'})).toBeInTheDocument();
297+
expect(screen.getByRole('button', {name: 'Find Root Cause'})).toBeInTheDocument();
333298
});
334299
});
335300

static/app/views/issueDetails/streamline/sidebar/solutionsSection.tsx

+12-94
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import {useRef, useState} from 'react';
1+
import {useState} from 'react';
22
import styled from '@emotion/styled';
33

44
import FeatureBadge from 'sentry/components/badge/featureBadge';
55
import {Button} from 'sentry/components/button';
6-
import {Chevron} from 'sentry/components/chevron';
7-
import useDrawer from 'sentry/components/globalDrawer';
86
import {GroupSummary} from 'sentry/components/group/groupSummary';
9-
import Placeholder from 'sentry/components/placeholder';
107
import {IconMegaphone} from 'sentry/icons';
118
import {t, tct} from 'sentry/locale';
129
import {space} from 'sentry/styles/space';
@@ -19,9 +16,10 @@ import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
1916
import {FoldSection} from 'sentry/views/issueDetails/streamline/foldSection';
2017
import {useAiConfig} from 'sentry/views/issueDetails/streamline/hooks/useAiConfig';
2118
import Resources from 'sentry/views/issueDetails/streamline/sidebar/resources';
22-
import {SolutionsHubDrawer} from 'sentry/views/issueDetails/streamline/sidebar/solutionsHubDrawer';
2319
import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
2420

21+
import {SolutionsSectionCtaButton} from './solutionsSectionCtaButton';
22+
2523
function SolutionsHubFeedbackButton({hidden}: {hidden: boolean}) {
2624
const openFeedbackForm = useFeedbackForm();
2725
if (hidden) {
@@ -57,60 +55,10 @@ export default function SolutionsSection({
5755
const hasStreamlinedUI = useHasStreamlinedUI();
5856
// We don't use this on the streamlined UI, since the section folds.
5957
const [isExpanded, setIsExpanded] = useState(false);
60-
const openButtonRef = useRef<HTMLButtonElement>(null);
61-
const {openDrawer} = useDrawer();
62-
63-
const openSolutionsDrawer = () => {
64-
if (!event) {
65-
return;
66-
}
67-
openDrawer(
68-
() => <SolutionsHubDrawer group={group} project={project} event={event} />,
69-
{
70-
ariaLabel: t('Solutions drawer'),
71-
// We prevent a click on the Open/Close Autofix button from closing the drawer so that
72-
// we don't reopen it immediately, and instead let the button handle this itself.
73-
shouldCloseOnInteractOutside: element => {
74-
const viewAllButton = openButtonRef.current;
75-
if (
76-
viewAllButton?.contains(element) ||
77-
document.getElementById('sentry-feedback')?.contains(element) ||
78-
document.getElementById('autofix-rethink-input')?.contains(element) ||
79-
document.getElementById('autofix-output-stream')?.contains(element) ||
80-
document.getElementById('autofix-write-access-modal')?.contains(element) ||
81-
element.closest('[data-overlay="true"]')
82-
) {
83-
return false;
84-
}
85-
return true;
86-
},
87-
transitionProps: {stiffness: 1000},
88-
}
89-
);
90-
};
9158

9259
const aiConfig = useAiConfig(group, event, project);
9360
const issueTypeConfig = getConfigForIssueType(group, project);
9461

95-
const showCtaButton =
96-
aiConfig.needsGenAIConsent ||
97-
aiConfig.hasAutofix ||
98-
(aiConfig.hasSummary && aiConfig.hasResources);
99-
const isButtonLoading = aiConfig.isAutofixSetupLoading;
100-
101-
const getButtonText = () => {
102-
if (aiConfig.needsGenAIConsent) {
103-
return t('Set up Sentry AI');
104-
}
105-
if (!aiConfig.hasAutofix) {
106-
return t('Open Resources');
107-
}
108-
if (aiConfig.needsAutofixSetup) {
109-
return t('Set up Autofix');
110-
}
111-
return aiConfig.hasResources ? t('Open Resources & Autofix') : t('Open Autofix');
112-
};
113-
11462
const renderContent = () => {
11563
if (aiConfig.needsGenAIConsent) {
11664
return (
@@ -176,24 +124,15 @@ export default function SolutionsSection({
176124
>
177125
<SolutionsSectionContainer>
178126
{renderContent()}
179-
{isButtonLoading ? (
180-
<ButtonPlaceholder />
181-
) : showCtaButton ? (
182-
<StyledButton
183-
ref={openButtonRef}
184-
onClick={() => openSolutionsDrawer()}
185-
analyticsEventKey="issue_details.solutions_hub_opened"
186-
analyticsEventName="Issue Details: Solutions Hub Opened"
187-
analyticsParams={{
188-
has_streamlined_ui: hasStreamlinedUI,
189-
}}
190-
>
191-
{getButtonText()}
192-
<ChevronContainer>
193-
<Chevron direction="right" size="large" />
194-
</ChevronContainer>
195-
</StyledButton>
196-
) : null}
127+
{event && (
128+
<SolutionsSectionCtaButton
129+
aiConfig={aiConfig}
130+
event={event}
131+
group={group}
132+
project={project}
133+
hasStreamlinedUI={hasStreamlinedUI}
134+
/>
135+
)}
197136
</SolutionsSectionContainer>
198137
</SidebarFoldSection>
199138
);
@@ -254,30 +193,9 @@ const ExpandButton = styled(Button)`
254193
}
255194
`;
256195

257-
const StyledButton = styled(Button)`
258-
margin-top: ${space(1)};
259-
width: 100%;
260-
background: ${p => p.theme.background}
261-
linear-gradient(to right, ${p => p.theme.background}, ${p => p.theme.pink400}20);
262-
color: ${p => p.theme.pink400};
263-
`;
264-
265-
const ChevronContainer = styled('div')`
266-
margin-left: ${space(0.5)};
267-
height: 16px;
268-
width: 16px;
269-
`;
270-
271196
const HeaderContainer = styled('div')`
272197
font-size: ${p => p.theme.fontSizeMedium};
273198
display: flex;
274199
align-items: center;
275200
gap: ${space(0.25)};
276201
`;
277-
278-
const ButtonPlaceholder = styled(Placeholder)`
279-
width: 100%;
280-
height: 38px;
281-
border-radius: ${p => p.theme.borderRadius};
282-
margin-top: ${space(1)};
283-
`;

0 commit comments

Comments
 (0)