Skip to content

Commit 0ea16df

Browse files
committed
feat(aci): add GET DataCondition endpoint
1 parent 250c257 commit 0ea16df

File tree

5 files changed

+189
-1
lines changed

5 files changed

+189
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from drf_spectacular.utils import extend_schema
2+
from rest_framework import serializers, status
3+
from rest_framework.response import Response
4+
5+
from sentry.api.api_owners import ApiOwner
6+
from sentry.api.api_publish_status import ApiPublishStatus
7+
from sentry.api.base import region_silo_endpoint
8+
from sentry.api.bases import OrganizationEndpoint
9+
from sentry.api.paginator import OffsetPaginator
10+
from sentry.api.serializers import serialize
11+
from sentry.apidocs.constants import (
12+
RESPONSE_BAD_REQUEST,
13+
RESPONSE_FORBIDDEN,
14+
RESPONSE_NOT_FOUND,
15+
RESPONSE_UNAUTHORIZED,
16+
)
17+
from sentry.apidocs.parameters import GlobalParams
18+
from sentry.workflow_engine.endpoints.serializers import DataConditionHandlerSerializer
19+
from sentry.workflow_engine.models.data_condition import IGNORED_CONDITIONS
20+
from sentry.workflow_engine.registry import condition_handler_registry
21+
from sentry.workflow_engine.types import DataConditionHandler
22+
23+
24+
class DataConditionRequestSerializer(serializers.Serializer):
25+
type = serializers.ChoiceField(choices=DataConditionHandler.Type, required=False)
26+
27+
28+
@region_silo_endpoint
29+
class OrganizationDataConditionIndexEndpoint(OrganizationEndpoint):
30+
publish_status = {
31+
"GET": ApiPublishStatus.EXPERIMENTAL,
32+
}
33+
owner = ApiOwner.ISSUES
34+
35+
@extend_schema(
36+
operation_id="Fetch Data Conditions",
37+
parameters=[
38+
GlobalParams.ORG_ID_OR_SLUG,
39+
],
40+
responses={
41+
201: DataConditionHandlerSerializer,
42+
400: RESPONSE_BAD_REQUEST,
43+
401: RESPONSE_UNAUTHORIZED,
44+
403: RESPONSE_FORBIDDEN,
45+
404: RESPONSE_NOT_FOUND,
46+
},
47+
)
48+
def get(self, request, organization):
49+
"""
50+
Returns a list of data conditions for a given org
51+
"""
52+
serializer = DataConditionRequestSerializer(data=request.GET)
53+
if not serializer.is_valid():
54+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
55+
serialized = serializer.validated_data
56+
57+
type = serialized.get("type")
58+
59+
data_conditions = []
60+
61+
for condition, handler in condition_handler_registry.registrations.items():
62+
if not type or (condition not in IGNORED_CONDITIONS and handler.type == type):
63+
condition_json = {"condition_id": condition.value}
64+
condition_json.update(
65+
serialize(handler, request.user, DataConditionHandlerSerializer())
66+
)
67+
data_conditions.append(condition_json)
68+
69+
return self.paginate(
70+
request=request,
71+
queryset=data_conditions,
72+
paginator_cls=OffsetPaginator,
73+
)

src/sentry/workflow_engine/endpoints/serializers.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
WorkflowDataConditionGroup,
1818
)
1919
from sentry.workflow_engine.models.data_condition_group_action import DataConditionGroupAction
20-
from sentry.workflow_engine.types import DataSourceTypeHandler
20+
from sentry.workflow_engine.types import DataConditionHandler, DataSourceTypeHandler
2121

2222

2323
@register(Action)
@@ -122,6 +122,18 @@ def serialize(
122122
}
123123

124124

125+
@register(DataConditionHandler)
126+
class DataConditionHandlerSerializer(Serializer):
127+
def serialize(self, obj: DataConditionHandler, *args, **kwargs) -> dict[str, Any]:
128+
result = {
129+
"type": obj.type.value,
130+
"comparison_json_schema": obj.comparison_json_schema,
131+
}
132+
if hasattr(obj, "filter_group"):
133+
result["filter_group"] = obj.filter_group.value
134+
return result
135+
136+
125137
@register(Detector)
126138
class DetectorSerializer(Serializer):
127139
def get_attrs(

src/sentry/workflow_engine/endpoints/urls.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.urls import re_path
22

3+
from .organization_data_condition_index import OrganizationDataConditionIndexEndpoint
34
from .organization_workflow_details import OrganizationWorkflowDetailsEndpoint
45
from .organization_workflow_index import OrganizationWorkflowIndexEndpoint
56
from .project_detector_details import ProjectDetectorDetailsEndpoint
@@ -40,4 +41,9 @@
4041
OrganizationWorkflowDetailsEndpoint.as_view(),
4142
name="sentry-api-0-organization-workflow-details",
4243
),
44+
re_path(
45+
r"^(?P<organization_id_or_slug>[^\/]+)/data_conditions/$",
46+
OrganizationDataConditionIndexEndpoint.as_view(),
47+
name="sentry-api-0-organization-data-condition-index",
48+
),
4349
]

