Skip to content

Commit 7b39044

Browse files
authored
feat(detectors): Add workflow filter to detector search query (#113115)
Modifies the detectors endpoint to support a `workflow` filter to give us a more flexible way of querying a workflow's connected detectors. This will be used to separately query the workflows connected projects and monitors. Supports the full set of operators: `workflow:100` for a single match, `workflow:[100, 200]` for multiple, and negation with `!workflow:100` or `!workflow:[100, 200]`.
1 parent 924bddb commit 7b39044

File tree

2 files changed

+87
-2
lines changed

2 files changed

+87
-2
lines changed

src/sentry/workflow_engine/endpoints/organization_detector_index.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@
6666

6767
detector_search_config = SearchConfig.create_from(
6868
default_config,
69-
text_operator_keys={"name", "type"},
70-
allowed_keys={"name", "type", "assignee"},
69+
text_operator_keys={"name", "type", "workflow"},
70+
allowed_keys={"name", "type", "assignee", "workflow"},
7171
allow_boolean=False,
7272
free_text_key="query",
7373
)
@@ -218,6 +218,24 @@ def filter_detectors(self, request: Request, organization: Any) -> QuerySet[Dete
218218
queryset = queryset.exclude(assignee_q)
219219
else:
220220
queryset = queryset.filter(assignee_q)
221+
case SearchFilter(
222+
key=SearchKey("workflow"),
223+
operator=("=" | "IN" | "!=" | "NOT IN"),
224+
):
225+
workflow_ids = (
226+
filter.value.value
227+
if isinstance(filter.value.value, list)
228+
else [filter.value.value]
229+
)
230+
workflow_ids = to_valid_int_id_list("workflow", workflow_ids)
231+
if filter.operator in ("!=", "NOT IN"):
232+
queryset = queryset.exclude(
233+
detectorworkflow__workflow_id__in=workflow_ids
234+
)
235+
else:
236+
queryset = queryset.filter(
237+
detectorworkflow__workflow_id__in=workflow_ids
238+
).distinct()
221239
case SearchFilter(key=SearchKey("query"), operator="="):
222240
# 'query' is our free text key; all free text gets returned here
223241
# as '=', and we search any relevant fields for it.

tests/sentry/workflow_engine/endpoints/test_organization_detector_index.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,73 @@ def test_query_invalid_search_key(self) -> None:
501501
assert "query" in response.data
502502
assert "Invalid key for this search: tpe" in str(response.data["query"])
503503

504+
def test_query_by_workflow(self) -> None:
505+
workflow = self.create_workflow(organization_id=self.organization.id)
506+
workflow_2 = self.create_workflow(organization_id=self.organization.id)
507+
detector_a = self.create_detector(
508+
project=self.project, name="Detector A", type=MetricIssue.slug
509+
)
510+
detector_b = self.create_detector(
511+
project=self.project, name="Detector B", type=MetricIssue.slug
512+
)
513+
self.create_detector(project=self.project, name="Detector C", type=MetricIssue.slug)
514+
self.create_detector_workflow(detector=detector_a, workflow=workflow)
515+
self.create_detector_workflow(detector=detector_b, workflow=workflow)
516+
self.create_detector_workflow(detector=detector_b, workflow=workflow_2)
517+
518+
# Filter by single workflow
519+
response = self.get_success_response(
520+
self.organization.slug,
521+
qs_params={"project": self.project.id, "query": f"workflow:{workflow.id}"},
522+
)
523+
assert {d["name"] for d in response.data} == {detector_a.name, detector_b.name}
524+
525+
# Filter by a different workflow
526+
response = self.get_success_response(
527+
self.organization.slug,
528+
qs_params={"project": self.project.id, "query": f"workflow:{workflow_2.id}"},
529+
)
530+
assert {d["name"] for d in response.data} == {detector_b.name}
531+
532+
# Filter by multiple workflows (IN)
533+
response = self.get_success_response(
534+
self.organization.slug,
535+
qs_params={
536+
"project": self.project.id,
537+
"query": f"workflow:[{workflow.id}, {workflow_2.id}]",
538+
},
539+
)
540+
assert [d["name"] for d in response.data].count(detector_b.name) == 1
541+
assert {d["name"] for d in response.data} == {detector_a.name, detector_b.name}
542+
543+
# Negation
544+
response = self.get_success_response(
545+
self.organization.slug,
546+
qs_params={"project": self.project.id, "query": f"!workflow:{workflow.id}"},
547+
)
548+
returned_names = {d["name"] for d in response.data}
549+
assert detector_a.name not in returned_names
550+
assert detector_b.name not in returned_names
551+
552+
# Negation with list (!IN)
553+
response = self.get_success_response(
554+
self.organization.slug,
555+
qs_params={
556+
"project": self.project.id,
557+
"query": f"!workflow:[{workflow.id}, {workflow_2.id}]",
558+
},
559+
)
560+
returned_names = {d["name"] for d in response.data}
561+
assert detector_a.name not in returned_names
562+
assert detector_b.name not in returned_names
563+
564+
def test_query_by_workflow_invalid_value(self) -> None:
565+
self.get_error_response(
566+
self.organization.slug,
567+
qs_params={"project": self.project.id, "query": "workflow:abc"},
568+
status_code=400,
569+
)
570+
504571
def test_query_by_assignee_user_email(self) -> None:
505572
user = self.create_user(email="assignee@example.com")
506573
self.create_member(organization=self.organization, user=user)

0 commit comments

Comments
 (0)