From ecbba247ec3e29f98b0ba92a71310b2995f008bb Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Mon, 5 Jan 2026 15:14:22 -0800 Subject: [PATCH 1/3] feat(aci): Add ability to filter workflows by connected detectors --- .../endpoints/organization_workflow_index.py | 10 ++++ .../test_organization_workflow_index.py | 47 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/sentry/workflow_engine/endpoints/organization_workflow_index.py b/src/sentry/workflow_engine/endpoints/organization_workflow_index.py index 72913ad196383f..198169b3fd8336 100644 --- a/src/sentry/workflow_engine/endpoints/organization_workflow_index.py +++ b/src/sentry/workflow_engine/endpoints/organization_workflow_index.py @@ -121,6 +121,16 @@ def filter_workflows(self, request: Request, organization: Organization) -> Quer # If specific IDs are provided, skip query and project filtering return queryset + if raw_detectorlist := request.GET.getlist("detector"): + try: + detector_ids = [int(id) for id in raw_detectorlist] + except ValueError: + raise ValidationError({"detector": ["Invalid detector ID format"]}) + queryset = queryset.filter(detectorworkflow__detector_id__in=detector_ids).distinct() + + # If detector IDs are provided, skip query and project filtering + return queryset + if raw_query := request.GET.get("query"): for filter in parse_workflow_query(raw_query): assert isinstance(filter, SearchFilter) diff --git a/tests/sentry/workflow_engine/endpoints/test_organization_workflow_index.py b/tests/sentry/workflow_engine/endpoints/test_organization_workflow_index.py index 3dd3a49b9b16c0..76de6d1587f26a 100644 --- a/tests/sentry/workflow_engine/endpoints/test_organization_workflow_index.py +++ b/tests/sentry/workflow_engine/endpoints/test_organization_workflow_index.py @@ -346,6 +346,53 @@ def test_query_filter_by_action(self) -> None: assert len(response3.data) == 2 assert {self.workflow.name, self.workflow_two.name} == {w["name"] for w in response3.data} + def test_filter_by_detector(self) -> None: + project_1 = self.create_project(organization=self.organization) + project_2 = self.create_project(organization=self.organization) + project_3 = self.create_project(organization=self.organization) + + detector_1 = self.create_detector(project=project_1, name="Detector 1") + detector_2 = self.create_detector(project=project_2, name="Detector 2") + detector_3 = self.create_detector(project=project_3, name="Detector 3") + + self.create_detector_workflow(workflow=self.workflow, detector=detector_1) + self.create_detector_workflow(workflow=self.workflow_two, detector=detector_2) + self.create_detector_workflow(workflow=self.workflow_three, detector=detector_3) + + # Filter by single detector + response = self.get_success_response( + self.organization.slug, + qs_params={"detector": str(detector_1.id)}, + ) + assert len(response.data) == 1 + assert response.data[0]["name"] == self.workflow.name + + # Filter by multiple detectors + response2 = self.get_success_response( + self.organization.slug, + qs_params=[ + ("detector", str(detector_1.id)), + ("detector", str(detector_2.id)), + ], + ) + assert len(response2.data) == 2 + assert {w["name"] for w in response2.data} == {self.workflow.name, self.workflow_two.name} + + # Filter by non-existent detector ID returns no results + response3 = self.get_success_response( + self.organization.slug, + qs_params={"detector": "999999"}, + ) + assert len(response3.data) == 0 + + # Invalid detector ID format returns error + response4 = self.get_error_response( + self.organization.slug, + qs_params={"detector": "not-an-id"}, + status_code=400, + ) + assert response4.data == {"detector": ["Invalid detector ID format"]} + def test_compound_query(self) -> None: self.create_detector_workflow( workflow=self.workflow, detector=self.create_detector(project=self.project) From 5bb718504aea46f2dbad128e32612da4ca2732bd Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Mon, 5 Jan 2026 15:46:14 -0800 Subject: [PATCH 2/3] Remove unnecssary early return --- .../workflow_engine/endpoints/organization_workflow_index.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/sentry/workflow_engine/endpoints/organization_workflow_index.py b/src/sentry/workflow_engine/endpoints/organization_workflow_index.py index 198169b3fd8336..202bbd0434dbda 100644 --- a/src/sentry/workflow_engine/endpoints/organization_workflow_index.py +++ b/src/sentry/workflow_engine/endpoints/organization_workflow_index.py @@ -128,9 +128,6 @@ def filter_workflows(self, request: Request, organization: Organization) -> Quer raise ValidationError({"detector": ["Invalid detector ID format"]}) queryset = queryset.filter(detectorworkflow__detector_id__in=detector_ids).distinct() - # If detector IDs are provided, skip query and project filtering - return queryset - if raw_query := request.GET.get("query"): for filter in parse_workflow_query(raw_query): assert isinstance(filter, SearchFilter) From 0a8a832dc8977a62a99d2c2097f7d5997fda0146 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Mon, 5 Jan 2026 16:01:21 -0800 Subject: [PATCH 3/3] Add test for org filtering --- .../endpoints/test_organization_workflow_index.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/sentry/workflow_engine/endpoints/test_organization_workflow_index.py b/tests/sentry/workflow_engine/endpoints/test_organization_workflow_index.py index 76de6d1587f26a..dceaa6335dc6d8 100644 --- a/tests/sentry/workflow_engine/endpoints/test_organization_workflow_index.py +++ b/tests/sentry/workflow_engine/endpoints/test_organization_workflow_index.py @@ -65,6 +65,20 @@ def test_simple(self) -> None: hits = int(response["X-Hits"]) assert hits == 3 + def test_only_returns_workflows_from_organization(self) -> None: + other_org = self.create_organization() + self.create_workflow(organization_id=other_org.id, name="Other Org Workflow") + + response = self.get_success_response(self.organization.slug) + assert len(response.data) == 3 + workflow_names = {w["name"] for w in response.data} + assert "Other Org Workflow" not in workflow_names + assert workflow_names == { + self.workflow.name, + self.workflow_two.name, + self.workflow_three.name, + } + def test_empty_result(self) -> None: response = self.get_success_response( self.organization.slug, qs_params={"query": "aaaaaaaaaaaaa"}