Skip to content

Commit c4b9f72

Browse files
Mihir-Mavalankarandrewshie-sentry
authored andcommitted
feat(seer setting): Auto-Trigger Autofix turned into toggle [feature flagged] (#103252)
## PR Details + Change Auto Trigger Autofix from a dropdown to a toggle behind feature flag. + Backend remains the same: Off maps to "off" and On maps to "always" since we want to run of all issues depending on the fixability score. + Notion doc [reference](https://www.notion.so/sentry/Triage-Signals-V0-Technical-Implementation-Details-2a18b10e4b5d8086a7ceddaf4194849a?source=copy_link#2a18b10e4b5d8068a296e9109a9629f8)
1 parent 2f68158 commit c4b9f72

File tree

2 files changed

+153
-8
lines changed

2 files changed

+153
-8
lines changed

static/app/views/settings/projectSeer/index.spec.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,4 +583,118 @@ describe('ProjectSeer', () => {
583583
});
584584
expect(toggle).toBeInTheDocument();
585585
});
586+
587+
describe('Auto-Trigger Fixes with triage-signals-v0', () => {
588+
it('shows as toggle when flag enabled, dropdown when disabled', async () => {
589+
const projectWithFlag = ProjectFixture({
590+
features: ['triage-signals-v0'],
591+
seerScannerAutomation: true,
592+
autofixAutomationTuning: 'off',
593+
});
594+
595+
const {unmount} = render(<ProjectSeer />, {
596+
organization,
597+
outletContext: {project: projectWithFlag},
598+
});
599+
600+
await screen.findByText(/Automation/i);
601+
expect(
602+
screen.getByRole('checkbox', {name: /Auto-Trigger Fixes/i})
603+
).toBeInTheDocument();
604+
expect(
605+
screen.queryByRole('textbox', {name: /Auto-Trigger Fixes/i})
606+
).not.toBeInTheDocument();
607+
608+
unmount();
609+
610+
render(<ProjectSeer />, {
611+
organization,
612+
outletContext: {
613+
project: ProjectFixture({
614+
seerScannerAutomation: true,
615+
autofixAutomationTuning: 'high',
616+
}),
617+
},
618+
});
619+
620+
await screen.findByText(/Automation/i);
621+
expect(
622+
screen.getByRole('textbox', {name: /Auto-Trigger Fixes/i})
623+
).toBeInTheDocument();
624+
expect(
625+
screen.queryByRole('checkbox', {name: /Auto-Trigger Fixes/i})
626+
).not.toBeInTheDocument();
627+
});
628+
629+
it('maps values correctly: off=unchecked, others=checked', async () => {
630+
const {unmount} = render(<ProjectSeer />, {
631+
organization,
632+
outletContext: {
633+
project: ProjectFixture({
634+
features: ['triage-signals-v0'],
635+
seerScannerAutomation: true,
636+
autofixAutomationTuning: 'off',
637+
}),
638+
},
639+
});
640+
641+
expect(
642+
await screen.findByRole('checkbox', {name: /Auto-Trigger Fixes/i})
643+
).not.toBeChecked();
644+
unmount();
645+
646+
render(<ProjectSeer />, {
647+
organization,
648+
outletContext: {
649+
project: ProjectFixture({
650+
features: ['triage-signals-v0'],
651+
seerScannerAutomation: true,
652+
autofixAutomationTuning: 'high',
653+
}),
654+
},
655+
});
656+
657+
expect(
658+
await screen.findByRole('checkbox', {name: /Auto-Trigger Fixes/i})
659+
).toBeChecked();
660+
});
661+
662+
it('saves "always" when toggled ON, "off" when toggled OFF', async () => {
663+
const projectPutRequest = MockApiClient.addMockResponse({
664+
url: `/projects/${organization.slug}/${project.slug}/`,
665+
method: 'PUT',
666+
body: {},
667+
});
668+
669+
render(<ProjectSeer />, {
670+
organization,
671+
outletContext: {
672+
project: ProjectFixture({
673+
features: ['triage-signals-v0'],
674+
seerScannerAutomation: true,
675+
autofixAutomationTuning: 'off',
676+
}),
677+
},
678+
});
679+
680+
const toggle = await screen.findByRole('checkbox', {name: /Auto-Trigger Fixes/i});
681+
await userEvent.click(toggle);
682+
683+
await waitFor(() => {
684+
expect(projectPutRequest).toHaveBeenCalledWith(
685+
expect.any(String),
686+
expect.objectContaining({data: {autofixAutomationTuning: 'always'}})
687+
);
688+
});
689+
690+
await userEvent.click(toggle);
691+
692+
await waitFor(() => {
693+
expect(projectPutRequest).toHaveBeenCalledWith(
694+
expect.any(String),
695+
expect.objectContaining({data: {autofixAutomationTuning: 'off'}})
696+
);
697+
});
698+
});
699+
});
586700
});

