Skip to content

Commit 5c9c651

Browse files
authored
feat(seer settings): Adds recommended automation button (#95486)
Adds a button to turn on automation for projects where it makes sense. <img width="1228" height="723" alt="Screenshot 2025-07-14 at 2 38 49 PM" src="https://github.com/user-attachments/assets/cc28ec57-175f-4aef-b4bd-cffb399f7add" />
1 parent 5b90560 commit 5c9c651

File tree

3 files changed

+132
-7
lines changed

3 files changed

+132
-7
lines changed

static/app/components/events/autofix/preferences/hooks/useProjectSeerPreferences.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ export interface SeerPreferencesResponse {
1111
preference?: ProjectSeerPreferences | null;
1212
}
1313

14+
export function makeProjectSeerPreferencesQueryKey(orgSlug: string, projectSlug: string) {
15+
return `/projects/${orgSlug}/${projectSlug}/seer/preferences/`;
16+
}
17+
1418
export function useProjectSeerPreferences(project: Project) {
1519
const organization = useOrganization();
1620

1721
const {data, ...rest} = useApiQuery<SeerPreferencesResponse>(
18-
[`/projects/${organization.slug}/${project.slug}/seer/preferences/`],
22+
[makeProjectSeerPreferencesQueryKey(organization.slug, project.slug)],
1923
{
2024
staleTime: 60000, // 1 minute
2125
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,13 @@ export function AutofixRepositories({project}: ProjectSeerProps) {
258258
icon={<IconAdd />}
259259
disabled={isRepoLimitReached || unselectedRepositories?.length === 0}
260260
onClick={openAddRepoModal}
261+
priority={
262+
!isFetchingRepositories &&
263+
!isLoadingPreferences &&
264+
filteredSelectedRepositories.length === 0
265+
? 'primary'
266+
: 'default'
267+
}
261268
>
262269
{t('Add Repos')}
263270
</Button>

static/gsApp/views/seerAutomation/seerAutomationProjectList.tsx

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import {ButtonBar} from 'sentry/components/core/button/buttonBar';
1313
import {Checkbox} from 'sentry/components/core/checkbox';
1414
import {Flex} from 'sentry/components/core/layout';
1515
import {DropdownMenu} from 'sentry/components/dropdownMenu';
16-
import {useProjectSeerPreferences} from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences';
16+
import {
17+
makeProjectSeerPreferencesQueryKey,
18+
useProjectSeerPreferences,
19+
} from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences';
1720
import LoadingError from 'sentry/components/loadingError';
1821
import LoadingIndicator from 'sentry/components/loadingIndicator';
1922
import Panel from 'sentry/components/panels/panel';
@@ -290,6 +293,97 @@ export function SeerAutomationProjectList() {
290293
}
291294
}
292295

296+
async function setAllToRecommended() {
297+
addLoadingMessage('Setting all projects to recommended settings...', {
298+
duration: 30000,
299+
});
300+
try {
301+
// Get preferences for all filtered projects to check repo counts
302+
const projectPreferences = await Promise.all(
303+
filteredProjects.map(async project => {
304+
try {
305+
const response = await queryClient.fetchQuery({
306+
queryKey: [
307+
makeProjectSeerPreferencesQueryKey(organization.slug, project.slug),
308+
],
309+
queryFn: () =>
310+
api.requestPromise(
311+
makeProjectSeerPreferencesQueryKey(organization.slug, project.slug)
312+
),
313+
staleTime: 60000,
314+
});
315+
return {
316+
project,
317+
repoCount: response[0].preference?.repositories?.length || 0,
318+
};
319+
} catch (err) {
320+
// If we can't get preferences, assume no repos
321+
return {
322+
project,
323+
repoCount: 0,
324+
};
325+
}
326+
})
327+
);
328+
329+
// Update all projects
330+
await Promise.all(
331+
projectPreferences.map(({project, repoCount}) => {
332+
const updateData: any = {};
333+
334+
if (!project.seerScannerAutomation) {
335+
updateData.seerScannerAutomation = true; // Set scanner to on for all projects
336+
}
337+
338+
// Sets fixes to highly actionable if repos are connected and no automation already set
339+
if (
340+
(!project.autofixAutomationTuning ||
341+
project.autofixAutomationTuning === 'off') &&
342+
repoCount > 0
343+
) {
344+
updateData.autofixAutomationTuning = 'low';
345+
}
346+
347+
// no updates, so don't make a request
348+
if (Object.keys(updateData).length === 0) {
349+
return Promise.resolve();
350+
}
351+
352+
// make the request
353+
return api.requestPromise(`/projects/${organization.slug}/${project.slug}/`, {
354+
method: 'PUT',
355+
data: updateData,
356+
});
357+
})
358+
);
359+
360+
const projectsWithNoRepos = projectPreferences.filter(
361+
({repoCount}) => repoCount === 0
362+
).length;
363+
const updatedProjectsCount = projectPreferences.length;
364+
365+
if (projectsWithNoRepos > 0) {
366+
addSuccessMessage(
367+
`Settings applied to ${updatedProjectsCount} project(s). ${projectsWithNoRepos} project(s) have no repos connected and were skipped.`
368+
);
369+
} else {
370+
addSuccessMessage(`Settings applied to ${updatedProjectsCount} projects.`);
371+
}
372+
} catch (err) {
373+
addErrorMessage('Failed to update some projects');
374+
} finally {
375+
// Invalidate queries for all filtered projects
376+
filteredProjects.forEach(project => {
377+
queryClient.invalidateQueries({
378+
queryKey: makeDetailedProjectQueryKey({
379+
orgSlug: organization.slug,
380+
projectSlug: project.slug,
381+
}),
382+
});
383+
});
384+
}
385+
}
386+
293387
const actionMenuItems = SEER_THRESHOLD_MAP.map(key => ({
294388
key,
295389
label: getSeerDropdownLabel(key),
@@ -312,11 +406,24 @@ export function SeerAutomationProjectList() {
312406
return (
313407
<Fragment>
314408
<SearchWrapper>
315-
<SearchBar
316-
query={search}
317-
onChange={handleSearchChange}
318-
placeholder={t('Search projects')}
319-
/>
409+
<SearchBarWrapper>
410+
<SearchBar
411+
query={search}
412+
onChange={handleSearchChange}
413+
placeholder={t('Search projects')}
414+
/>
415+
</SearchBarWrapper>
416+
<Button
417+
size="sm"
418+
priority="primary"
419+
onClick={setAllToRecommended}
420+
disabled={filteredProjects.length === 0}
421+
title={t(
422+
'For all projects, turns Issue Scans on, and if repos are connected, sets Issue Fixes to run automatically.'
423+
)}
424+
>
425+
{t('Turn On for Recommended Projects')}
426+
</Button>
320427
</SearchWrapper>
321428
<Panel>
322429
<PanelHeader hasButtons>
@@ -390,6 +497,13 @@ export function SeerAutomationProjectList() {
390497

391498
const SearchWrapper = styled('div')`
392499
margin-bottom: ${space(2)};
500+
display: flex;
501+
gap: ${space(2)};
502+
align-items: center;
503+
`;
504+
505+
const SearchBarWrapper = styled('div')`
506+
flex: 1;
393507
`;
394508

395509
const SeerValue = styled('div')`

0 commit comments

Comments
 (0)