Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions static/app/components/events/autofix/useExplorerAutofix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -541,22 +541,28 @@ export function useExplorerAutofix(
* @param runId - Optional run ID to continue an existing run
*/
const startStep = useCallback(
async (step: AutofixExplorerStep, runId?: number) => {
async (step: AutofixExplorerStep, runId?: number, userContext?: string) => {
setWaitingForResponse(true);

try {
const data: Record<string, any> = {step};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactoring accidentally removed intelligence_level from request data

Low Severity

The refactoring to support user_feedback accidentally dropped intelligence_level: 'low' from the POST request data. The backend serializer happens to default to 'low', so behavior is currently preserved, but the explicit field was clearly intentional and its silent removal could cause issues if the backend default changes.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally removed to allow the backend to decide.


if (defined(runId)) {
data.run_id = runId;
}

if (userContext) {
data.user_context = userContext;
}
Comment on lines +555 to +556
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The user_context sent from the frontend is not handled by the backend, causing user feedback for the autofix rethink feature to be silently ignored.
Severity: HIGH

Suggested Fix

Update the backend to handle the user_context. This involves adding the user_context field to the ExplorerAutofixRequestSerializer, passing it through the trigger_autofix_explorer function, and forwarding it to the Seer Explorer client's continue_run method.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: static/app/components/events/autofix/useExplorerAutofix.tsx#L555-L556

Potential issue: The frontend sends a `user_context` field when a user provides feedback
for the autofix feature. However, the backend `ExplorerAutofixRequestSerializer` does
not define this field, causing Django REST Framework to silently drop it. Consequently,
the user's feedback is never passed to the `trigger_autofix_explorer` function or the
underlying Seer Explorer agent. This makes the entire "rethink" feedback feature
non-functional, as the user's input has no effect on the autofix process.


const response = await api.requestPromise(
getApiUrl('/organizations/$organizationIdOrSlug/issues/$issueId/autofix/', {
path: {organizationIdOrSlug: orgSlug, issueId: groupId},
}),
{
method: 'POST',
query: {mode: 'explorer'},
data: {
step,
intelligence_level: 'low',
...(runId !== undefined && {run_id: runId}),
},
data,
}
);

Expand Down
111 changes: 105 additions & 6 deletions static/app/components/events/autofix/v3/nextStep.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,46 @@ describe('SeerDrawerNextStep', () => {
expect(autofix.startStep).toHaveBeenCalledWith('solution', 1);
});

it('calls startStep with root_cause on no click', async () => {
it('shows feedback UI on no click', async () => {
const autofix = makeAutofix();
render(
<SeerDrawerNextStep sections={[makeSection('root_cause')]} autofix={autofix} />
);
await userEvent.click(screen.getByRole('button', {name: 'No'}));
expect(autofix.startStep).toHaveBeenCalledWith('root_cause', 1);
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(
screen.getByRole('button', {name: 'Rethink root cause'})
).toBeInTheDocument();
expect(
screen.getByRole('button', {name: 'Nevermind, make an implementation plan'})
).toBeInTheDocument();
});

it('calls startStep with root_cause and feedback on rethink click', async () => {
const autofix = makeAutofix();
render(
<SeerDrawerNextStep sections={[makeSection('root_cause')]} autofix={autofix} />
);
await userEvent.click(screen.getByRole('button', {name: 'No'}));
await userEvent.type(screen.getByRole('textbox'), 'Try a different approach');
await userEvent.click(screen.getByRole('button', {name: 'Rethink root cause'}));
expect(autofix.startStep).toHaveBeenCalledWith(
'root_cause',
1,
'Try a different approach'
);
});

it('proceeds like yes on nevermind click', async () => {
const autofix = makeAutofix();
render(
<SeerDrawerNextStep sections={[makeSection('root_cause')]} autofix={autofix} />
);
await userEvent.click(screen.getByRole('button', {name: 'No'}));
await userEvent.click(
screen.getByRole('button', {name: 'Nevermind, make an implementation plan'})
);
expect(autofix.startStep).toHaveBeenCalledWith('solution', 1);
});
});

Expand Down Expand Up @@ -103,13 +136,48 @@ describe('SeerDrawerNextStep', () => {
expect(autofix.startStep).toHaveBeenCalledWith('code_changes', 1);
});

it('calls startStep with solution on no click', async () => {
it('shows feedback UI on no click', async () => {
const autofix = makeAutofix();
render(
<SeerDrawerNextStep sections={[makeSection('solution')]} autofix={autofix} />
);
await userEvent.click(screen.getByRole('button', {name: 'No'}));
expect(autofix.startStep).toHaveBeenCalledWith('solution', 1);
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(
screen.getByRole('button', {name: 'Rethink implementation plan'})
).toBeInTheDocument();
expect(
screen.getByRole('button', {name: 'Nevermind, write a code fix'})
).toBeInTheDocument();
});

it('calls startStep with solution and feedback on rethink click', async () => {
const autofix = makeAutofix();
render(
<SeerDrawerNextStep sections={[makeSection('solution')]} autofix={autofix} />
);
await userEvent.click(screen.getByRole('button', {name: 'No'}));
await userEvent.type(screen.getByRole('textbox'), 'Consider edge cases');
await userEvent.click(
screen.getByRole('button', {name: 'Rethink implementation plan'})
);
expect(autofix.startStep).toHaveBeenCalledWith(
'solution',
1,
'Consider edge cases'
);
});

it('proceeds like yes on nevermind click', async () => {
const autofix = makeAutofix();
render(
<SeerDrawerNextStep sections={[makeSection('solution')]} autofix={autofix} />
);
await userEvent.click(screen.getByRole('button', {name: 'No'}));
await userEvent.click(
screen.getByRole('button', {name: 'Nevermind, write a code fix'})
);
expect(autofix.startStep).toHaveBeenCalledWith('code_changes', 1);
});
});

Expand All @@ -135,13 +203,44 @@ describe('SeerDrawerNextStep', () => {
expect(autofix.createPR).toHaveBeenCalledWith(1);
});

it('calls startStep with code_changes on no click', async () => {
it('shows feedback UI on no click', async () => {
const autofix = makeAutofix();
render(
<SeerDrawerNextStep sections={[makeSection('code_changes')]} autofix={autofix} />
);
await userEvent.click(screen.getByRole('button', {name: 'No'}));
expect(autofix.startStep).toHaveBeenCalledWith('code_changes', 1);
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(
screen.getByRole('button', {name: 'Rethink code changes'})
).toBeInTheDocument();
expect(
screen.getByRole('button', {name: 'Nevermind, draft a PR'})
).toBeInTheDocument();
});

it('calls startStep with code_changes and feedback on rethink click', async () => {
const autofix = makeAutofix();
render(
<SeerDrawerNextStep sections={[makeSection('code_changes')]} autofix={autofix} />
);
await userEvent.click(screen.getByRole('button', {name: 'No'}));
await userEvent.type(screen.getByRole('textbox'), 'Fix the error handling');
await userEvent.click(screen.getByRole('button', {name: 'Rethink code changes'}));
expect(autofix.startStep).toHaveBeenCalledWith(
'code_changes',
1,
'Fix the error handling'
);
});

it('proceeds like yes on nevermind click', async () => {
const autofix = makeAutofix();
render(
<SeerDrawerNextStep sections={[makeSection('code_changes')]} autofix={autofix} />
);
await userEvent.click(screen.getByRole('button', {name: 'No'}));
await userEvent.click(screen.getByRole('button', {name: 'Nevermind, draft a PR'}));
expect(autofix.createPR).toHaveBeenCalledWith(1);
});
});
});
90 changes: 72 additions & 18 deletions static/app/components/events/autofix/v3/nextStep.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {useCallback, type ReactNode} from 'react';
import {useCallback, useState, type ReactNode} from 'react';

import {Button} from '@sentry/scraps/button';
import {Flex} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
import {TextArea} from '@sentry/scraps/textarea';

import {
isCodeChangesSection,
Expand Down Expand Up @@ -54,18 +55,24 @@ function RootCauseNextStep({autofix, runId}: NextStepProps) {
startStep('solution', runId);
}, [startStep, runId]);

const handleNoClick = useCallback(() => {
// for now, just re run the current step
startStep('root_cause', runId);
}, [startStep, runId]);
const handleNoClick = useCallback(
(userContext: string) => {
startStep('root_cause', runId, userContext);
},
[startStep, runId]
);

return (
<NextStep
<NextStepTemplate
prompt={t('Are you happy with this root cause?')}
labelYes={t('Yes, make an implementation plan')}
onClickYes={handleYesClick}
labelNo={t('No')}
onClickNo={handleNoClick}
placeholderPrompt={t('Give seer additional context to improve this root cause.')}
rethinkPrompt={t('How can this root cause be improved?')}
labelNevermind={t('Nevermind, make an implementation plan')}
labelRethink={t('Rethink root cause')}
/>
);
}
Expand All @@ -77,18 +84,26 @@ function SolutionNextStep({autofix, runId}: NextStepProps) {
startStep('code_changes', runId);
}, [startStep, runId]);

const handleNoClick = useCallback(() => {
// for now, just re run the current step
startStep('solution', runId);
}, [startStep, runId]);
const handleNoClick = useCallback(
(userContext: string) => {
startStep('solution', runId, userContext);
},
[startStep, runId]
);

return (
<NextStep
<NextStepTemplate
prompt={t('Are you happy with this implementation plan?')}
labelYes={t('Yes, write a code fix')}
onClickYes={handleYesClick}
labelNo={t('No')}
onClickNo={handleNoClick}
placeholderPrompt={t(
'Give seer additional context to improve this implementation plan.'
)}
rethinkPrompt={t('How can this implementation plan be improved?')}
labelNevermind={t('Nevermind, write a code fix')}
labelRethink={t('Rethink implementation plan')}
/>
);
}
Expand All @@ -100,41 +115,80 @@ function CodeChangesNextStep({autofix, runId}: NextStepProps) {
createPR(runId);
}, [createPR, runId]);