static/app/views/settings/projectSeer/index.tsx

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,29 @@ export const autofixAutomatingTuningField = {
8484
visible: ({model}) => model?.getValue('seerScannerAutomation') === true,
8585
} satisfies FieldObject;
8686

87+
const autofixAutomationToggleField = {
88+
name: 'autofixAutomationTuning',
89+
label: t('Auto-Trigger Fixes'),
90+
help: () =>
91+
t(
92+
'When enabled, Seer will automatically analyze actionable issues in the background.'
93+
),
94+
type: 'boolean',
95+
saveOnBlur: true,
96+
saveMessage: t('Automatic Seer settings updated'),
97+
getData: (data: Record<PropertyKey, unknown>) => ({
98+
autofixAutomationTuning: data.autofixAutomationTuning ? 'always' : 'off',
99+
}),
100+
} satisfies FieldObject;
101+
87102
function ProjectSeerGeneralForm({project}: {project: Project}) {
88103
const organization = useOrganization();
89104
const queryClient = useQueryClient();
90105
const {preference} = useProjectSeerPreferences(project);
91106
const {mutate: updateProjectSeerPreferences} = useUpdateProjectSeerPreferences(project);
92107
const {data: codingAgentIntegrations} = useCodingAgentIntegrations();
93108

109+
const isTriageSignalsFeatureOn = project.features.includes('triage-signals-v0');
94110
const canWriteProject = hasEveryAccess(['project:read'], {organization, project});
95111

96112
const cursorIntegration = codingAgentIntegrations?.integrations.find(
@@ -190,12 +206,22 @@ function ProjectSeerGeneralForm({project}: {project: Project}) {
190206
saveOnBlur: true,
191207
saveMessage: t('Stopping point updated'),
192208
onChange: handleStoppingPointChange,
193-
visible: ({model}) =>
194-
model?.getValue('seerScannerAutomation') === true &&
195-
model?.getValue('autofixAutomationTuning') !== 'off',
196-
} satisfies FieldObject;
209+
visible: ({model}) => {
210+
const tuningValue = model?.getValue('autofixAutomationTuning');
211+
// Handle both boolean (toggle) and string (dropdown) values
212+
const automationEnabled =
213+
typeof tuningValue === 'boolean' ? tuningValue : tuningValue !== 'off';
197214

198-
const isTriageSignalsFeatureOn = project.features.includes('triage-signals-v0');
215+
// When feature flag is ON (toggle mode): only check automation
216+
// When feature flag is OFF (dropdown mode): check both scanner and automation
217+
if (isTriageSignalsFeatureOn) {
218+
return automationEnabled;
219+
}
220+
221+
const scannerEnabled = model?.getValue('seerScannerAutomation') === true;
222+
return scannerEnabled && automationEnabled;
223+
},
224+
} satisfies FieldObject;
199225

200226
const seerFormGroups: JsonFormObject[] = [
201227
{
@@ -222,7 +248,9 @@ function ProjectSeerGeneralForm({project}: {project: Project}) {
222248
),
223249
fields: [
224250
...(isTriageSignalsFeatureOn ? [] : [seerScannerAutomationField]),
225-
autofixAutomatingTuningField,
251+
isTriageSignalsFeatureOn
252+
? autofixAutomationToggleField
253+
: autofixAutomatingTuningField,
226254
automatedRunStoppingPointField,
227255
],
228256
},
@@ -235,14 +263,17 @@ function ProjectSeerGeneralForm({project}: {project: Project}) {
235263
preference?.automation_handoff
236264
? 'cursor_handoff'
237265
: (preference?.automated_run_stopping_point ?? 'root_cause')
238-
}`}
266+
}-${isTriageSignalsFeatureOn}`}
239267
saveOnBlur
240268
apiMethod="PUT"
241269
apiEndpoint={`/projects/${organization.slug}/${project.slug}/`}
242270
allowUndo
243271
initialData={{
244272
seerScannerAutomation: project.seerScannerAutomation ?? false,
245-
autofixAutomationTuning: project.autofixAutomationTuning ?? 'off',
273+
// Same DB field, different UI: toggle (boolean) vs dropdown (string)
274+
autofixAutomationTuning: isTriageSignalsFeatureOn
275+
? (project.autofixAutomationTuning ?? 'off') !== 'off'
276+
: (project.autofixAutomationTuning ?? 'off'),
246277
automated_run_stopping_point: preference?.automation_handoff
247278
? 'cursor_handoff'
248279
: (preference?.automated_run_stopping_point ?? 'root_cause'),

0 commit comments

Comments
 (0)