diff --git a/static/app/types/prevent.tsx b/static/app/types/prevent.tsx index db8885962e9d42..db3e699bdad367 100644 --- a/static/app/types/prevent.tsx +++ b/static/app/types/prevent.tsx @@ -1,6 +1,8 @@ // Add any new providers here e.g., 'github' | 'bitbucket' | 'gitlab' export type PreventAIProvider = 'github'; +export type Sensitivity = 'low' | 'medium' | 'high' | 'critical'; + interface PreventAIRepo { fullName: string; id: string; @@ -18,7 +20,7 @@ export interface PreventAIOrg { interface PreventAIFeatureConfig { enabled: boolean; triggers: PreventAIFeatureTriggers; - sensitivity?: string; + sensitivity?: Sensitivity; } export interface PreventAIFeatureTriggers { diff --git a/static/app/views/prevent/preventAI/hooks/useUpdatePreventAIFeature.spec.tsx b/static/app/views/prevent/preventAI/hooks/useUpdatePreventAIFeature.spec.tsx index 55f6127e40bb47..217a3b65d6924b 100644 --- a/static/app/views/prevent/preventAI/hooks/useUpdatePreventAIFeature.spec.tsx +++ b/static/app/views/prevent/preventAI/hooks/useUpdatePreventAIFeature.spec.tsx @@ -25,7 +25,6 @@ describe('useUpdatePreventAIFeature', () => { test_generation: { enabled: false, triggers: {on_command_phrase: false, on_ready_for_review: false}, - sensitivity: 'medium', }, bug_prediction: { enabled: false, @@ -46,7 +45,6 @@ describe('useUpdatePreventAIFeature', () => { test_generation: { enabled: false, triggers: {on_command_phrase: false, on_ready_for_review: false}, - sensitivity: 'medium', }, bug_prediction: { enabled: false, @@ -193,19 +191,19 @@ describe('useUpdatePreventAIFeature', () => { expect(feature?.triggers.on_ready_for_review).toBe(true); }); - it('should preserve sensitivity', () => { + it('should update sensitivity', () => { const config = structuredClone(mockOrg.preventAiConfigGithub!); const updatedConfig = makePreventAIConfig(config, { feature: 'bug_prediction', - enabled: false, + enabled: true, orgName: 'org-1', repoName: 'repo-xyz', - trigger: {on_ready_for_review: true}, + sensitivity: 'low', }); const feature = updatedConfig.github_organizations?.['org-1']?.repo_overrides?.['repo-xyz'] ?.bug_prediction; - expect(feature?.sensitivity).toBe('medium'); + expect(feature?.sensitivity).toBe('low'); }); }); }); diff --git a/static/app/views/prevent/preventAI/hooks/useUpdatePreventAIFeature.tsx b/static/app/views/prevent/preventAI/hooks/useUpdatePreventAIFeature.tsx index fc933ffb8c3239..6dfbdf6315771a 100644 --- a/static/app/views/prevent/preventAI/hooks/useUpdatePreventAIFeature.tsx +++ b/static/app/views/prevent/preventAI/hooks/useUpdatePreventAIFeature.tsx @@ -1,6 +1,10 @@ import {updateOrganization} from 'sentry/actionCreators/organizations'; import type {Organization} from 'sentry/types/organization'; -import type {PreventAIConfig, PreventAIFeatureTriggers} from 'sentry/types/prevent'; +import type { + PreventAIConfig, + PreventAIFeatureTriggers, + Sensitivity, +} from 'sentry/types/prevent'; import {fetchMutation, useMutation} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; @@ -10,6 +14,7 @@ interface UpdatePreventAIFeatureParams { orgName: string; // if repoName is provided, edit repo_overrides for that repo, otherwise edit org_defaults repoName?: string; + sensitivity?: Sensitivity; trigger?: Partial; } @@ -73,7 +78,7 @@ export function makePreventAIConfig( featureConfig[params.feature] = { enabled: params.enabled, triggers: {...featureConfig[params.feature].triggers, ...params.trigger}, - sensitivity: featureConfig[params.feature].sensitivity, + sensitivity: params.sensitivity ?? featureConfig[params.feature].sensitivity, }; return updatedConfig; diff --git a/static/app/views/prevent/preventAI/manageReposPanel.spec.tsx b/static/app/views/prevent/preventAI/manageReposPanel.spec.tsx index 561798e5722012..e5abd2b82dc599 100644 --- a/static/app/views/prevent/preventAI/manageReposPanel.spec.tsx +++ b/static/app/views/prevent/preventAI/manageReposPanel.spec.tsx @@ -35,6 +35,7 @@ describe('ManageReposPanel', () => { bug_prediction: { enabled: true, triggers: {on_command_phrase: true, on_ready_for_review: false}, + sensitivity: 'medium', }, test_generation: { enabled: false, @@ -43,6 +44,7 @@ describe('ManageReposPanel', () => { vanilla: { enabled: true, triggers: {on_command_phrase: false, on_ready_for_review: false}, + sensitivity: 'medium', }, }, repo_overrides: {}, @@ -69,9 +71,9 @@ describe('ManageReposPanel', () => { it('shows feature toggles with correct initial state', async () => { render(, {organization: mockOrganization}); - expect(await screen.findByLabelText(/PR Review/i)).toBeChecked(); - expect(await screen.findByLabelText(/Test Generation/i)).not.toBeChecked(); - expect(await screen.findByLabelText(/Error Prediction/i)).toBeChecked(); + expect(await screen.findByLabelText(/Enable PR Review/i)).toBeChecked(); + expect(await screen.findByLabelText(/Enable Test Generation/i)).not.toBeChecked(); + expect(await screen.findByLabelText(/Enable Error Prediction/i)).toBeChecked(); expect( await screen.findByLabelText(/Auto Run on Opened Pull Requests/i) ).not.toBeChecked(); @@ -84,9 +86,9 @@ describe('ManageReposPanel', () => { isLoading: true, }; render(, {organization: mockOrganization}); - expect(await screen.findByLabelText(/PR Review/i)).toBeDisabled(); - expect(await screen.findByLabelText(/Test Generation/i)).toBeDisabled(); - expect(await screen.findByLabelText(/Error Prediction/i)).toBeDisabled(); + expect(await screen.findByLabelText(/Enable PR Review/i)).toBeDisabled(); + expect(await screen.findByLabelText(/Enable Test Generation/i)).toBeDisabled(); + expect(await screen.findByLabelText(/Enable Error Prediction/i)).toBeDisabled(); }); it('shows error message if updateError is present', async () => { @@ -99,6 +101,22 @@ describe('ManageReposPanel', () => { expect(await screen.findByText(/Could not update settings/i)).toBeInTheDocument(); }); + it('shows sensitivity options when feature is enabled', async () => { + render(, {organization: mockOrganization}); + const prReviewCheckbox = await screen.findByLabelText(/Enable PR Review/i); + const errorPredictionCheckbox = await screen.findByLabelText( + /Enable Error Prediction/i + ); + expect(prReviewCheckbox).toBeChecked(); + expect(errorPredictionCheckbox).toBeChecked(); + expect( + await screen.findByTestId(/pr-review-sensitivity-dropdown/i) + ).toBeInTheDocument(); + expect( + await screen.findByTestId(/error-prediction-sensitivity-dropdown/i) + ).toBeInTheDocument(); + }); + describe('getRepoConfig', () => { it('returns repo override config when present', () => { const orgConfig: PreventAIOrgConfig = { diff --git a/static/app/views/prevent/preventAI/manageReposPanel.tsx b/static/app/views/prevent/preventAI/manageReposPanel.tsx index 1f59ae83d2700c..5498741ca9ff48 100644 --- a/static/app/views/prevent/preventAI/manageReposPanel.tsx +++ b/static/app/views/prevent/preventAI/manageReposPanel.tsx @@ -1,5 +1,6 @@ import {Alert} from 'sentry/components/core/alert'; import {Button} from 'sentry/components/core/button'; +import {CompactSelect} from 'sentry/components/core/compactSelect'; import {Flex} from 'sentry/components/core/layout'; import {ExternalLink} from 'sentry/components/core/link'; import {Switch} from 'sentry/components/core/switch'; @@ -9,7 +10,7 @@ import SlideOverPanel from 'sentry/components/slideOverPanel'; import {IconClose} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {type PreventAIOrgConfig} from 'sentry/types/prevent'; -import type {PreventAIFeatureConfigsByName} from 'sentry/types/prevent'; +import type {PreventAIFeatureConfigsByName, Sensitivity} from 'sentry/types/prevent'; import useOrganization from 'sentry/utils/useOrganization'; import {useUpdatePreventAIFeature} from 'sentry/views/prevent/preventAI/hooks/useUpdatePreventAIFeature'; @@ -20,6 +21,31 @@ interface ManageReposPanelProps { repoName: string; } +interface SensitivityOption { + details: string; + label: string; + value: Sensitivity; +} + +const sensitivityOptions: SensitivityOption[] = [ + {value: 'low', label: 'Low', details: 'Post all potential issues for maximum breadth.'}, + { + value: 'medium', + label: 'Medium', + details: 'Post likely issues for a balance of thoroughness and noise.', + }, + { + value: 'high', + label: 'High', + details: 'Post only major issues to highlight most impactful findings.', + }, + { + value: 'critical', + label: 'Critical', + details: 'Post only high-impact, high-sensitivity issues for maximum focus.', + }, +]; + function ManageReposPanel({ collapsed, onClose, @@ -111,9 +137,9 @@ function ManageReposPanel({ justify="between" > - Enable PR Review + {t('Enable PR Review')} - Run when @sentry review is commented on a PR. + {t('Run when @sentry review is commented on a PR.')} + {repoConfig.vanilla.enabled && ( + + + {t('Sensitivity')}} + help={ + + {t('Set the sensitivity level for PR review analysis.')} + + } + alignRight + flexibleControlStateSize + > + + await enableFeature({ + feature: 'vanilla', + enabled: true, + orgName, + repoName, + sensitivity: option.value, + }) + } + aria-label="PR Review Sensitivity" + menuWidth={350} + maxMenuWidth={500} + data-test-id="pr-review-sensitivity-dropdown" + /> + + + + )} {/* Test Generation Feature */} @@ -145,9 +206,9 @@ function ManageReposPanel({ justify="between" > - Enable Test Generation + {t('Enable Test Generation')} - Run when @sentry generate-test is commented on a PR. + {t('Run when @sentry generate-test is commented on a PR.')} - Enable Error Prediction + {t('Enable Error Prediction')} - Allow organization members to review potential bugs. + {t('Allow organization members to review potential bugs.')} { const newValue = !repoConfig.bug_prediction.enabled; - // Enable/disable the main bug prediction feature await enableFeature({ feature: 'bug_prediction', enabled: newValue, @@ -202,15 +262,37 @@ function ManageReposPanel({ /> {repoConfig.bug_prediction.enabled && ( - // width 150% because FieldGroup > FieldDescription has fixed width 50% - - + + + {t('Sensitivity')}} + help={ + + {t('Set the sensitivity level for error prediction.')} + + } + alignRight + flexibleControlStateSize + > + + await enableFeature({ + feature: 'bug_prediction', + enabled: true, + orgName, + repoName, + sensitivity: option.value, + }) + } + aria-label="Error Prediction Sensitivity" + menuWidth={350} + maxMenuWidth={500} + data-test-id="error-prediction-sensitivity-dropdown" + /> + {t('Auto Run on Opened Pull Requests')}} help={ @@ -218,7 +300,7 @@ function ManageReposPanel({ {t('Run when a PR is published, ignoring new pushes.')} } - inline + alignRight flexibleControlStateSize > {t('Run When Mentioned')}} help={ - {t('Run when @sentry review is commented on a PR')} + {t('Run when @sentry review is commented on a PR.')} } - inline + alignRight flexibleControlStateSize >