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