From 659e955897f29ebe91114b4532a34491d6dad07d Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Tue, 7 Apr 2026 16:37:19 -0700 Subject: [PATCH 1/3] ref(metric-issues): Remove workflow-engine-metric-issue-ui flag usage --- src/sentry/features/temporary.py | 2 - src/sentry/incidents/grouptype.py | 16 +----- .../workflow_engine/processors/detector.py | 14 ++--- .../processors/test_detector.py | 53 ++++--------------- 4 files changed, 16 insertions(+), 69 deletions(-) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index f366ce628325db..40751b01be6abe 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -443,8 +443,6 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:workflow-engine-action-filters-cache", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable ingestion through trusted relays only manager.add("organizations:ingest-through-trusted-relays-only", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enable metric issue UI for issue alerts - manager.add("organizations:workflow-engine-metric-issue-ui", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Disable issue stream detector notifications for metric issues manager.add("organizations:workflow-engine-metric-issue-disable-issue-detector-notifications", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable new workflow_engine UI (see: alerts create issues) diff --git a/src/sentry/incidents/grouptype.py b/src/sentry/incidents/grouptype.py index 22f5380b588724..c4e69255ee930a 100644 --- a/src/sentry/incidents/grouptype.py +++ b/src/sentry/incidents/grouptype.py @@ -350,6 +350,7 @@ class MetricIssue(GroupType): category_v2 = GroupCategory.METRIC.value creation_quota = Quota(3600, 60, 100) default_priority = PriorityLevel.HIGH + released = True enable_auto_resolve = False enable_escalation_detection = False enable_status_change_workflow_notifications = False @@ -375,18 +376,3 @@ class MetricIssue(GroupType): }, }, ) - - @classmethod - def allow_ingest(cls, organization: Organization) -> bool: - return True - - @classmethod - def allow_post_process_group(cls, organization: Organization) -> bool: - return True - - @classmethod - def build_visible_feature_name(cls) -> list[str]: - return [ - "organizations:workflow-engine-ui", - "organizations:workflow-engine-metric-issue-ui", - ] diff --git a/src/sentry/workflow_engine/processors/detector.py b/src/sentry/workflow_engine/processors/detector.py index fcd67574b72e63..94c77f9c65f596 100644 --- a/src/sentry/workflow_engine/processors/detector.py +++ b/src/sentry/workflow_engine/processors/detector.py @@ -67,31 +67,25 @@ def detectors(self) -> set[Detector]: def _is_issue_stream_detector_enabled(event_data: WorkflowEventData) -> bool: """ Check if the issue stream detector should be enabled for this event's group type. - - Most group types enable the issue stream detector by default. MetricIssue is excluded - unless the workflow-engine-metric-issue-ui feature flag is enabled for the organization, - which allows incremental rollout of issue alerts for metric issues. """ group_type_id = event_data.group.type disabled_type_ids = options.get("workflow_engine.group.type_id.disable_issue_stream_detector") if group_type_id not in disabled_type_ids: return True + # Metric isssues are a special case currently. + # In order to give users time to adjust to the new behavior, we allow them to disable the + # issue stream detector for metric issues via a feature flag. if group_type_id != MetricIssue.type_id: return False organization = event_data.event.project.organization - has_metric_issue_ui = features.has( - "organizations:workflow-engine-metric-issue-ui", organization - ) - # For most users, the issue stream detector for metric issues will be rolled out along with the metric issue UI. - # For users who find that behavior undesirable, this feature flag will disable it for them. disable_issue_stream_detector_for_metric_issues = features.has( "organizations:workflow-engine-metric-issue-disable-issue-detector-notifications", organization, ) - return has_metric_issue_ui and not disable_issue_stream_detector_for_metric_issues + return not disable_issue_stream_detector_for_metric_issues def get_detectors_for_event_data( diff --git a/tests/sentry/workflow_engine/processors/test_detector.py b/tests/sentry/workflow_engine/processors/test_detector.py index 27be466cc00f4f..e9f25a715afec3 100644 --- a/tests/sentry/workflow_engine/processors/test_detector.py +++ b/tests/sentry/workflow_engine/processors/test_detector.py @@ -867,7 +867,6 @@ def setUp(self) -> None: self.group_event = GroupEvent.from_event(self.event, self.group) def test_activity_update(self) -> None: - # only picks up metric detector because the group type does not enable the issue stream detector activity = Activity.objects.create( project=self.project, group=self.group, @@ -878,7 +877,7 @@ def test_activity_update(self) -> None: result = get_detectors_for_event_data(event_data, detector=self.detector) assert result is not None assert result.preferred_detector == self.detector - assert result.detectors == {self.detector} + assert result.detectors == {self.issue_stream_detector, self.detector} def test_error_group_type(self) -> None: # default behavior for a group type is to pick up the issue stream detector @@ -896,30 +895,6 @@ def test_metric_issue(self) -> None: result = get_detectors_for_event_data(event_data) assert result is not None assert result.preferred_detector == self.detector - assert result.detectors == {self.detector} - - def test_metric_issue_with_feature_flag(self) -> None: - self.group_event.occurrence = self.occurrence - - event_data = WorkflowEventData(event=self.group_event, group=self.group) - with self.feature("organizations:workflow-engine-metric-issue-ui"): - result = get_detectors_for_event_data(event_data) - assert result is not None - assert result.preferred_detector == self.detector - assert result.detectors == {self.issue_stream_detector, self.detector} - - def test_activity_update_with_feature_flag(self) -> None: - activity = Activity.objects.create( - project=self.project, - group=self.group, - type=ActivityType.SET_RESOLVED.value, - user_id=self.user.id, - ) - event_data = WorkflowEventData(event=activity, group=self.group) - with self.feature("organizations:workflow-engine-metric-issue-ui"): - result = get_detectors_for_event_data(event_data, detector=self.detector) - assert result is not None - assert result.preferred_detector == self.detector assert result.detectors == {self.issue_stream_detector, self.detector} def test_metric_issue_with_disable_detector_flag(self) -> None: @@ -928,18 +903,15 @@ def test_metric_issue_with_disable_detector_flag(self) -> None: event_data = WorkflowEventData(event=self.group_event, group=self.group) with self.feature( - { - "organizations:workflow-engine-metric-issue-ui": True, - "organizations:workflow-engine-metric-issue-disable-issue-detector-notifications": True, - } + "organizations:workflow-engine-metric-issue-disable-issue-detector-notifications" ): result = get_detectors_for_event_data(event_data) assert result is not None assert result.preferred_detector == self.detector assert result.detectors == {self.detector} - def test_non_metric_issue_in_disable_list_with_feature_flag(self) -> None: - """Feature flag override only applies to MetricIssue, not other disabled group types.""" + def test_non_metric_issue_in_disable_list(self) -> None: + """Disable override only applies to MetricIssue, not other disabled group types.""" self.group.update(type=FeedbackGroup.type_id) activity = Activity.objects.create( project=self.project, @@ -948,16 +920,13 @@ def test_non_metric_issue_in_disable_list_with_feature_flag(self) -> None: user_id=self.user.id, ) event_data = WorkflowEventData(event=activity, group=self.group) - with ( - self.feature("organizations:workflow-engine-metric-issue-ui"), - self.options( - { - "workflow_engine.group.type_id.disable_issue_stream_detector": [ - MetricIssue.type_id, - FeedbackGroup.type_id, - ] - } - ), + with self.options( + { + "workflow_engine.group.type_id.disable_issue_stream_detector": [ + MetricIssue.type_id, + FeedbackGroup.type_id, + ] + } ): result = get_detectors_for_event_data(event_data) assert result is None From 04c64de9c15de0a2b0093907259bf8fd49ebe59c Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Wed, 8 Apr 2026 10:13:32 -0700 Subject: [PATCH 2/3] fix(metric-issues): Update workflow tests for issue stream detector enablement The removal of the workflow-engine-metric-issue-ui flag means the issue stream detector is now always enabled for MetricIssue events. Update two tests: - test_no_detector: Delete issue_stream_detector so the 'no detectors' path is still exercised. - test_multiple_detectors__preferred: Use the existing setUp issue_stream_detector instead of creating a duplicate, avoiding both the extra triggered workflow and a potential MultipleObjectsReturned from get_issue_stream_detector_for_project. --- tests/sentry/workflow_engine/processors/test_workflow.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/sentry/workflow_engine/processors/test_workflow.py b/tests/sentry/workflow_engine/processors/test_workflow.py index 07818e0ee03576..5c657a6d9187d6 100644 --- a/tests/sentry/workflow_engine/processors/test_workflow.py +++ b/tests/sentry/workflow_engine/processors/test_workflow.py @@ -327,6 +327,7 @@ def test_regressed_event(self) -> None: @patch("sentry.workflow_engine.processors.detector.logger") def test_no_detector(self, mock_logger: MagicMock, mock_incr: MagicMock) -> None: self.group_event.occurrence = self.build_occurrence(evidence_data={}) + self.issue_stream_detector.delete() result = process_workflows(self.batch_client, self.event_data, FROZEN_TIME) assert result.msg == "No Detectors associated with the issue were found" @@ -446,13 +447,8 @@ def test_uses_issue_stream_workflows(self) -> None: assert len(result.data.triggered_actions) == 0 def test_multiple_detectors__preferred(self) -> None: - _, issue_stream_detector, _, _ = self.create_detector_and_workflow( - name_prefix="issue_stream", - workflow_triggers=self.create_data_condition_group(), - detector_type=IssueStreamGroupType.slug, - ) self.create_detector_workflow( - detector=issue_stream_detector, + detector=self.issue_stream_detector, workflow=self.error_workflow, ) From a07cacb7ecaec1b4d2209c4777dc117c696c6188 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Wed, 8 Apr 2026 10:30:20 -0700 Subject: [PATCH 3/3] Remove now unnecessary test case --- tests/snuba/search/test_backend.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/tests/snuba/search/test_backend.py b/tests/snuba/search/test_backend.py index ba982e5b71a5f9..595330d2b6fab3 100644 --- a/tests/snuba/search/test_backend.py +++ b/tests/snuba/search/test_backend.py @@ -8,7 +8,6 @@ from sentry.exceptions import InvalidSearchQuery from sentry.grouping.grouptype import ErrorGroupType -from sentry.incidents.grouptype import MetricIssue from sentry.issues.grouptype import ( FeedbackGroup, NoiseConfig, @@ -3489,28 +3488,6 @@ def setUp(self) -> None: ) self.error_group_2 = error_event_2.group - def test_no_feature(self) -> None: - event_id = uuid.uuid4().hex - - with self.feature(MetricIssue.build_ingest_feature_name()): - _, group_info = self.process_occurrence( - event_id=event_id, - project_id=self.project.id, - type=MetricIssue.type_id, - fingerprint=["some perf issue"], - event_data={ - "title": "some problem", - "platform": "python", - "tags": {"my_tag": "1"}, - "timestamp": before_now(minutes=1).isoformat(), - "received": before_now(minutes=1).isoformat(), - }, - ) - assert group_info is not None - - results = self.make_query(search_filter_query="issue.category:metric_alert my_tag:1") - assert list(results) == [] - def test_generic_query(self) -> None: results = self.make_query(search_filter_query="issue.category:performance my_tag:1") assert list(results) == [self.profile_group_1, self.profile_group_2]