From 6e0e4e56bea302d943d7eba53a68466b11eb96cb Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Wed, 5 Nov 2025 18:14:32 -0500 Subject: [PATCH 1/2] performance detector settings in the frontend for web vitals --- .../projectPerformance.spec.tsx | 21 +++++++-- .../projectPerformance/projectPerformance.tsx | 47 +++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx b/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx index 58bb789f97eee9..dabeacf624687c 100644 --- a/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx +++ b/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx @@ -49,10 +49,13 @@ 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'], + }); const project = ProjectFixture(); const configUrl = '/projects/org-slug/project-slug/transaction-threshold/configure/'; let getMock: jest.Mock; @@ -327,6 +330,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 +364,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 +478,7 @@ describe('projectPerformance', () => { render(, { organization: OrganizationFixture({ - features: ['performance-view'], + features: ['performance-view', 'performance-web-vitals-seer-suggestions'], }), initialRouterConfig, }); @@ -526,7 +541,7 @@ describe('projectPerformance', () => { render(, { organization: OrganizationFixture({ - features: ['performance-view'], + features: ['performance-view', 'performance-web-vitals-seer-suggestions'], access: ['project:read'], }), initialRouterConfig, diff --git a/static/app/views/settings/projectPerformance/projectPerformance.tsx b/static/app/views/settings/projectPerformance/projectPerformance.tsx index c571c548520415..b764162030c084 100644 --- a/static/app/views/settings/projectPerformance/projectPerformance.tsx +++ b/static/app/views/settings/projectPerformance/projectPerformance.tsx @@ -90,6 +90,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 +109,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 = { @@ -549,6 +551,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: organization.features.includes('performance-web-vitals-seer-suggestions'), + }, }; const performanceRegressionAdminFields: Field[] = [ @@ -957,6 +976,34 @@ 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: organization.features.includes( + 'performance-web-vitals-seer-suggestions' + ), + }, + ], + initiallyCollapsed: issueType !== IssueType.WEB_VITALS, + }, ]; // If the organization can manage detectors, add the admin field to the existing settings From 216e962ec11fbabfd3c360e9ab1ae0940935f7dd Mon Sep 17 00:00:00 2001 From: Edward Gou Date: Fri, 7 Nov 2025 11:02:37 -0500 Subject: [PATCH 2/2] add more checks --- .../utils/useHasSeerWebVitalsSuggestions.tsx | 5 ++- .../projectPerformance.spec.tsx | 45 +++++++++++++++++-- .../projectPerformance/projectPerformance.tsx | 9 ++-- 3 files changed, 49 insertions(+), 10 deletions(-) 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 dabeacf624687c..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, { @@ -54,7 +55,11 @@ const manageDetectorData = [ describe('projectPerformance', () => { const org = OrganizationFixture({ - features: ['performance-view', 'performance-web-vitals-seer-suggestions'], + features: [ + 'performance-view', + 'performance-web-vitals-seer-suggestions', + 'gen-ai-features', + ], }); const project = ProjectFixture(); const configUrl = '/projects/org-slug/project-slug/transaction-threshold/configure/'; @@ -68,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', @@ -100,7 +110,7 @@ describe('projectPerformance', () => { MockApiClient.addMockResponse({ url: '/projects/org-slug/project-slug/', method: 'GET', - body: {}, + body: project, statusCode: 200, }); MockApiClient.addMockResponse({ @@ -115,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 () => { @@ -478,7 +507,11 @@ describe('projectPerformance', () => { render(, { organization: OrganizationFixture({ - features: ['performance-view', 'performance-web-vitals-seer-suggestions'], + features: [ + 'performance-view', + 'performance-web-vitals-seer-suggestions', + 'gen-ai-features', + ], }), initialRouterConfig, }); @@ -541,7 +574,11 @@ describe('projectPerformance', () => { render(, { organization: OrganizationFixture({ - features: ['performance-view', 'performance-web-vitals-seer-suggestions'], + 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 b764162030c084..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'; @@ -179,6 +180,8 @@ function ProjectPerformance() { orgSlug: organization.slug, }); + const hasWebVitalsSeerSuggestions = useHasSeerWebVitalsSuggestions(project); + const { data: threshold, isPending: isPendingThreshold, @@ -566,7 +569,7 @@ function ProjectPerformance() { }) ); }, - visible: organization.features.includes('performance-web-vitals-seer-suggestions'), + visible: hasWebVitalsSeerSuggestions, }, }; @@ -997,9 +1000,7 @@ function ProjectPerformance() { performanceIssueSettings[DetectorConfigAdmin.WEB_VITALS_ENABLED] ), disabledReason, - visible: organization.features.includes( - 'performance-web-vitals-seer-suggestions' - ), + visible: hasWebVitalsSeerSuggestions, }, ], initiallyCollapsed: issueType !== IssueType.WEB_VITALS,