src/sentry/workflow_engine/models/data_condition.py

+11
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ class Condition(StrEnum):
8282
Condition.PERCENT_SESSIONS_COUNT,
8383
] + PERCENT_CONDITIONS
8484

85+
# Conditions that are not supported in the UI
86+
IGNORED_CONDITIONS = [
87+
Condition.EVERY_EVENT,
88+
Condition.EVENT_CREATED_BY_DETECTOR,
89+
Condition.EVENT_SEEN_COUNT,
90+
Condition.NEW_HIGH_PRIORITY_ISSUE,
91+
Condition.EXISTING_HIGH_PRIORITY_ISSUE,
92+
Condition.ISSUE_CATEGORY,
93+
Condition.ISSUE_RESOLUTION_CHANGE,
94+
]
95+
8596

8697
T = TypeVar("T")
8798

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from dataclasses import dataclass
2+
from unittest.mock import patch
3+
4+
from sentry.testutils.cases import APITestCase
5+
from sentry.testutils.silo import region_silo_test
6+
from sentry.utils.registry import Registry
7+
from sentry.workflow_engine.models.data_condition import Condition
8+
from sentry.workflow_engine.types import DataConditionHandler
9+
10+
11+
class OrganizationDataConditionAPITestCase(APITestCase):
12+
endpoint = "sentry-api-0-organization-data-condition-index"
13+
14+
def setUp(self):
15+
super().setUp()
16+
self.login_as(user=self.user)
17+
self.registry = Registry[DataConditionHandler](enable_reverse_lookup=False)
18+
self.registry_patcher = patch(
19+
"sentry.workflow_engine.registry.condition_handler_registry",
20+
new=self.registry,
21+
)
22+
self.registry_patcher.__enter__()
23+
24+
@self.registry.register(Condition.REAPPEARED_EVENT)
25+
@dataclass(frozen=True)
26+
class TestWorkflowTrigger(DataConditionHandler):
27+
type = DataConditionHandler.Type.WORKFLOW_TRIGGER
28+
comparison_json_schema = {"type": "boolean"}
29+
30+
@self.registry.register(Condition.AGE_COMPARISON)
31+
@dataclass(frozen=True)
32+
class TestActionFilter(DataConditionHandler):
33+
type = DataConditionHandler.Type.ACTION_FILTER
34+
filter_group = DataConditionHandler.FilterGroup.ISSUE_ATTRIBUTES
35+
comparison_json_schema = {
36+
"type": "object",
37+
"properties": {
38+
"value": {"type": "integer", "minimum": 0},
39+
},
40+
"required": ["value"],
41+
"additionalProperties": False,
42+
}
43+
44+
def tearDown(self) -> None:
45+
super().tearDown()
46+
self.registry_patcher.__exit__(None, None, None)
47+
48+
49+
@region_silo_test
50+
class OrganizationDataCondiitonIndexBaseTest(OrganizationDataConditionAPITestCase):
51+
def test_simple(self):
52+
response = self.get_success_response(self.organization.slug)
53+
assert len(response.data) == 2
54+
55+
def test_type_filter(self):
56+
response = self.get_success_response(
57+
self.organization.slug, type=DataConditionHandler.Type.WORKFLOW_TRIGGER
58+
)
59+
assert len(response.data) == 1
60+
assert response.data[0] == {
61+
"condition_id": Condition.REAPPEARED_EVENT.value,
62+
"type": DataConditionHandler.Type.WORKFLOW_TRIGGER.value,
63+
"comparison_json_schema": {"type": "boolean"},
64+
}
65+
66+
response = self.get_success_response(
67+
self.organization.slug, type=DataConditionHandler.Type.ACTION_FILTER
68+
)
69+
assert len(response.data) == 1
70+
assert response.data[0] == {
71+
"condition_id": Condition.AGE_COMPARISON.value,
72+
"type": DataConditionHandler.Type.ACTION_FILTER.value,
73+
"filter_group": DataConditionHandler.FilterGroup.ISSUE_ATTRIBUTES.value,
74+
"comparison_json_schema": {
75+
"type": "object",
76+
"properties": {
77+
"value": {"type": "integer", "minimum": 0},
78+
},
79+
"required": ["value"],
80+
"additionalProperties": False,
81+
},
82+
}
83+
84+
def test_invalid_type(self):
85+
response = self.get_error_response(self.organization.slug, type="invalid")
86+
assert response.status_code == 400

0 commit comments

Comments
 (0)