Skip to content

Commit 7580ceb

Browse files
committed
feat(aci): APIs to allow filter monitors by assignee
1 parent 01bf898 commit 7580ceb

File tree

2 files changed

+107
-17
lines changed

2 files changed

+107
-17
lines changed

src/sentry/workflow_engine/endpoints/organization_detector_index.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections.abc import Iterable, Sequence
12
from functools import partial
23

34
from django.db import router, transaction
@@ -17,6 +18,7 @@
1718
from sentry.api.event_search import SearchConfig, SearchFilter, SearchKey, default_config
1819
from sentry.api.event_search import parse_search_query as base_parse_search_query
1920
from sentry.api.exceptions import ResourceDoesNotExist
21+
from sentry.api.issue_search import convert_actor_or_none_value
2022
from sentry.api.paginator import OffsetPaginator
2123
from sentry.api.serializers import serialize
2224
from sentry.apidocs.constants import (
@@ -30,6 +32,9 @@
3032
from sentry.issues import grouptype
3133
from sentry.models.organization import Organization
3234
from sentry.models.project import Project
35+
from sentry.models.team import Team
36+
from sentry.users.models.user import User
37+
from sentry.users.services.user import RpcUser
3338
from sentry.workflow_engine.endpoints.serializers import DetectorSerializer
3439
from sentry.workflow_engine.endpoints.utils.filters import apply_filter
3540
from sentry.workflow_engine.endpoints.utils.sortby import SortByParam
@@ -43,12 +48,34 @@
4348
detector_search_config = SearchConfig.create_from(
4449
default_config,
4550
text_operator_keys={"name", "type"},
46-
allowed_keys={"name", "type"},
51+
allowed_keys={"name", "type", "assignee"},
4752
allow_boolean=False,
4853
free_text_key="query",
4954
)
5055
parse_detector_query = partial(base_parse_search_query, config=detector_search_config)
5156

57+
58+
def convert_assignee_values(value: Iterable[str], projects: Sequence[Project], user: User) -> Q:
59+
"""
60+
Convert an assignee search value to a Django Q object for filtering detectors.
61+
"""
62+
actors_or_none: list[RpcUser | Team | None] = convert_actor_or_none_value(
63+
value, projects, user, None
64+
)
65+
assignee_query = Q()
66+
for actor in actors_or_none:
67+
if isinstance(actor, (User, RpcUser)):
68+
assignee_query |= Q(owner_user_id=actor.id)
69+
elif isinstance(actor, Team):
70+
assignee_query |= Q(owner_team_id=actor.id)
71+
elif actor is None:
72+
assignee_query |= Q(owner_team_id__isnull=True, owner_user_id__isnull=True)
73+
else:
74+
# Unknown actor type, return a query that matches nothing
75+
assignee_query |= Q(pk__isnull=True)
76+
return assignee_query
77+
78+
5279
# Maps API field name to database field name, with synthetic aggregate fields keeping
5380
# to our field naming scheme for consistency.
5481
SORT_ATTRS = {
@@ -138,6 +165,19 @@ def get(self, request: Request, organization: Organization) -> Response:
138165
queryset = apply_filter(queryset, filter, "name")
139166
case SearchFilter(key=SearchKey("type"), operator=("=" | "IN" | "!=")):
140167
queryset = apply_filter(queryset, filter, "type")
168+
case SearchFilter(key=SearchKey("assignee"), operator=("=" | "IN" | "!=")):
169+
# Filter values can be emails, team slugs, "me", "my_teams", "none"
170+
values = (
171+
filter.value.value
172+
if isinstance(filter.value.value, list)
173+
else [filter.value.value]
174+
)
175+
assignee_q = convert_assignee_values(values, projects, request.user)
176+
177+
if filter.operator == "!=":
178+
queryset = queryset.exclude(assignee_q)
179+
else:
180+
queryset = queryset.filter(assignee_q)
141181
case SearchFilter(key=SearchKey("query"), operator="="):
142182
# 'query' is our free text key; all free text gets returned here
143183
# as '=', and we search any relevant fields for it.

tests/sentry/workflow_engine/endpoints/test_organization_detector_index.py

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from unittest import mock
22

3+
from django.db.models import Q
34
from rest_framework.exceptions import ErrorDetail
45

56
from sentry.api.serializers import serialize
67
from sentry.grouping.grouptype import ErrorGroupType
78
from sentry.incidents.grouptype import MetricIssue
89
from sentry.incidents.models.alert_rule import AlertRuleDetectionType
910
from sentry.models.environment import Environment
11+
from sentry.search.utils import _HACKY_INVALID_USER
1012
from sentry.snuba.dataset import Dataset
1113
from sentry.snuba.models import (
1214
QuerySubscription,
@@ -19,6 +21,7 @@
1921
from sentry.testutils.silo import region_silo_test
2022
from sentry.uptime.grouptype import UptimeDomainCheckFailure
2123
from sentry.uptime.types import DATA_SOURCE_UPTIME_SUBSCRIPTION
24+
from sentry.workflow_engine.endpoints.organization_detector_index import convert_assignee_values
2225
from sentry.workflow_engine.models import DataCondition, DataConditionGroup, DataSource, Detector
2326
from sentry.workflow_engine.models.data_condition import Condition
2427
from sentry.workflow_engine.models.detector_workflow import DetectorWorkflow
@@ -592,21 +595,68 @@ def test_invalid_owner(self):
592595
)
593596
assert "owner" in response.data
594597

595-
def test_owner_not_in_organization(self):
596-
# Create a user in another organization
597-
other_org = self.create_organization()
598-
other_user = self.create_user()
599-
self.create_member(organization=other_org, user=other_user)
600598

601-
# Test with owner not in current organization
602-
data_with_invalid_owner = {
603-
**self.valid_data,
604-
"owner": other_user.get_actor_identifier(),
605-
}
599+
@region_silo_test
600+
class ConvertAssigneeValuesTest(APITestCase):
601+
"""Test the convert_assignee_values function"""
606602

607-
response = self.get_error_response(
608-
self.organization.slug,
609-
**data_with_invalid_owner,
610-
status_code=400,
611-
)
612-
assert "owner" in response.data
603+
def setUp(self):
604+
super().setUp()
605+
self.user = self.create_user()
606+
self.team = self.create_team(organization=self.organization)
607+
self.other_user = self.create_user()
608+
self.create_member(organization=self.organization, user=self.other_user)
609+
self.projects = [self.project]
610+
611+
def test_convert_assignee_values_user_email(self):
612+
result = convert_assignee_values([self.user.email], self.projects, self.user)
613+
expected = Q(owner_user_id=self.user.id)
614+
self.assertEqual(str(result), str(expected))
615+
616+
def test_convert_assignee_values_user_username(self):
617+
result = convert_assignee_values([self.user.username], self.projects, self.user)
618+
expected = Q(owner_user_id=self.user.id)
619+
self.assertEqual(str(result), str(expected))
620+
621+
def test_convert_assignee_values_team_slug(self):
622+
result = convert_assignee_values([f"#{self.team.slug}"], self.projects, self.user)
623+
expected = Q(owner_team_id=self.team.id)
624+
self.assertEqual(str(result), str(expected))
625+
626+
def test_convert_assignee_values_me(self):
627+
result = convert_assignee_values(["me"], self.projects, self.user)
628+
expected = Q(owner_user_id=self.user.id)
629+
self.assertEqual(str(result), str(expected))
630+
631+
def test_convert_assignee_values_none(self):
632+
result = convert_assignee_values(["none"], self.projects, self.user)
633+
expected = Q(owner_team_id__isnull=True, owner_user_id__isnull=True)
634+
self.assertEqual(str(result), str(expected))
635+
636+
def test_convert_assignee_values_multiple(self):
637+
result = convert_assignee_values(
638+
[str(self.user.email), f"#{self.team.slug}"], self.projects, self.user
639+
)
640+
expected = Q(owner_user_id=self.user.id) | Q(owner_team_id=self.team.id)
641+
self.assertEqual(str(result), str(expected))
642+
643+
def test_convert_assignee_values_mixed(self):
644+
result = convert_assignee_values(
645+
["me", "none", f"#{self.team.slug}"], self.projects, self.user
646+
)
647+
expected = (
648+
Q(owner_user_id=self.user.id)
649+
| Q(owner_team_id__isnull=True, owner_user_id__isnull=True)
650+
| Q(owner_team_id=self.team.id)
651+
)
652+
self.assertEqual(str(result), str(expected))
653+
654+
def test_convert_assignee_values_invalid(self):
655+
result = convert_assignee_values(["999999"], self.projects, self.user)
656+
expected = Q(owner_user_id=_HACKY_INVALID_USER.id)
657+
self.assertEqual(str(result), str(expected))
658+
659+
def test_convert_assignee_values_empty(self):
660+
result = convert_assignee_values([], self.projects, self.user)
661+
expected = Q()
662+
self.assertEqual(str(result), str(expected))

0 commit comments

Comments
 (0)