diff --git a/static/app/views/insights/browser/webVitals/utils/useHasSeerWebVitalsSuggestions.tsx b/static/app/views/insights/browser/webVitals/utils/useHasSeerWebVitalsSuggestions.tsx index adec0fb8350efb..d6e638086874c8 100644 --- a/static/app/views/insights/browser/webVitals/utils/useHasSeerWebVitalsSuggestions.tsx +++ b/static/app/views/insights/browser/webVitals/utils/useHasSeerWebVitalsSuggestions.tsx @@ -1,5 +1,6 @@ import {useProjectSeerPreferences} from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences'; import {useOrganizationSeerSetup} from 'sentry/components/events/autofix/useOrganizationSeerSetup'; +import type {Project} from 'sentry/types/project'; import {getSelectedProjectList} from 'sentry/utils/project/useSelectedProjectsHaveField'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; @@ -9,7 +10,7 @@ import useProjects from 'sentry/utils/useProjects'; // - Org has web vitals suggestions feature enabled // - Org has ai features enabled and has given consent // - Project has a github repository set up -export function useHasSeerWebVitalsSuggestions() { +export function useHasSeerWebVitalsSuggestions(selectedProject?: Project) { const organization = useOrganization(); const { @@ -17,7 +18,7 @@ export function useHasSeerWebVitalsSuggestions() { } = usePageFilters(); const {projects: allProjects} = useProjects(); const selectedProjects = getSelectedProjectList(projects, allProjects); - const project = selectedProjects[0]; + const project = selectedProject ?? selectedProjects[0]; // By default, use the first selected project if no project is provided const {preference, codeMappingRepos} = useProjectSeerPreferences(project!); const hasConfiguredRepos = Boolean( diff --git a/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx b/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx index 58bb789f97eee9..4a9ae238dcf368 100644 --- a/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx +++ b/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx @@ -9,6 +9,7 @@ import { userEvent, } from 'sentry-test/reactTestingLibrary'; +import ProjectsStore from 'sentry/stores/projectsStore'; import {IssueTitle} from 'sentry/types/group'; import * as utils from 'sentry/utils/isActiveSuperuser'; import ProjectPerformance, { @@ -49,10 +50,17 @@ const manageDetectorData = [ label: 'HTTP/1.1 Overhead Detection', key: 'http_overhead_detection_enabled', }, + {label: 'Web Vitals Detection', key: 'web_vitals_detection_enabled'}, ]; describe('projectPerformance', () => { - const org = OrganizationFixture({features: ['performance-view']}); + const org = OrganizationFixture({ + features: [ + 'performance-view', + 'performance-web-vitals-seer-suggestions', + 'gen-ai-features', + ], + }); const project = ProjectFixture(); const configUrl = '/projects/org-slug/project-slug/transaction-threshold/configure/'; let getMock: jest.Mock; @@ -65,10 +73,15 @@ describe('projectPerformance', () => { pathname: `/organizations/${org.slug}/settings/projects/${project.slug}/performance/`, query: {}, }, + params: { + orgId: org.slug, + projectId: project.slug, + }, }; beforeEach(() => { MockApiClient.clearMockResponses(); + ProjectsStore.loadInitialData([project]); getMock = MockApiClient.addMockResponse({ url: configUrl, method: 'GET', @@ -97,7 +110,7 @@ describe('projectPerformance', () => { MockApiClient.addMockResponse({ url: '/projects/org-slug/project-slug/', method: 'GET', - body: {}, + body: project, statusCode: 200, }); MockApiClient.addMockResponse({ @@ -112,6 +125,25 @@ describe('projectPerformance', () => { body: {}, statusCode: 200, }); + MockApiClient.addMockResponse({ + url: '/projects/org-slug/project-slug/seer/preferences/', + method: 'GET', + body: { + code_mapping_repos: [ + {provider: 'github', owner: 'owner', name: 'repo', externalId: '123'}, + ], + }, + statusCode: 200, + }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/seer/setup-check/', + method: 'GET', + body: { + setupAcknowledgement: { + orgHasAcknowledged: true, + }, + }, + }); }); it('renders the fields', async () => { @@ -327,6 +359,17 @@ describe('projectPerformance', () => { index: 1, }, }, + { + title: IssueTitle.WEB_VITALS, + threshold: DetectorConfigCustomer.WEB_VITALS_COUNT, + allowedValues: allowedCountValues, + defaultValue: 10, + newValue: 20, + sliderIdentifier: { + label: 'Minimum Sample Count', + index: 0, + }, + }, ])( 'renders detector thresholds settings for $title issue', async ({ @@ -350,6 +393,7 @@ describe('projectPerformance', () => { large_http_payload_detection_enabled: true, n_plus_one_api_calls_detection_enabled: true, consecutive_http_spans_detection_enabled: true, + web_vitals_detection_enabled: true, }; const performanceIssuesGetMock = MockApiClient.addMockResponse({ url: '/projects/org-slug/project-slug/performance-issues/configure/', @@ -463,7 +507,11 @@ describe('projectPerformance', () => { render(, { organization: OrganizationFixture({ - features: ['performance-view'], + features: [ + 'performance-view', + 'performance-web-vitals-seer-suggestions', + 'gen-ai-features', + ], }), initialRouterConfig, }); @@ -526,7 +574,11 @@ describe('projectPerformance', () => { render(, { organization: OrganizationFixture({ - features: ['performance-view'], + features: [ + 'performance-view', + 'performance-web-vitals-seer-suggestions', + 'gen-ai-features', + ], access: ['project:read'], }), initialRouterConfig, diff --git a/static/app/views/settings/projectPerformance/projectPerformance.tsx b/static/app/views/settings/projectPerformance/projectPerformance.tsx index c571c548520415..ba5f1d833a1a01 100644 --- a/static/app/views/settings/projectPerformance/projectPerformance.tsx +++ b/static/app/views/settings/projectPerformance/projectPerformance.tsx @@ -41,6 +41,7 @@ import useApi from 'sentry/utils/useApi'; import {useDetailedProject} from 'sentry/utils/useDetailedProject'; import useOrganization from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; +import {useHasSeerWebVitalsSuggestions} from 'sentry/views/insights/browser/webVitals/utils/useHasSeerWebVitalsSuggestions'; import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; import {ProjectPermissionAlert} from 'sentry/views/settings/project/projectPermissionAlert'; @@ -90,6 +91,7 @@ enum DetectorConfigAdmin { TRANSACTION_DURATION_REGRESSION_ENABLED = 'transaction_duration_regression_detection_enabled', FUNCTION_DURATION_REGRESSION_ENABLED = 'function_duration_regression_detection_enabled', DB_QUERY_INJECTION_ENABLED = 'db_query_injection_detection_enabled', + WEB_VITALS_ENABLED = 'web_vitals_detection_enabled', } export enum DetectorConfigCustomer { @@ -108,6 +110,7 @@ export enum DetectorConfigCustomer { CONSECUTIVE_HTTP_MIN_TIME_SAVED = 'consecutive_http_spans_min_time_saved_threshold', HTTP_OVERHEAD_REQUEST_DELAY = 'http_request_delay_threshold', SQL_INJECTION_QUERY_VALUE_LENGTH = 'sql_injection_query_value_length_threshold', + WEB_VITALS_COUNT = 'web_vitals_count', } type ProjectThreshold = { @@ -177,6 +180,8 @@ function ProjectPerformance() { orgSlug: organization.slug, }); + const hasWebVitalsSeerSuggestions = useHasSeerWebVitalsSuggestions(project); + const { data: threshold, isPending: isPendingThreshold, @@ -549,6 +554,23 @@ function ProjectPerformance() { 'issue-query-injection-vulnerability-visible' ), }, + [IssueTitle.WEB_VITALS]: { + name: DetectorConfigAdmin.WEB_VITALS_ENABLED, + type: 'boolean', + label: t('Web Vitals Detection'), + defaultValue: true, + onChange: value => { + setApiQueryData( + queryClient, + getPerformanceIssueSettingsQueryKey(organization.slug, projectSlug), + data => ({ + ...data!, + web_vitals_detection_enabled: value, + }) + ); + }, + visible: hasWebVitalsSeerSuggestions, + }, }; const performanceRegressionAdminFields: Field[] = [ @@ -957,6 +979,32 @@ function ProjectPerformance() { ], initiallyCollapsed: issueType !== IssueType.QUERY_INJECTION_VULNERABILITY, }, + { + title: IssueTitle.WEB_VITALS, + fields: [ + { + name: DetectorConfigCustomer.WEB_VITALS_COUNT, + type: 'range', + label: t('Minimum Sample Count'), + defaultValue: 10, + help: t( + 'Setting the value to 10, means that web vital issues will only be created if there are at least 10 samples of the web vital type.' + ), + tickValues: [0, allowedCountValues.length - 1], + allowedValues: allowedCountValues, + showTickLabels: true, + formatLabel: formatCount, + flexibleControlStateSize: true, + disabled: !( + hasAccess && + performanceIssueSettings[DetectorConfigAdmin.WEB_VITALS_ENABLED] + ), + disabledReason, + visible: hasWebVitalsSeerSuggestions, + }, + ], + initiallyCollapsed: issueType !== IssueType.WEB_VITALS, + }, ]; // If the organization can manage detectors, add the admin field to the existing settings