Skip to content

Commit e6d3b77

Browse files
feat(aci): add alertrule/workflow GET endpoint (#97351)
used to get a rule/alert rule for a given workflow id or vice versa, for redirecting urls --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent ad983ef commit e6d3b77

File tree

6 files changed

+210
-2
lines changed

6 files changed

+210
-2
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from drf_spectacular.utils import extend_schema
2+
from rest_framework.response import Response
3+
4+
from sentry.api.api_owners import ApiOwner
5+
from sentry.api.api_publish_status import ApiPublishStatus
6+
from sentry.api.base import region_silo_endpoint
7+
from sentry.api.bases import OrganizationEndpoint
8+
from sentry.api.bases.organization import OrganizationDetectorPermission
9+
from sentry.api.exceptions import ResourceDoesNotExist
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.alertrule_workflow_serializer import (
19+
AlertRuleWorkflowSerializer,
20+
)
21+
from sentry.workflow_engine.endpoints.validators.alertrule_workflow import (
22+
AlertRuleWorkflowValidator,
23+
)
24+
from sentry.workflow_engine.models.alertrule_workflow import AlertRuleWorkflow
25+
26+
27+
@region_silo_endpoint
28+
class OrganizationAlertRuleWorkflowIndexEndpoint(OrganizationEndpoint):
29+
publish_status = {
30+
"GET": ApiPublishStatus.EXPERIMENTAL,
31+
}
32+
owner = ApiOwner.ISSUES
33+
permission_classes = (OrganizationDetectorPermission,)
34+
35+
@extend_schema(
36+
operation_id="Fetch Dual-Written Rule/Alert Rules and Workflows",
37+
parameters=[
38+
GlobalParams.ORG_ID_OR_SLUG,
39+
],
40+
responses={
41+
200: AlertRuleWorkflowSerializer,
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 dual-written rule/alert rule and its associated workflow.
51+
"""
52+
validator = AlertRuleWorkflowValidator(data=request.query_params)
53+
validator.is_valid(raise_exception=True)
54+
rule_id = validator.validated_data.get("rule_id")
55+
alert_rule_id = validator.validated_data.get("alert_rule_id")
56+
workflow_id = validator.validated_data.get("workflow_id")
57+
58+
queryset = AlertRuleWorkflow.objects.filter(workflow__organization=organization)
59+
60+
if workflow_id:
61+
queryset = queryset.filter(workflow_id=workflow_id)
62+
63+
if alert_rule_id:
64+
queryset = queryset.filter(alert_rule_id=alert_rule_id)
65+
66+
if rule_id:
67+
queryset = queryset.filter(rule_id=rule_id)
68+
69+
alert_rule_workflow = queryset.first()
70+
if not alert_rule_workflow:
71+
raise ResourceDoesNotExist
72+
73+
return Response(serialize(alert_rule_workflow, request.user))
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from collections.abc import Mapping
2+
from typing import Any, TypedDict
3+
4+
from sentry.api.serializers import Serializer, register
5+
from sentry.workflow_engine.models import AlertRuleWorkflow
6+
7+
8+
class ActionHandlerSerializerResponse(TypedDict):
9+
ruleId: str | None
10+
alertRuleId: str | None
11+
workflowId: str
12+
13+
14+
@register(AlertRuleWorkflow)
15+
class AlertRuleWorkflowSerializer(Serializer):
16+
def serialize(
17+
self, obj: AlertRuleWorkflow, attrs: Mapping[str, Any], user, **kwargs
18+
) -> ActionHandlerSerializerResponse:
19+
return {
20+
"ruleId": str(obj.rule_id) if obj.rule_id else None,
21+
"alertRuleId": str(obj.alert_rule_id) if obj.alert_rule_id else None,
22+
"workflowId": str(obj.workflow.id),
23+
}

src/sentry/workflow_engine/endpoints/urls.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from django.urls import re_path
22

3+
from sentry.workflow_engine.endpoints.organization_alertrule_workflow_index import (
4+
OrganizationAlertRuleWorkflowIndexEndpoint,
5+
)
6+
37
from .organization_available_action_index import OrganizationAvailableActionIndexEndpoint
48
from .organization_data_condition_index import OrganizationDataConditionIndexEndpoint
59
from .organization_detector_count import OrganizationDetectorCountEndpoint
@@ -97,4 +101,9 @@
97101
OrganizationOpenPeriodsEndpoint.as_view(),
98102
name="sentry-api-0-organization-open-periods",
99103
),
104+
re_path(
105+
r"^(?P<organization_id_or_slug>[^/]+)/alert-rule-workflow/$",
106+
OrganizationAlertRuleWorkflowIndexEndpoint.as_view(),
107+
name="sentry-api-0-organization-alert-rule-workflow-index",
108+
),
100109
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from rest_framework import serializers
2+
3+
4+
class AlertRuleWorkflowValidator(serializers.Serializer):
5+
rule_id = serializers.CharField(required=False)
6+
alert_rule_id = serializers.CharField(required=False)
7+
workflow_id = serializers.CharField(required=False)
8+
9+
def validate(self, attrs):
10+
super().validate(attrs)
11+
if (
12+
not attrs.get("rule_id")
13+
and not attrs.get("alert_rule_id")
14+
and not attrs.get("workflow_id")
15+
):
16+
raise serializers.ValidationError(
17+
"One of 'rule_id', 'alert_rule_id', or 'workflow_id' must be provided."
18+
)
19+
return attrs

static/app/utils/api/knownSentryApiUrls.generated.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,8 @@ export type KnownSentryApiUrls =
121121
| '/internal/prevent/pr-review/configs/resolved/'
122122
| '/internal/prevent/pr-review/github/sentry-org/'
123123
| '/internal/project-config/'
124-
| '/internal/queue/tasks/'
125124
| '/internal/rpc/$serviceName/$methodName/'
126125
| '/internal/seer-rpc/$methodName/'
127-
| '/internal/stats/'
128126
| '/internal/warnings/'
129127
| '/issues/$issueId/'
130128
| '/issues/$issueId/activities/'
@@ -192,6 +190,7 @@ export type KnownSentryApiUrls =
192190
| '/organizations/$organizationIdOrSlug/access-requests/'
193191
| '/organizations/$organizationIdOrSlug/access-requests/$requestId/'
194192
| '/organizations/$organizationIdOrSlug/ai-conversations/'
193+
| '/organizations/$organizationIdOrSlug/alert-rule-workflow/'
195194
| '/organizations/$organizationIdOrSlug/alert-rules/'
196195
| '/organizations/$organizationIdOrSlug/alert-rules/$alertRuleId/'
197196
| '/organizations/$organizationIdOrSlug/alert-rules/available-actions/'
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from sentry.api.serializers import serialize
2+
from sentry.testutils.cases import APITestCase
3+
from sentry.testutils.silo import region_silo_test
4+
5+
6+
class OrganizationAlertRuleWorkflowAPITestCase(APITestCase):
7+
endpoint = "sentry-api-0-organization-alert-rule-workflow-index"
8+
9+
def setUp(self) -> None:
10+
super().setUp()
11+
self.login_as(user=self.user)
12+
13+
self.workflow_1 = self.create_workflow(organization=self.organization)
14+
self.workflow_2 = self.create_workflow(organization=self.organization)
15+
self.workflow_3 = self.create_workflow(organization=self.organization)
16+
17+
self.alert_rule_workflow_1 = self.create_alert_rule_workflow(
18+
alert_rule_id=12345, workflow=self.workflow_1
19+
)
20+
self.alert_rule_workflow_2 = self.create_alert_rule_workflow(
21+
rule_id=67890, workflow=self.workflow_2
22+
)
23+
self.alert_rule_workflow_3 = self.create_alert_rule_workflow(
24+
alert_rule_id=11111, workflow=self.workflow_3
25+
)
26+
27+
# Create workflow in different organization to test filtering
28+
self.other_org = self.create_organization()
29+
self.other_workflow = self.create_workflow(organization=self.other_org)
30+
self.other_alert_rule_workflow = self.create_alert_rule_workflow(
31+
alert_rule_id=99999, workflow=self.other_workflow
32+
)
33+
34+
35+
@region_silo_test
36+
class OrganizationAlertRuleWorkflowIndexGetTest(OrganizationAlertRuleWorkflowAPITestCase):
37+
def test_get_with_workflow_id_filter(self) -> None:
38+
response = self.get_success_response(
39+
self.organization.slug, workflow_id=str(self.workflow_1.id)
40+
)
41+
assert response.data == serialize(self.alert_rule_workflow_1, self.user)
42+
43+
def test_get_with_alert_rule_id_filter(self) -> None:
44+
response = self.get_success_response(self.organization.slug, alert_rule_id="12345")
45+
46+
assert response.data["alertRuleId"] == "12345"
47+
assert response.data["ruleId"] is None
48+
assert response.data["workflowId"] == str(self.workflow_1.id)
49+
50+
def test_get_with_rule_id_filter(self) -> None:
51+
response = self.get_success_response(self.organization.slug, rule_id="67890")
52+
53+
assert response.data["ruleId"] == "67890"
54+
assert response.data["alertRuleId"] is None
55+
assert response.data["workflowId"] == str(self.workflow_2.id)
56+
57+
def test_get_with_multiple_filters(self) -> None:
58+
response = self.get_success_response(
59+
self.organization.slug,
60+
workflow_id=str(self.workflow_1.id),
61+
alert_rule_id="12345",
62+
)
63+
64+
assert response.data == serialize(self.alert_rule_workflow_1, self.user)
65+
66+
def test_get_with_multiple_filters_with_invalid_filter(self) -> None:
67+
self.get_error_response(
68+
self.organization.slug,
69+
workflow_id=str(self.workflow_1.id),
70+
alert_rule_id="this is not a valid ID",
71+
)
72+
73+
def test_get_with_nonexistent_workflow_id(self) -> None:
74+
self.get_error_response(self.organization.slug, workflow_id="99999", status_code=404)
75+
76+
def test_get_with_nonexistent_alert_rule_id(self) -> None:
77+
self.get_error_response(self.organization.slug, alert_rule_id="99999", status_code=404)
78+
79+
def test_get_with_nonexistent_rule_id(self) -> None:
80+
self.get_error_response(self.organization.slug, rule_id="99999", status_code=404)
81+
82+
def test_organization_isolation(self) -> None:
83+
self.get_error_response(
84+
self.organization.slug, workflow_id=str(self.other_workflow.id), status_code=404
85+
)

0 commit comments

Comments
 (0)