const handleNoClick = useCallback(() => {
startStep('code_changes', runId);
}, [startStep, runId]);
const handleNoClick = useCallback(
(userContext: string) => {
startStep('code_changes', runId, userContext);
},
[startStep, runId]
);

return (
<NextStep
<NextStepTemplate
prompt={t('Are you happy with these code changes?')}
labelYes={t('Yes, draft a PR')}
onClickYes={handleYesClick}
labelNo={t('No')}
onClickNo={handleNoClick}
placeholderPrompt={t('Give seer additional context to improve this code change.')}
rethinkPrompt={t('How can this code change be improved?')}
labelNevermind={t('Nevermind, draft a PR')}
labelRethink={t('Rethink code changes')}
/>
);
}

interface NextStepTemplateProps {
labelNevermind: ReactNode;
labelNo: ReactNode;
labelRethink: ReactNode;
labelYes: ReactNode;
onClickNo: () => void;
onClickNo: (prompt: string) => void;
onClickYes: () => void;
placeholderPrompt: string;
prompt: ReactNode;
rethinkPrompt: ReactNode;
}

function NextStep({
function NextStepTemplate({
prompt,
labelYes,
onClickYes,
labelNo,
onClickNo,
placeholderPrompt,
rethinkPrompt,
labelNevermind,
labelRethink,
}: NextStepTemplateProps) {
const [clickedNo, handleClickedNo] = useState(false);
const [userContext, setUserContext] = useState('');

if (clickedNo) {
return (
<Flex direction="column" gap="lg">
<Text>{rethinkPrompt}</Text>
<TextArea
autosize
rows={2}
placeholder={placeholderPrompt}
value={userContext}
onChange={event => setUserContext(event.target.value)}
/>
<Flex gap="md">
<Button onClick={onClickYes}>{labelNevermind}</Button>
<Button priority="primary" onClick={() => onClickNo(userContext)}>
{labelRethink}
</Button>
</Flex>
</Flex>
);
}

return (
<Flex direction="column" gap="lg">
<Text>{prompt}</Text>
<Flex gap="md">
<Button onClick={onClickNo}>{labelNo}</Button>
<Button onClick={() => handleClickedNo(true)}>{labelNo}</Button>
<Button priority="primary" onClick={onClickYes}>
{labelYes}
</Button>
Expand Down
Loading