Skip to content

Commit a29b035

Browse files
feat(aci): add Incident to GroupOpenPeriod lookup endpoint (#103782)
need this endpoint to redirect metric alert urls to the new group open period --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 0ef83ff commit a29b035

File tree

6 files changed

+242
-7
lines changed

6 files changed

+242
-7
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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.incident_groupopenperiod_serializer import (
19+
IncidentGroupOpenPeriodSerializer,
20+
)
21+
from sentry.workflow_engine.endpoints.validators.incident_groupopenperiod import (
22+
IncidentGroupOpenPeriodValidator,
23+
)
24+
from sentry.workflow_engine.models.incident_groupopenperiod import IncidentGroupOpenPeriod
25+
26+
27+
@region_silo_endpoint
28+
class OrganizationIncidentGroupOpenPeriodIndexEndpoint(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 Incident and Group Open Period Relationship",
37+
parameters=[
38+
GlobalParams.ORG_ID_OR_SLUG,
39+
],
40+
responses={
41+
200: IncidentGroupOpenPeriodSerializer,
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 an incident and group open period relationship.
51+
Can optionally filter by incident_id, incident_identifier, group_id, or open_period_id.
52+
"""
53+
validator = IncidentGroupOpenPeriodValidator(data=request.query_params)
54+
validator.is_valid(raise_exception=True)
55+
incident_id = validator.validated_data.get("incident_id")
56+
incident_identifier = validator.validated_data.get("incident_identifier")
57+
group_id = validator.validated_data.get("group_id")
58+
open_period_id = validator.validated_data.get("open_period_id")
59+
60+
queryset = IncidentGroupOpenPeriod.objects.filter(
61+
group_open_period__project__organization=organization
62+
)
63+
64+
if incident_id:
65+
queryset = queryset.filter(incident_id=incident_id)
66+
67+
if incident_identifier:
68+
queryset = queryset.filter(incident_identifier=incident_identifier)
69+
70+
if group_id:
71+
queryset = queryset.filter(group_open_period__group_id=group_id)
72+
73+
if open_period_id:
74+
queryset = queryset.filter(group_open_period_id=open_period_id)
75+
76+
incident_groupopenperiod = queryset.first()
77+
if not incident_groupopenperiod:
78+
raise ResourceDoesNotExist
79+
80+
return Response(serialize(incident_groupopenperiod, request.user))
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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.incident_groupopenperiod import IncidentGroupOpenPeriod
6+
7+
8+
class IncidentGroupOpenPeriodSerializerResponse(TypedDict):
9+
incidentId: str | None
10+
incidentIdentifier: int | None
11+
groupId: str
12+
openPeriodId: str
13+
14+
15+
@register(IncidentGroupOpenPeriod)
16+
class IncidentGroupOpenPeriodSerializer(Serializer):
17+
def serialize(
18+
self, obj: IncidentGroupOpenPeriod, attrs: Mapping[str, Any], user, **kwargs
19+
) -> IncidentGroupOpenPeriodSerializerResponse:
20+
return {
21+
"incidentId": str(obj.incident_id) if obj.incident_id else None,
22+
"incidentIdentifier": obj.incident_identifier,
23+
"groupId": str(obj.group_open_period.group_id),
24+
"openPeriodId": str(obj.group_open_period.id),
25+
}

src/sentry/workflow_engine/endpoints/urls.py

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

3-
from sentry.workflow_engine.endpoints.organization_alertrule_detector_index import (
4-
OrganizationAlertRuleDetectorIndexEndpoint,
5-
)
6-
from sentry.workflow_engine.endpoints.organization_alertrule_workflow_index import (
7-
OrganizationAlertRuleWorkflowIndexEndpoint,
8-
)
9-
3+
from .organization_alertrule_detector_index import OrganizationAlertRuleDetectorIndexEndpoint
4+
from .organization_alertrule_workflow_index import OrganizationAlertRuleWorkflowIndexEndpoint
105
from .organization_available_action_index import OrganizationAvailableActionIndexEndpoint
116
from .organization_data_condition_index import OrganizationDataConditionIndexEndpoint
127
from .organization_detector_count import OrganizationDetectorCountEndpoint
@@ -15,6 +10,9 @@
1510
from .organization_detector_types import OrganizationDetectorTypeIndexEndpoint
1611
from .organization_detector_workflow_details import OrganizationDetectorWorkflowDetailsEndpoint
1712
from .organization_detector_workflow_index import OrganizationDetectorWorkflowIndexEndpoint
13+
from .organization_incident_groupopenperiod_index import (
14+
OrganizationIncidentGroupOpenPeriodIndexEndpoint,
15+
)
1816
from .organization_open_periods import OrganizationOpenPeriodsEndpoint
1917
from .organization_test_fire_action import OrganizationTestFireActionsEndpoint
2018
from .organization_workflow_details import OrganizationWorkflowDetailsEndpoint
@@ -114,4 +112,9 @@
114112
OrganizationAlertRuleDetectorIndexEndpoint.as_view(),
115113
name="sentry-api-0-organization-alert-rule-detector-index",
116114
),
115+
re_path(
116+
r"^(?P<organization_id_or_slug>[^/]+)/incident-groupopenperiod/$",
117+
OrganizationIncidentGroupOpenPeriodIndexEndpoint.as_view(),
118+
name="sentry-api-0-organization-incident-groupopenperiod-index",
119+
),
117120
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from rest_framework import serializers
2+
3+
4+
class IncidentGroupOpenPeriodValidator(serializers.Serializer):
5+
incident_id = serializers.IntegerField(required=False)
6+
incident_identifier = serializers.IntegerField(required=False)
7+
group_id = serializers.IntegerField(required=False)
8+
open_period_id = serializers.IntegerField(required=False)
9+
10+
def validate(self, attrs):
11+
super().validate(attrs)
12+
if (
13+
not attrs.get("incident_id")
14+
and not attrs.get("incident_identifier")
15+
and not attrs.get("group_id")
16+
and not attrs.get("open_period_id")
17+
):
18+
raise serializers.ValidationError(
19+
"One of 'incident_id', 'incident_identifier', 'group_id', or 'open_period_id' must be provided."
20+
)
21+
return attrs

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ export type KnownSentryApiUrls =
346346
| '/organizations/$organizationIdOrSlug/groups/$issueId/tags/$key/values/'
347347
| '/organizations/$organizationIdOrSlug/groups/$issueId/user-feedback/'
348348
| '/organizations/$organizationIdOrSlug/groups/$issueId/user-reports/'
349+
| '/organizations/$organizationIdOrSlug/incident-groupopenperiod/'
349350
| '/organizations/$organizationIdOrSlug/incidents/'
350351
| '/organizations/$organizationIdOrSlug/incidents/$incidentIdentifier/'
351352
| '/organizations/$organizationIdOrSlug/insights/starred-segments/'
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from sentry.api.serializers import serialize
2+
from sentry.incidents.grouptype import MetricIssue
3+
from sentry.models.groupopenperiod import GroupOpenPeriod
4+
from sentry.testutils.cases import APITestCase
5+
from sentry.testutils.silo import region_silo_test
6+
7+
8+
class OrganizationIncidentGroupOpenPeriodAPITestCase(APITestCase):
9+
endpoint = "sentry-api-0-organization-incident-groupopenperiod-index"
10+
11+
def setUp(self) -> None:
12+
super().setUp()
13+
self.login_as(user=self.user)
14+
15+
# Create groups and incidents
16+
self.group_1 = self.create_group(type=MetricIssue.type_id)
17+
self.group_2 = self.create_group(type=MetricIssue.type_id)
18+
self.group_3 = self.create_group(type=MetricIssue.type_id)
19+
20+
# Get the open periods created automatically with the groups
21+
self.open_period_1 = GroupOpenPeriod.objects.get(group=self.group_1)
22+
self.open_period_2 = GroupOpenPeriod.objects.get(group=self.group_2)
23+
self.open_period_3 = GroupOpenPeriod.objects.get(group=self.group_3)
24+
25+
# Create incidents
26+
self.alert_rule = self.create_alert_rule()
27+
self.incident_1 = self.create_incident(
28+
organization=self.organization, projects=[self.project], alert_rule=self.alert_rule
29+
)
30+
self.incident_2 = self.create_incident(
31+
organization=self.organization, projects=[self.project], alert_rule=self.alert_rule
32+
)
33+
self.incident_3 = self.create_incident(
34+
organization=self.organization, projects=[self.project], alert_rule=self.alert_rule
35+
)
36+
37+
# Create incident-group-open-period relationships
38+
self.igop_1 = self.create_incident_group_open_period(
39+
incident=self.incident_1, group_open_period=self.open_period_1
40+
)
41+
self.igop_2 = self.create_incident_group_open_period(
42+
incident=self.incident_2, group_open_period=self.open_period_2
43+
)
44+
self.igop_3 = self.create_incident_group_open_period(
45+
incident=self.incident_3, group_open_period=self.open_period_3
46+
)
47+
48+
49+
@region_silo_test
50+
class OrganizationIncidentGroupOpenPeriodIndexGetTest(
51+
OrganizationIncidentGroupOpenPeriodAPITestCase
52+
):
53+
def test_get_with_incident_id_filter(self) -> None:
54+
response = self.get_success_response(
55+
self.organization.slug, incident_id=str(self.incident_1.id)
56+
)
57+
assert response.data == serialize(self.igop_1, self.user)
58+
59+
def test_get_with_incident_identifier_filter(self) -> None:
60+
response = self.get_success_response(
61+
self.organization.slug, incident_identifier=str(self.incident_1.identifier)
62+
)
63+
assert response.data == serialize(self.igop_1, self.user)
64+
65+
def test_get_with_group_id_filter(self) -> None:
66+
response = self.get_success_response(self.organization.slug, group_id=str(self.group_2.id))
67+
assert response.data == serialize(self.igop_2, self.user)
68+
69+
def test_get_with_open_period_id_filter(self) -> None:
70+
response = self.get_success_response(
71+
self.organization.slug, open_period_id=str(self.open_period_3.id)
72+
)
73+
assert response.data == serialize(self.igop_3, self.user)
74+
75+
def test_get_with_multiple_filters(self) -> None:
76+
response = self.get_success_response(
77+
self.organization.slug,
78+
incident_id=str(self.incident_1.id),
79+
group_id=str(self.group_1.id),
80+
)
81+
assert response.data == serialize(self.igop_1, self.user)
82+
83+
def test_get_with_multiple_filters_with_invalid_filter(self) -> None:
84+
self.get_error_response(
85+
self.organization.slug,
86+
incident_id=str(self.incident_1.id),
87+
group_id="99999",
88+
)
89+
90+
def test_get_with_nonexistent_incident_id(self) -> None:
91+
self.get_error_response(self.organization.slug, incident_id="99999", status_code=404)
92+
93+
def test_get_with_nonexistent_incident_identifier(self) -> None:
94+
self.get_error_response(
95+
self.organization.slug, incident_identifier="99999", status_code=404
96+
)
97+
98+
def test_get_with_nonexistent_group_id(self) -> None:
99+
self.get_error_response(self.organization.slug, group_id="99999", status_code=404)
100+
101+
def test_get_with_nonexistent_open_period_id(self) -> None:
102+
self.get_error_response(self.organization.slug, open_period_id="99999", status_code=404)
103+
104+
def test_no_filter_provided(self) -> None:
105+
self.get_error_response(self.organization.slug, status_code=400)

0 commit comments

Comments
 (0)