From 564ac2f58969488cc9c95c49cf7aa3b298aed767 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 20:54:33 +0000 Subject: [PATCH] ref(perf): Remove projects:workflow-engine-performance-detectors flag Remove the flagpole-disabled projects:workflow-engine-performance-detectors feature flag and the workflow-engine performance-detector migration code it gated. The flag was never enabled (it is not present in sentry-options-automator), so behavior now defaults to the flag being off: performance detection always uses ProjectOption-based settings, and the settings endpoint no longer syncs to or auto-creates WFE Detectors. The shared detector type mappings remain, as they are used elsewhere. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01MTV5tNMyvqCcf2L7B6p5c7 --- src/sentry/features/temporary.py | 2 - .../issue_detection/performance_detection.py | 315 +----------------- .../project_performance_issue_settings.py | 8 +- .../workflow_engine/defaults/detectors.py | 54 --- .../test_performance_detection.py | 267 +-------------- ...test_project_performance_issue_settings.py | 268 --------------- .../receivers/test_project_detectors.py | 81 +---- 7 files changed, 15 insertions(+), 980 deletions(-) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index d3aa57b4673c..5ead9da2c9e7 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -494,8 +494,6 @@ def register_temporary_features(manager: FeatureManager) -> None: # Enable lightweight RCA clustering write path (generate embeddings on new issues) manager.add("organizations:supergroups-lightweight-rca-clustering-write", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) - manager.add("projects:workflow-engine-performance-detectors", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) - manager.add("organizations:relay-generate-billing-outcome", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) manager.add("organizations:claude-code-vault-reuse", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) diff --git a/src/sentry/issue_detection/performance_detection.py b/src/sentry/issue_detection/performance_detection.py index f9e1fd15543b..b9eb6bf4dd02 100644 --- a/src/sentry/issue_detection/performance_detection.py +++ b/src/sentry/issue_detection/performance_detection.py @@ -4,15 +4,13 @@ import random from collections.abc import Sequence from dataclasses import dataclass -from enum import IntEnum from typing import Any import sentry_sdk -from django.db import router, transaction from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import Span -from sentry import features, options, projectoptions +from sentry import options, projectoptions from sentry.models.options.project_option import ProjectOption from sentry.models.organization import Organization from sentry.models.project import Project @@ -21,7 +19,6 @@ from sentry.utils.event import is_event_from_browser_javascript_sdk from sentry.utils.event_frames import get_sdk_name from sentry.utils.tracing import set_span_tag, start_span -from sentry.workflow_engine.models import Detector from .base import DetectorType, PerformanceDetector from .detectors.consecutive_db_detector import ConsecutiveDBSpanDetector @@ -144,34 +141,14 @@ def detect_performance_problems( return [] -class SettingsMode(IntEnum): - """ - Performance detection settings modes for gradual migration from ProjectOptions to WFE Detectors. - - LEGACY: Use system defaults + well-known defaults + ProjectOption values. - Traditional behavior, no WFE Detector involvement. - - COMPARE: Generate both legacy and WFE settings, log differences for monitoring, - return legacy settings. Used to detect config drift during migration. - - WFE: Hybrid mode combining WFE Detector configs with ProjectOptions. - For detectors with WFE Detector objects: use WFE config (falls back to system defaults). - For detectors without WFE Detector objects: use ProjectOptions. - """ - - LEGACY = 0 - COMPARE = 1 - WFE = 2 - - def get_merged_settings( project: Project | None = None, - settings_mode: SettingsMode = SettingsMode.LEGACY, ) -> dict[str | Any, Any]: """ - Get performance detection settings based on the specified mode. + Get performance detection settings. - Returns a dict with all performance detection settings. + Returns a dict with all performance detection settings, built from system + defaults, well-known project defaults, and stored ProjectOption values. """ system_settings = { "n_plus_one_db_count": options.get("performance.issues.n_plus_one_db.count_threshold"), @@ -255,318 +232,46 @@ def get_merged_settings( else DEFAULT_PROJECT_PERFORMANCE_DETECTION_SETTINGS ) - legacy_settings = { + return { **system_settings, **default_project_settings, **project_option_settings, } - # If feature flag is disabled, always use LEGACY mode because we can't decide check the feature - # flag without a Project. - # TODO: WFE config should fall back to DEFAULT_PROJECT_PERFORMANCE_DETECTION_SETTINGS without a Project. - if not project or not features.has("projects:workflow-engine-performance-detectors", project): - settings_mode = SettingsMode.LEGACY - - if settings_mode == SettingsMode.LEGACY: - return legacy_settings - - assert project is not None - - # Get WFE-specific settings (analogous to project_option_settings) - wfe_option_settings, wfe_managed_keys = _build_wfe_settings(project.id) - - # Build complete WFE settings by merging in order - wfe_settings = {**system_settings, **default_project_settings, **wfe_option_settings} - - if settings_mode == SettingsMode.COMPARE: - # Compare and log differences, but return legacy settings - _log_settings_diff( - project.id, - project.organization_id, - wfe_managed_keys, - legacy=legacy_settings, - wfe=wfe_settings, - ) - return legacy_settings - elif settings_mode == SettingsMode.WFE: - # Build hybrid settings with clear priority: - # 1. Base: system + well-known defaults - # 2. For unmanaged keys: use project_option_settings - # 3. For managed keys: use wfe_option_settings (if specified, else base) - hybrid_settings = {**system_settings, **default_project_settings} - - # Apply project options only for keys NOT managed by WFE - for key, value in project_option_settings.items(): - if key not in wfe_managed_keys: - hybrid_settings[key] = value - - # Apply WFE options for managed keys (overwrites base if specified) - hybrid_settings.update(wfe_option_settings) - - return hybrid_settings - - # Unreachable: all modes should be handled above - raise ValueError(f"Unknown settings_mode: {settings_mode}") - - -def _build_wfe_settings(project_id: int) -> tuple[dict[str, Any], set[str]]: - """ - Build WFE-specific settings (Detector.config-derived version of sentry:performance_issue_settings). - The returned settings dict will only contain keys that are managed by WFE Detectors. - - Returns: - - wfe_option_settings: Dict with only WFE-specified config values - - wfe_managed_keys: Set of all setting keys managed by WFE; wfe_option_setings keys are a subset of this - """ - wfe_option_settings: dict[str, Any] = {} - wfe_managed_keys: set[str] = set() - wfe_configs = _get_wfe_detector_configs(project_id) - - # Generate settings only for detectors that have WFE Detector objects - for detector_type, wfe_config in wfe_configs.items(): - mapping = PERFORMANCE_DETECTOR_CONFIG_MAPPINGS[detector_type] - - # All Detectors provide a detection_enabled key. - wfe_managed_keys.add(mapping.detection_enabled_key) - wfe_option_settings[mapping.detection_enabled_key] = wfe_config["detection_enabled"] - - # For each config field: only set if present in WFE config - for field_name, option_key in mapping.option_keys.items(): - wfe_managed_keys.add(option_key) - - if field_name in wfe_config: - # TODO: Consider using None so we don't have to track managed keys. - wfe_option_settings[option_key] = wfe_config[field_name] - - return wfe_option_settings, wfe_managed_keys - - -def _log_settings_diff( - project_id: int, - organization_id: int, - wfe_managed_keys: set[str], - *, - legacy: dict[str, Any], - wfe: dict[str, Any], -) -> None: - """ - Compare legacy and WFE settings and log any differences. - - Only compares keys that are managed by WFE Detectors (detectors that have - WFE Detector objects). This avoids noise from detectors that haven't been - migrated yet. - - This is used in COMPARE mode to monitor config drift between - ProjectOption-based settings and WFE Detector-based settings. - """ - differences: dict[str, dict[str, Any]] = {} - - # Only compare keys that are managed by WFE Detectors - for key in wfe_managed_keys: - legacy_value = legacy.get(key) - wfe_value = wfe.get(key) - - if legacy_value != wfe_value: - differences[key] = { - "legacy": legacy_value, - "wfe": wfe_value, - } - - if differences: - logger.info( - "performance_detector.settings_diff", - extra={ - "project_id": project_id, - "organization_id": organization_id, - "differences": differences, - "diff_count": len(differences), - }, - ) - - -def _get_wfe_detector_configs(project_id: int) -> dict[DetectorType, dict[str, Any]]: - """ - Fetch WFE Detector configs for performance detectors. - - Returns a dict mapping DetectorType to config values from Detector.config - that should be used INSTEAD OF ProjectOption values for that detector. - - When a WFE Detector exists, its config is always used (regardless of enabled state). - The Detector.enabled field controls detection_enabled, not whether we use WFE config. - """ - # Get all WFE detector types we can map. - wfe_types = [ - mapping.wfe_detector_type for mapping in PERFORMANCE_DETECTOR_CONFIG_MAPPINGS.values() - ] - - # Per DetectorType configs - wfe_configs: dict[DetectorType, dict[str, Any]] = {} - for detector in Detector.objects.filter( - project_id=project_id, - type__in=wfe_types, - ): - # Map WFE detector type to our DetectorType enum - detector_type = WFE_DETECTOR_TYPE_TO_CONFIG_MAPPING.get(detector.type) - if not detector_type: - continue - - # Store raw Detector.config plus detection_enabled from Detector.enabled - wfe_configs[detector_type] = { - "detection_enabled": detector.enabled, - **detector.config, # Include all fields from Detector.config - } - - return wfe_configs - - -def _get_wfe_detectors_by_type(project_id: int) -> dict[str, Detector]: - """Fetch all performance WFE detectors for a project, keyed by detector type.""" - return { - d.type: d - for d in Detector.objects.filter( - project_id=project_id, type__in=PERFORMANCE_WFE_DETECTOR_TYPES - ) - } - - -def sync_project_options_to_wfe_detectors( - project_id: int, settings_data: dict[str, Any] -) -> dict[DetectorType, bool]: - """ - Sync ProjectOption settings to WFE Detector configs. - - For each detector type with WFE mapping, updates an existing Detector with: - - Detector.enabled from detection_enabled setting - - Detector.config from mapped config fields - - Returns dict of DetectorType -> bool indicating which detectors were updated. - """ - updated: dict[DetectorType, bool] = {} - existing_detectors = _get_wfe_detectors_by_type(project_id) - - for detector_type, mapping in PERFORMANCE_DETECTOR_CONFIG_MAPPINGS.items(): - detector = existing_detectors.get(mapping.wfe_detector_type) - if not detector: - continue - - # Extract config fields (option_keys maps: wfe_field -> project_option_key) - new_config: dict[str, Any] = {} - for wfe_field, project_option_key in mapping.option_keys.items(): - if project_option_key in settings_data: - new_config[wfe_field] = settings_data[project_option_key] - - new_enabled: bool | None = None - if mapping.detection_enabled_key in settings_data: - new_enabled = settings_data[mapping.detection_enabled_key] - - if new_enabled is None and not new_config: - continue - - if new_enabled is not None and detector.enabled != new_enabled: - detector.update(enabled=new_enabled) - updated[detector_type] = True - - if new_config: - merged_config = {**detector.config, **new_config} - if detector.config != merged_config: - detector.config = merged_config - detector.save(update_fields=["config"]) - updated[detector_type] = True - - return updated - - -def reset_wfe_detector_configs(project_id: int) -> dict[DetectorType, bool]: - """ - Reset WFE Detector configs to defaults by clearing config on enabled detectors. - - Used when DELETE is called on performance settings. For enabled detectors, clears - custom config values (falls back to system defaults). For disabled detectors, - leaves config unchanged (assumes already in sync with ProjectOption). - - Returns dict of DetectorType -> bool indicating which detectors were updated. - """ - updated: dict[DetectorType, bool] = {} - existing_detectors = _get_wfe_detectors_by_type(project_id) - - for detector_type, mapping in PERFORMANCE_DETECTOR_CONFIG_MAPPINGS.items(): - detector = existing_detectors.get(mapping.wfe_detector_type) - if not detector: - continue - - # Clear config on enabled detectors (reset to defaults) - if detector.enabled and detector.config: - detector.config = {} - detector.save(update_fields=["config"]) - updated[detector_type] = True - - return updated - SETTINGS_PROJECT_OPTION_KEY = "sentry:performance_issue_settings" -@transaction.atomic(using=router.db_for_write(Detector)) -def update_performance_settings( - project: Project, - settings: dict[str, Any], - sync_detectors: bool = False, -) -> dict[DetectorType, bool]: +def update_performance_settings(project: Project, settings: dict[str, Any]) -> None: """ - Write performance issue settings to ProjectOption and optionally sync to WFE Detectors. + Write performance issue settings to ProjectOption. Args: project: The project to update settings for settings: The full settings dict to store - sync_detectors: Whether to sync settings to WFE Detectors - - Returns: - Dict of DetectorType -> bool indicating which detectors were updated - (empty if sync_detectors is False) """ # TODO: Fix potential cache inconsistency here. See ISWF-2156. project.update_option(SETTINGS_PROJECT_OPTION_KEY, settings) - if sync_detectors: - return sync_project_options_to_wfe_detectors(project.id, settings) - - return {} - -@transaction.atomic(using=router.db_for_write(Detector)) -def reset_performance_settings( - project: Project, - unchanged_options: dict[str, Any], - sync_detectors: bool = False, -) -> dict[DetectorType, bool]: +def reset_performance_settings(project: Project, unchanged_options: dict[str, Any]) -> None: """ - Atomically reset ProjectOption and optionally reset WFE Detector configs. + Reset ProjectOption performance issue settings, preserving the given options. Args: project: The project to reset settings for unchanged_options: Settings that should be preserved (not reset) - sync_detectors: Whether to reset WFE Detector configs - - Returns: - Dict of DetectorType -> bool indicating which detectors were updated - (empty if sync_detectors is False) """ project.update_option(SETTINGS_PROJECT_OPTION_KEY, unchanged_options) - if sync_detectors: - return reset_wfe_detector_configs(project.id) - - return {} - # Gets the thresholds to perform performance detection. # Duration thresholds are in milliseconds. # Allowed span ops are allowed span prefixes. (eg. 'http' would work for a span with 'http.client' as its op) def get_detection_settings( project: Project | None = None, - settings_mode: SettingsMode = SettingsMode.LEGACY, ) -> dict[DetectorType, dict[str, Any]]: - settings = get_merged_settings(project, settings_mode) + settings = get_merged_settings(project) return { DetectorType.SLOW_DB_QUERY: { diff --git a/src/sentry/issues/endpoints/project_performance_issue_settings.py b/src/sentry/issues/endpoints/project_performance_issue_settings.py index 62c60739dc9c..cdb88bfb5375 100644 --- a/src/sentry/issues/endpoints/project_performance_issue_settings.py +++ b/src/sentry/issues/endpoints/project_performance_issue_settings.py @@ -350,10 +350,7 @@ def put(self, request: Request, project: Project) -> Response: status=status.HTTP_403_FORBIDDEN, ) - sync_detectors = features.has("projects:workflow-engine-performance-detectors", project) - update_performance_settings( - project, {**current_settings, **data}, sync_detectors=sync_detectors - ) + update_performance_settings(project, {**current_settings, **data}) if body_has_admin_options or body_has_management_options: self.create_audit_entry( @@ -389,7 +386,6 @@ def delete(self, request: Request, project: Project) -> Response: for option, value in project_overrides.items() if option in to_preserve } - sync_detectors = features.has("projects:workflow-engine-performance-detectors", project) - reset_performance_settings(project, unchanged_options, sync_detectors=sync_detectors) + reset_performance_settings(project, unchanged_options) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/sentry/workflow_engine/defaults/detectors.py b/src/sentry/workflow_engine/defaults/detectors.py index 25d40f3c3e23..e23ab2532030 100644 --- a/src/sentry/workflow_engine/defaults/detectors.py +++ b/src/sentry/workflow_engine/defaults/detectors.py @@ -1,12 +1,9 @@ import logging -from collections.abc import Mapping from datetime import timedelta -from functools import cache from django.db import router, transaction from rest_framework import status -from sentry import features from sentry.api.exceptions import SentryAPIException from sentry.grouping.grouptype import ErrorGroupType from sentry.incidents.grouptype import MetricIssue @@ -17,7 +14,6 @@ from sentry.issues import grouptype from sentry.locks import locks from sentry.models.project import Project -from sentry.projectoptions.defaults import DEFAULT_PROJECT_PERFORMANCE_DETECTION_SETTINGS from sentry.seer.anomaly_detection.store_data_workflow_engine import send_new_detector_data from sentry.seer.anomaly_detection.types import ( AnomalyDetectionSeasonality, @@ -52,33 +48,6 @@ logger = logging.getLogger(__name__) -@cache -def get_disabled_platforms_by_detector_type() -> Mapping[str, frozenset[str]]: - """ - Map WFE detector types to platforms where they should be disabled by default. - Derives from DEFAULT_DETECTOR_DISABLING_CONFIGS using the detection_enabled_key. - """ - from sentry.issue_detection.detectors.disable_detectors import ( - DEFAULT_DETECTOR_DISABLING_CONFIGS, - ) - - disabled_by_detector_type: dict[str, frozenset[str]] = {} - - for disable_config in DEFAULT_DETECTOR_DISABLING_CONFIGS: - detector_option_key = disable_config["detector_project_option"] - languages_to_disable = disable_config["languages_to_disable"] - - # Find matching WFE detector via detection_enabled_key - for mapping in PERFORMANCE_DETECTOR_CONFIG_MAPPINGS.values(): - if mapping.detection_enabled_key == detector_option_key: - disabled_by_detector_type[mapping.wfe_detector_type] = frozenset( - languages_to_disable - ) - break - - return disabled_by_detector_type - - class UnableToAcquireLockApiError(SentryAPIException): status_code = status.HTTP_400_BAD_REQUEST code = "unable_to_acquire_lock" @@ -256,31 +225,8 @@ def ensure_default_anomaly_detector( raise UnableToAcquireLockApiError -def ensure_performance_detectors(project: Project) -> dict[str, Detector]: - if not features.has("projects:workflow-engine-performance-detectors", project): - return {} - - disabled_platforms_map = get_disabled_platforms_by_detector_type() - - detectors = {} - for mapping in PERFORMANCE_DETECTOR_CONFIG_MAPPINGS.values(): - detector_type = mapping.wfe_detector_type - - # Determine initial enabled state based on platform and default settings - disabled_platforms = disabled_platforms_map.get(detector_type, frozenset()) - default_enabled = DEFAULT_PROJECT_PERFORMANCE_DETECTION_SETTINGS[ - mapping.detection_enabled_key - ] - enabled = (project.platform not in disabled_platforms) and default_enabled - - detectors[detector_type] = _ensure_detector(project, detector_type, default_enabled=enabled) - - return detectors - - def ensure_default_detectors(project: Project) -> dict[str, Detector]: detectors: dict[str, Detector] = {} detectors[ErrorGroupType.slug] = _ensure_detector(project, ErrorGroupType.slug) detectors[IssueStreamGroupType.slug] = _ensure_detector(project, IssueStreamGroupType.slug) - detectors.update(ensure_performance_detectors(project)) return detectors diff --git a/tests/sentry/issue_detection/test_performance_detection.py b/tests/sentry/issue_detection/test_performance_detection.py index fdc17ed8ee00..056fa6b613c8 100644 --- a/tests/sentry/issue_detection/test_performance_detection.py +++ b/tests/sentry/issue_detection/test_performance_detection.py @@ -11,13 +11,10 @@ from sentry.issue_detection.performance_detection import ( PERFORMANCE_DETECTOR_CONFIG_MAPPINGS, SETTINGS_PROJECT_OPTION_KEY, - SettingsMode, _detect_performance_problems, detect_performance_problems, get_detection_settings, reset_performance_settings, - reset_wfe_detector_configs, - sync_project_options_to_wfe_detectors, update_performance_settings, ) from sentry.issue_detection.performance_problem import PerformanceProblem @@ -25,14 +22,12 @@ from sentry.issues.grouptype import ( PerformanceConsecutiveHTTPQueriesGroupType, PerformanceNPlusOneGroupType, - PerformanceSlowDBQueryGroupType, registry, ) from sentry.testutils.cases import TestCase from sentry.testutils.helpers import override_options from sentry.testutils.issue_detection.event_generators import get_event from sentry.testutils.issue_detection.experiments import exclude_experimental_detectors -from sentry.workflow_engine.models.detector import Detector BASE_DETECTOR_OPTIONS = { "performance.issues.n_plus_one_db.problem-creation": 1.0, @@ -600,95 +595,6 @@ def setUp(self) -> None: super().setUp() self.project = self.create_project() - def test_wfe_detector_enabled_uses_wfe_config(self) -> None: - self.create_detector( - project=self.project, - type=PerformanceSlowDBQueryGroupType.slug, - name="Test SlowDB Detector", - enabled=True, - config={"duration_threshold": 5000}, - ) - - with self.feature("projects:workflow-engine-performance-detectors"): - settings = get_detection_settings(self.project, settings_mode=SettingsMode.WFE) - - assert settings[DetectorType.SLOW_DB_QUERY]["duration_threshold"] == 5000 - assert settings[DetectorType.SLOW_DB_QUERY]["detection_enabled"] is True - - def test_wfe_detector_disabled_still_uses_wfe_config(self) -> None: - self.create_detector( - project=self.project, - type=PerformanceSlowDBQueryGroupType.slug, - name="Test SlowDB Detector", - enabled=False, - config={"duration_threshold": 5000}, - ) - - projectoptions.set( - self.project, - "sentry:performance_issue_settings", - { - "slow_db_queries_detection_enabled": True, - "slow_db_query_duration_threshold": 2000, - }, - ) - - with self.feature("projects:workflow-engine-performance-detectors"): - settings = get_detection_settings(self.project, settings_mode=SettingsMode.WFE) - - assert settings[DetectorType.SLOW_DB_QUERY]["duration_threshold"] == 5000 - assert settings[DetectorType.SLOW_DB_QUERY]["detection_enabled"] is False - - def test_no_wfe_detector_uses_legacy_config(self) -> None: - projectoptions.set( - self.project, - "sentry:performance_issue_settings", - { - "slow_db_queries_detection_enabled": True, - "slow_db_query_duration_threshold": 2000, - }, - ) - - with self.feature("projects:workflow-engine-performance-detectors"): - settings = get_detection_settings(self.project, settings_mode=SettingsMode.WFE) - - assert settings[DetectorType.SLOW_DB_QUERY]["duration_threshold"] == 2000 - assert settings[DetectorType.SLOW_DB_QUERY]["detection_enabled"] is True - - def test_wfe_detector_missing_field_uses_system_default(self) -> None: - projectoptions.set( - self.project, - "sentry:performance_issue_settings", - {"slow_db_query_duration_threshold": 2000}, - ) - - self.create_detector( - project=self.project, - type=PerformanceSlowDBQueryGroupType.slug, - name="Test SlowDB Detector", - enabled=True, - config={}, - ) - - with self.feature("projects:workflow-engine-performance-detectors"): - settings = get_detection_settings(self.project, settings_mode=SettingsMode.WFE) - - assert settings[DetectorType.SLOW_DB_QUERY]["duration_threshold"] == 1000 - assert settings[DetectorType.SLOW_DB_QUERY]["detection_enabled"] is True - - def test_feature_flag_disabled_uses_legacy_config(self) -> None: - self.create_detector( - project=self.project, - type=PerformanceSlowDBQueryGroupType.slug, - name="Test SlowDB Detector", - enabled=True, - config={"duration_threshold": 5000}, - ) - - settings = get_detection_settings(self.project, settings_mode=SettingsMode.WFE) - - assert settings[DetectorType.SLOW_DB_QUERY]["duration_threshold"] == 1000 - def test_all_wfe_detector_types_are_registered_group_types(self) -> None: for mapping in PERFORMANCE_DETECTOR_CONFIG_MAPPINGS.values(): group_type = registry.get_by_slug(mapping.wfe_detector_type) @@ -697,133 +603,6 @@ def test_all_wfe_detector_types_are_registered_group_types(self) -> None: ) -@pytest.mark.django_db -class WFEDetectorSyncTest(TestCase): - def setUp(self) -> None: - super().setUp() - self.project = self.create_project() - - def test_sync_skips_nonexistent_detector(self) -> None: - settings_data = { - "slow_db_queries_detection_enabled": True, - "slow_db_query_duration_threshold": 3000, - } - - updated = sync_project_options_to_wfe_detectors(self.project, settings_data) - - assert DetectorType.SLOW_DB_QUERY not in updated - assert not Detector.objects.filter( - project=self.project, type="performance_slow_db_query" - ).exists() - - def test_sync_updates_existing_detector(self) -> None: - detector = self.create_detector( - project=self.project, - type="performance_slow_db_query", - name="Test", - enabled=False, - config={"duration_threshold": 1000}, - ) - - settings_data = { - "slow_db_queries_detection_enabled": True, - "slow_db_query_duration_threshold": 5000, - } - - updated = sync_project_options_to_wfe_detectors(self.project, settings_data) - - assert DetectorType.SLOW_DB_QUERY in updated - - detector.refresh_from_db() - assert detector.enabled is True - assert detector.config["duration_threshold"] == 5000 - - def test_sync_skips_update_when_config_already_matches(self) -> None: - self.create_detector( - project=self.project, - type="performance_slow_db_query", - name="Test", - enabled=True, - config={"duration_threshold": 5000, "some_other_field": "preserved"}, - ) - - settings_data = { - "slow_db_queries_detection_enabled": True, - "slow_db_query_duration_threshold": 5000, - } - - updated = sync_project_options_to_wfe_detectors(self.project, settings_data) - - # Should not be marked as updated since merged config is identical - assert DetectorType.SLOW_DB_QUERY not in updated - - def test_sync_skips_hardcoded_fields(self) -> None: - detector = self.create_detector( - project=self.project, - type="performance_slow_db_query", - name="Test", - enabled=False, - config={}, - ) - - settings_data = { - "slow_db_queries_detection_enabled": True, - "slow_db_query_duration_threshold": 2000, - } - - sync_project_options_to_wfe_detectors(self.project, settings_data) - - detector.refresh_from_db() - assert "allowed_span_ops" not in detector.config - assert detector.config["duration_threshold"] == 2000 - - -@pytest.mark.django_db -class WFEDetectorResetTest(TestCase): - def setUp(self) -> None: - super().setUp() - self.project = self.create_project() - - def test_reset_clears_enabled_detector_config(self) -> None: - detector = self.create_detector( - project=self.project, - type="performance_slow_db_query", - name="Test", - enabled=True, - config={"duration_threshold": 5000}, - ) - - reset_wfe_detector_configs(self.project) - - detector.refresh_from_db() - assert detector.config == {} - assert detector.enabled is True # Enabled state unchanged - - def test_reset_preserves_disabled_detector_config(self) -> None: - detector = self.create_detector( - project=self.project, - type="performance_slow_db_query", - name="Test", - enabled=False, - config={"duration_threshold": 5000}, - ) - - reset_wfe_detector_configs(self.project) - - detector.refresh_from_db() - assert detector.config["duration_threshold"] == 5000 - assert detector.enabled is False - - def test_reset_skips_nonexistent_detectors(self) -> None: - updated = reset_wfe_detector_configs(self.project) - - assert len(updated) == 0 - assert ( - Detector.objects.filter(project=self.project, type="performance_slow_db_query").count() - == 0 - ) - - @pytest.mark.django_db class AtomicPerformanceSettingsTest(TestCase): def setUp(self) -> None: @@ -833,36 +612,11 @@ def setUp(self) -> None: def test_update_performance_settings_updates_project_option(self) -> None: settings = {"slow_db_query_duration_threshold": 3000} - update_performance_settings(self.project, settings, sync_detectors=False) + update_performance_settings(self.project, settings) saved_settings = self.project.get_option(SETTINGS_PROJECT_OPTION_KEY) assert saved_settings["slow_db_query_duration_threshold"] == 3000 - def test_update_performance_settings_syncs_detectors_when_enabled(self) -> None: - self.create_detector( - project=self.project, - type="performance_slow_db_query", - name="Test", - enabled=False, - config={}, - ) - - settings = { - "slow_db_queries_detection_enabled": True, - "slow_db_query_duration_threshold": 4000, - } - - updated = update_performance_settings(self.project, settings, sync_detectors=True) - - assert DetectorType.SLOW_DB_QUERY in updated - - saved_settings = self.project.get_option(SETTINGS_PROJECT_OPTION_KEY) - assert saved_settings["slow_db_query_duration_threshold"] == 4000 - - detector = Detector.objects.get(project=self.project, type="performance_slow_db_query") - assert detector.enabled is True - assert detector.config["duration_threshold"] == 4000 - def test_reset_performance_settings_updates_project_option(self) -> None: self.project.update_option( SETTINGS_PROJECT_OPTION_KEY, @@ -870,25 +624,8 @@ def test_reset_performance_settings_updates_project_option(self) -> None: ) unchanged = {"slow_db_query_duration_threshold": 5000} - reset_performance_settings(self.project, unchanged, sync_detectors=False) + reset_performance_settings(self.project, unchanged) saved_settings = self.project.get_option(SETTINGS_PROJECT_OPTION_KEY) assert saved_settings == {"slow_db_query_duration_threshold": 5000} assert "some_other_option" not in saved_settings - - def test_reset_performance_settings_resets_detectors_when_enabled(self) -> None: - detector = self.create_detector( - project=self.project, - type="performance_slow_db_query", - name="Test", - enabled=True, - config={"duration_threshold": 5000}, - ) - - updated = reset_performance_settings(self.project, {}, sync_detectors=True) - - assert DetectorType.SLOW_DB_QUERY in updated - - detector.refresh_from_db() - assert detector.enabled is True # Enabled state unchanged - assert detector.config == {} # Config cleared diff --git a/tests/sentry/issues/endpoints/test_project_performance_issue_settings.py b/tests/sentry/issues/endpoints/test_project_performance_issue_settings.py index cdb3fd3e2332..05ae9c9b4fbd 100644 --- a/tests/sentry/issues/endpoints/test_project_performance_issue_settings.py +++ b/tests/sentry/issues/endpoints/test_project_performance_issue_settings.py @@ -6,13 +6,11 @@ from sentry.issue_detection.performance_detection import ( SETTINGS_PROJECT_OPTION_KEY, - SettingsMode, get_merged_settings, ) from sentry.testutils.cases import APITestCase from sentry.testutils.helpers import override_options from sentry.testutils.helpers.features import with_feature -from sentry.workflow_engine.models import Detector class ProjectPerformanceIssueSettingsTest(APITestCase): @@ -426,215 +424,6 @@ def test_delete_does_not_resets_enabled_project_settings(self) -> None: == 5000 ) # setting persists as detection is disabled for corresponding issue - @with_feature("organizations:performance-view") - @with_feature("projects:workflow-engine-performance-detectors") - def test_put_syncs_to_wfe_detector_config(self) -> None: - """ - Test that PUT updates WFE Detector.config and keeps it in sync with ProjectOption. - Verifies sync by comparing LEGACY vs WFE settings modes. - """ - # Create a detector for SLOW_DB_QUERY - detector = Detector.objects.create( - project_id=self.project.id, - type="performance_slow_db_query", - name="Slow DB Query Detector", - config={"duration_threshold": 1000}, - enabled=True, - ) - - # Update the threshold via PUT - response = self.get_success_response( - self.project.organization.slug, - self.project.slug, - slow_db_query_duration_threshold=5000, - method="put", - status_code=status.HTTP_200_OK, - ) - - assert response.data["slow_db_query_duration_threshold"] == 5000 - - # Verify the WFE Detector config was updated - detector.refresh_from_db() - assert detector.config["duration_threshold"] == 5000 - - # Verify LEGACY and WFE settings are identical - legacy_settings = get_merged_settings(self.project, SettingsMode.LEGACY) - wfe_settings = get_merged_settings(self.project, SettingsMode.WFE) - assert ( - legacy_settings["slow_db_query_duration_threshold"] - == wfe_settings["slow_db_query_duration_threshold"] - == 5000 - ) - - @with_feature("organizations:performance-view") - @with_feature("projects:workflow-engine-performance-detectors") - def test_delete_resets_enabled_wfe_detector_config(self) -> None: - """ - Test that DELETE clears config on enabled WFE Detectors. - Verifies sync by comparing LEGACY vs WFE settings modes. - """ - # Create a detector with custom config, enabled - detector = Detector.objects.create( - project_id=self.project.id, - type="performance_slow_db_query", - name="Slow DB Query Detector", - config={"duration_threshold": 5000}, - enabled=True, - ) - - # Set project options with custom threshold - self.project.update_option( - SETTINGS_PROJECT_OPTION_KEY, - { - "slow_db_queries_detection_enabled": True, - "slow_db_query_duration_threshold": 5000, - }, - ) - - # Call DELETE to reset - self.get_success_response( - self.project.organization.slug, - self.project.slug, - method="delete", - status_code=status.HTTP_204_NO_CONTENT, - ) - - # Verify the WFE Detector config was reset (empty since detector is enabled) - detector.refresh_from_db() - assert detector.config == {} - - # Verify management setting was preserved in ProjectOption - assert self.project.get_option(SETTINGS_PROJECT_OPTION_KEY)[ - "slow_db_queries_detection_enabled" - ] - # Verify threshold was removed from ProjectOption - assert "slow_db_query_duration_threshold" not in self.project.get_option( - SETTINGS_PROJECT_OPTION_KEY - ) - - # Verify LEGACY and WFE settings are identical after DELETE - legacy_settings = get_merged_settings(self.project, SettingsMode.LEGACY) - wfe_settings = get_merged_settings(self.project, SettingsMode.WFE) - assert ( - legacy_settings["slow_db_query_duration_threshold"] - == wfe_settings["slow_db_query_duration_threshold"] - ) - - @with_feature("organizations:performance-view") - @with_feature("projects:workflow-engine-performance-detectors") - def test_put_toggling_detection_enabled_syncs_wfe_detector_enabled(self) -> None: - """ - Test that toggling detection_enabled on/off syncs to Detector.enabled. - """ - # Create an enabled detector - detector = Detector.objects.create( - project_id=self.project.id, - type="performance_slow_db_query", - name="Slow DB Query Detector", - config={"duration_threshold": 1000}, - enabled=True, - ) - - # Disable the detector via PUT - response = self.get_success_response( - self.project.organization.slug, - self.project.slug, - slow_db_queries_detection_enabled=False, - method="put", - status_code=status.HTTP_200_OK, - ) - - assert not response.data["slow_db_queries_detection_enabled"] - - # Verify WFE Detector was disabled - detector.refresh_from_db() - assert not detector.enabled - - # Re-enable the detector via PUT - response = self.get_success_response( - self.project.organization.slug, - self.project.slug, - slow_db_queries_detection_enabled=True, - method="put", - status_code=status.HTTP_200_OK, - ) - - assert response.data["slow_db_queries_detection_enabled"] - - # Verify WFE Detector was re-enabled - detector.refresh_from_db() - assert detector.enabled - - # Verify LEGACY and WFE settings remain in sync - legacy_settings = get_merged_settings(self.project, SettingsMode.LEGACY) # type: ignore[unreachable] - wfe_settings = get_merged_settings(self.project, SettingsMode.WFE) - assert legacy_settings["slow_db_queries_detection_enabled"] - assert wfe_settings["slow_db_queries_detection_enabled"] - - @with_feature("organizations:performance-view") - @with_feature("projects:workflow-engine-performance-detectors") - def test_put_updates_threshold_after_re_enabling_detector(self) -> None: - """ - Test that thresholds can be updated after disabling and re-enabling a detector. - Verifies the workflow: enabled → disabled → enabled → update threshold. - """ - # Create an enabled detector with initial threshold - detector = Detector.objects.create( - project_id=self.project.id, - type="performance_slow_db_query", - name="Slow DB Query Detector", - config={"duration_threshold": 1000}, - enabled=True, - ) - - # Disable the detector - self.get_success_response( - self.project.organization.slug, - self.project.slug, - slow_db_queries_detection_enabled=False, - method="put", - status_code=status.HTTP_200_OK, - ) - - detector.refresh_from_db() - assert not detector.enabled - - # Re-enable the detector - self.get_success_response( - self.project.organization.slug, - self.project.slug, - slow_db_queries_detection_enabled=True, - method="put", - status_code=status.HTTP_200_OK, - ) - - detector.refresh_from_db() - assert detector.enabled - - # Update threshold after re-enabling - response = self.get_success_response( # type: ignore[unreachable] - self.project.organization.slug, - self.project.slug, - slow_db_query_duration_threshold=5000, - method="put", - status_code=status.HTTP_200_OK, - ) - - assert response.data["slow_db_query_duration_threshold"] == 5000 - - # Verify WFE Detector config was updated - detector.refresh_from_db() - assert detector.config["duration_threshold"] == 5000 - - # Verify LEGACY and WFE settings are in sync - legacy_settings = get_merged_settings(self.project, SettingsMode.LEGACY) - wfe_settings = get_merged_settings(self.project, SettingsMode.WFE) - assert ( - legacy_settings["slow_db_query_duration_threshold"] - == wfe_settings["slow_db_query_duration_threshold"] - == 5000 - ) - @with_feature("organizations:performance-view") def test_delete_preserves_thresholds_when_no_explicit_enabled_setting(self) -> None: """ @@ -664,60 +453,3 @@ def test_delete_preserves_thresholds_when_no_explicit_enabled_setting(self) -> N # Verify threshold was preserved (treated as "disabled" due to no explicit enabled setting) project_settings = self.project.get_option(SETTINGS_PROJECT_OPTION_KEY) assert project_settings["slow_db_query_duration_threshold"] == 5000 - - @with_feature("organizations:performance-view") - @with_feature("projects:workflow-engine-performance-detectors") - def test_delete_preserves_disabled_wfe_detector_config(self) -> None: - """ - Test that DELETE leaves config unchanged on disabled WFE Detectors. - Assumes WFE and ProjectOption are already in sync (verified by comparing settings modes). - """ - # Create a detector with custom config, disabled - detector = Detector.objects.create( - project_id=self.project.id, - type="performance_slow_db_query", - name="Slow DB Query Detector", - config={"duration_threshold": 5000}, - enabled=False, - ) - - # Set project options with detector disabled and matching threshold - self.project.update_option( - SETTINGS_PROJECT_OPTION_KEY, - { - "slow_db_queries_detection_enabled": False, - "slow_db_query_duration_threshold": 5000, - }, - ) - - # Verify sync before DELETE - legacy_settings = get_merged_settings(self.project, SettingsMode.LEGACY) - wfe_settings = get_merged_settings(self.project, SettingsMode.WFE) - assert legacy_settings["slow_db_query_duration_threshold"] == 5000 - assert wfe_settings["slow_db_query_duration_threshold"] == 5000 - - # Call DELETE to reset - self.get_success_response( - self.project.organization.slug, - self.project.slug, - method="delete", - status_code=status.HTTP_204_NO_CONTENT, - ) - - # Verify the WFE Detector config was preserved (detector is disabled) - detector.refresh_from_db() - assert detector.config["duration_threshold"] == 5000 - - # Verify both management setting and threshold were preserved in ProjectOption - project_settings = self.project.get_option(SETTINGS_PROJECT_OPTION_KEY) - assert not project_settings["slow_db_queries_detection_enabled"] - assert project_settings["slow_db_query_duration_threshold"] == 5000 - - # Verify LEGACY and WFE settings remain identical after DELETE - legacy_settings = get_merged_settings(self.project, SettingsMode.LEGACY) - wfe_settings = get_merged_settings(self.project, SettingsMode.WFE) - assert ( - legacy_settings["slow_db_query_duration_threshold"] - == wfe_settings["slow_db_query_duration_threshold"] - == 5000 - ) diff --git a/tests/sentry/workflow_engine/receivers/test_project_detectors.py b/tests/sentry/workflow_engine/receivers/test_project_detectors.py index 4b1c6a1f3704..7f62518c6c0d 100644 --- a/tests/sentry/workflow_engine/receivers/test_project_detectors.py +++ b/tests/sentry/workflow_engine/receivers/test_project_detectors.py @@ -6,16 +6,12 @@ from sentry.grouping.grouptype import ErrorGroupType from sentry.incidents.grouptype import MetricIssue from sentry.incidents.models.alert_rule import AlertRuleDetectionType -from sentry.issue_detection.performance_detection import PERFORMANCE_DETECTOR_CONFIG_MAPPINGS from sentry.models.project import Project from sentry.signals import project_created from sentry.snuba.models import QuerySubscription from sentry.testutils.cases import TestCase from sentry.testutils.helpers.features import with_feature -from sentry.workflow_engine.defaults.detectors import ( - ensure_default_anomaly_detector, - ensure_performance_detectors, -) +from sentry.workflow_engine.defaults.detectors import ensure_default_anomaly_detector from sentry.workflow_engine.models import DataSource, Detector from sentry.workflow_engine.models.data_condition import Condition, DataCondition from sentry.workflow_engine.receivers.project_detectors import ( @@ -204,78 +200,3 @@ def test_context_manager_disables_metric_detector_signal(self) -> None: # Metric detector should not be created because the signal handler was disconnected assert not Detector.objects.filter(project=project, type=MetricIssue.slug).exists() - - -class TestCreatePerformanceDetectors(TestCase): - @with_feature("projects:workflow-engine-performance-detectors") - def test_creates_detectors_on_project_creation(self) -> None: - project = self.create_project() - - for mapping in PERFORMANCE_DETECTOR_CONFIG_MAPPINGS.values(): - assert Detector.objects.filter(project=project, type=mapping.wfe_detector_type).exists() - - def test_does_not_create_detectors_when_flag_disabled(self) -> None: - project = self.create_project() - - for mapping in PERFORMANCE_DETECTOR_CONFIG_MAPPINGS.values(): - assert not Detector.objects.filter( - project=project, type=mapping.wfe_detector_type - ).exists() - - @with_feature("projects:workflow-engine-performance-detectors") - def test_idempotent_no_duplicates(self) -> None: - with disable_default_detector_creation(): - project = self.create_project() - - ensure_performance_detectors(project) - ensure_performance_detectors(project) - - for mapping in PERFORMANCE_DETECTOR_CONFIG_MAPPINGS.values(): - assert ( - Detector.objects.filter(project=project, type=mapping.wfe_detector_type).count() - == 1 - ) - - @with_feature("projects:workflow-engine-performance-detectors") - def test_disable_default_detector_creation_prevents_performance_detectors(self) -> None: - with disable_default_detector_creation(): - project = self.create_project() - - for mapping in PERFORMANCE_DETECTOR_CONFIG_MAPPINGS.values(): - assert not Detector.objects.filter( - project=project, type=mapping.wfe_detector_type - ).exists() - - @with_feature("projects:workflow-engine-performance-detectors") - @mock.patch( - "sentry.workflow_engine.defaults.detectors.DEFAULT_PROJECT_PERFORMANCE_DETECTION_SETTINGS", - { - "slow_db_queries_detection_enabled": True, - "large_http_payload_detection_enabled": True, - "db_query_injection_detection_enabled": False, - }, - ) - @mock.patch( - "sentry.workflow_engine.defaults.detectors.get_disabled_platforms_by_detector_type", - return_value={ - "performance_slow_db_query": frozenset({"ruby", "php"}), - }, - ) - def test_respects_default_enabled_state(self, mock_disabled: mock.MagicMock) -> None: - """Test that detectors respect both platform-specific disabling and default enabled state.""" - with disable_default_detector_creation(): - project = self.create_project(platform="ruby") - - ensure_performance_detectors(project) - - # Disabled because platform is in the disabled list - detector = Detector.objects.get(project=project, type="performance_slow_db_query") - assert detector.enabled is False - - # Enabled: platform not in any disabled list and default is True - detector = Detector.objects.get(project=project, type="performance_large_http_payload") - assert detector.enabled is True - - # Disabled because default setting is False (regardless of platform) - detector = Detector.objects.get(project=project, type="query_injection_vulnerability") - assert detector.enabled is False