Skip to content

Commit 857ba9d

Browse files
authored
feat(open periods): Add activities to open period serializer (getsentry#100627)
Add `GroupOpenPeriodActivity` results to the group open period serializer response.
1 parent ca3c674 commit 857ba9d

File tree

3 files changed

+125
-12
lines changed

3 files changed

+125
-12
lines changed

src/sentry/models/groupopenperiodactivity.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ class OpenPeriodActivityType(IntEnum):
1313
STATUS_CHANGE = 2
1414
CLOSED = 3
1515

16+
def to_str(self) -> str:
17+
"""
18+
Return the string representation of the activity type.
19+
"""
20+
return self.name.lower()
21+
1622

1723
def generate_random_uuid() -> UUID:
1824
return uuid4()
Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
from collections import defaultdict
12
from collections.abc import Mapping
23
from datetime import datetime, timedelta
34
from typing import Any, TypedDict
45

5-
from sentry.api.serializers import Serializer, register
6+
from sentry.api.serializers import Serializer, register, serialize
67
from sentry.models.groupopenperiod import GroupOpenPeriod, get_last_checked_for_open_period
8+
from sentry.models.groupopenperiodactivity import GroupOpenPeriodActivity, OpenPeriodActivityType
9+
from sentry.types.group import PriorityLevel
10+
11+
12+
class GroupOpenPeriodActivityResponse(TypedDict):
13+
id: str
14+
type: str
15+
value: str | None
716

817

918
class GroupOpenPeriodResponse(TypedDict):
@@ -13,18 +22,50 @@ class GroupOpenPeriodResponse(TypedDict):
1322
duration: timedelta | None
1423
isOpen: bool
1524
lastChecked: datetime
25+
activities: list[GroupOpenPeriodActivityResponse] | None
26+
27+
28+
@register(GroupOpenPeriodActivity)
29+
class GroupOpenPeriodActivitySerializer(Serializer):
30+
def serialize(
31+
self, obj: GroupOpenPeriodActivity, attrs: Mapping[str, Any], user, **kwargs
32+
) -> GroupOpenPeriodActivityResponse:
33+
return GroupOpenPeriodActivityResponse(
34+
id=str(obj.id),
35+
type=OpenPeriodActivityType(obj.type).to_str(),
36+
value=PriorityLevel(obj.value).to_str() if obj.value else None,
37+
)
1638

1739

1840
@register(GroupOpenPeriod)
1941
class GroupOpenPeriodSerializer(Serializer):
42+
def get_attrs(self, item_list, user, **kwargs):
43+
result: defaultdict[GroupOpenPeriod, dict[str, list[GroupOpenPeriodActivityResponse]]] = (
44+
defaultdict(dict)
45+
)
46+
activities = GroupOpenPeriodActivity.objects.filter(
47+
group_open_period__in=item_list
48+
).order_by("id")
49+
50+
gopas = defaultdict(list)
51+
for activity, serialized_activity in zip(
52+
activities, serialize(list(activities), user=user, **kwargs)
53+
):
54+
gopas[activity.group_open_period].append(serialized_activity)
55+
for item in item_list:
56+
result[item]["activities"] = gopas[item][:100]
57+
58+
return result
59+
2060
def serialize(
2161
self, obj: GroupOpenPeriod, attrs: Mapping[str, Any], user, **kwargs
2262
) -> GroupOpenPeriodResponse:
23-
return {
24-
"id": str(obj.id),
25-
"start": obj.date_started,
26-
"end": obj.date_ended,
27-
"duration": obj.date_ended - obj.date_started if obj.date_ended else None,
28-
"isOpen": obj.date_ended is None,
29-
"lastChecked": get_last_checked_for_open_period(obj.group),
30-
}
63+
return GroupOpenPeriodResponse(
64+
id=str(obj.id),
65+
start=obj.date_started,
66+
end=obj.date_ended,
67+
duration=obj.date_ended - obj.date_started if obj.date_ended else None,
68+
isOpen=obj.date_ended is None,
69+
lastChecked=get_last_checked_for_open_period(obj.group),
70+
activities=attrs.get("activities"),
71+
)

tests/sentry/workflow_engine/endpoints/test_organization_open_periods.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
get_open_periods_for_group,
1313
update_group_open_period,
1414
)
15+
from sentry.models.groupopenperiodactivity import GroupOpenPeriodActivity, OpenPeriodActivityType
1516
from sentry.testutils.cases import APITestCase
1617
from sentry.types.activity import ActivityType
18+
from sentry.types.group import PriorityLevel
1719
from sentry.workflow_engine.models.detector_group import DetectorGroup
1820

1921

@@ -27,14 +29,23 @@ def setUp(self) -> None:
2729
self.login_as(user=self.user)
2830

2931
self.detector = self.create_detector()
30-
self.group = self.create_group()
32+
self.group = self.create_group(priority=PriorityLevel.LOW)
3133
# Metric issue is the only type (currently) that has open periods
3234
self.group.type = MetricIssue.type_id
3335
self.group.save()
3436

3537
# Link detector to group
3638
DetectorGroup.objects.create(detector=self.detector, group=self.group)
3739

40+
self.group_open_period = GroupOpenPeriod.objects.get(group=self.group)
41+
42+
self.opened_gopa = GroupOpenPeriodActivity.objects.create(
43+
date_added=self.group_open_period.date_added,
44+
group_open_period=self.group_open_period,
45+
type=OpenPeriodActivityType.OPENED,
46+
value=self.group.priority,
47+
)
48+
3849
def get_url_args(self):
3950
return [self.organization.slug]
4051

@@ -58,6 +69,12 @@ def test_open_period_linked_to_group(self) -> None:
5869
assert open_period["end"] is None
5970
assert open_period["duration"] is None
6071
assert open_period["isOpen"] is True
72+
assert len(open_period["activities"]) == 1
73+
assert open_period["activities"][0] == {
74+
"id": str(self.opened_gopa.id),
75+
"type": OpenPeriodActivityType.OPENED.to_str(),
76+
"value": PriorityLevel(self.group.priority).to_str(),
77+
}
6178

6279
def test_open_periods_group_id(self) -> None:
6380
response = self.get_success_response(
@@ -82,13 +99,17 @@ def test_open_periods_new_group_with_last_checked(self) -> None:
8299
assert response.status_code == 200, response.content
83100
assert len(response.data) == 1
84101
resp = response.data[0]
85-
open_period = GroupOpenPeriod.objects.get(group=self.group)
86-
assert resp["id"] == str(open_period.id)
102+
assert resp["id"] == str(self.group_open_period.id)
87103
assert resp["start"] == self.group.first_seen
88104
assert resp["end"] is None
89105
assert resp["duration"] is None
90106
assert resp["isOpen"] is True
91107
assert resp["lastChecked"] >= last_checked
108+
assert resp["activities"][0] == {
109+
"id": str(self.opened_gopa.id),
110+
"type": OpenPeriodActivityType.OPENED.to_str(),
111+
"value": PriorityLevel(self.group.priority).to_str(),
112+
}
92113

93114
def test_open_periods_resolved_group(self) -> None:
94115
self.group.status = GroupStatus.RESOLVED
@@ -114,6 +135,9 @@ def test_open_periods_resolved_group(self) -> None:
114135
assert response.status_code == 200, response.content
115136
resp = response.data[0]
116137
open_period = GroupOpenPeriod.objects.get(group=self.group, date_ended=resolved_time)
138+
closed_gopa = GroupOpenPeriodActivity.objects.get(
139+
group_open_period=open_period, type=OpenPeriodActivityType.CLOSED
140+
)
117141
assert resp["id"] == str(open_period.id)
118142
assert resp["start"] == self.group.first_seen
119143
assert resp["end"] == resolved_time
@@ -122,6 +146,17 @@ def test_open_periods_resolved_group(self) -> None:
122146
assert resp["lastChecked"].replace(second=0, microsecond=0) == activity.datetime.replace(
123147
second=0, microsecond=0
124148
)
149+
assert len(resp["activities"]) == 2
150+
assert resp["activities"][0] == {
151+
"id": str(self.opened_gopa.id),
152+
"type": OpenPeriodActivityType.OPENED.to_str(),
153+
"value": PriorityLevel(self.group.priority).to_str(),
154+
}
155+
assert resp["activities"][1] == {
156+
"id": str(closed_gopa.id),
157+
"type": OpenPeriodActivityType.CLOSED.to_str(),
158+
"value": None,
159+
}
125160

126161
def test_open_periods_unresolved_group(self) -> None:
127162
self.group.status = GroupStatus.RESOLVED
@@ -140,6 +175,9 @@ def test_open_periods_unresolved_group(self) -> None:
140175
resolution_activity=resolve_activity,
141176
)
142177
open_period = GroupOpenPeriod.objects.get(group=self.group, date_ended=resolved_time)
178+
closed_gopa = GroupOpenPeriodActivity.objects.get(
179+
group_open_period=open_period, type=OpenPeriodActivityType.CLOSED
180+
)
143181

144182
unresolved_time = timezone.now()
145183
self.group.status = GroupStatus.UNRESOLVED
@@ -170,6 +208,12 @@ def test_open_periods_unresolved_group(self) -> None:
170208
open_period2 = GroupOpenPeriod.objects.get(
171209
group=self.group, date_ended=second_resolved_time
172210
)
211+
opened_gopa2 = GroupOpenPeriodActivity.objects.get(
212+
group_open_period=open_period2, type=OpenPeriodActivityType.OPENED
213+
)
214+
closed_gopa2 = GroupOpenPeriodActivity.objects.get(
215+
group_open_period=open_period2, type=OpenPeriodActivityType.CLOSED
216+
)
173217

174218
response = self.get_success_response(
175219
*self.get_url_args(), qs_params={"groupId": self.group.id}
@@ -186,6 +230,17 @@ def test_open_periods_unresolved_group(self) -> None:
186230
assert resp["lastChecked"].replace(second=0, microsecond=0) == second_resolved_time.replace(
187231
second=0, microsecond=0
188232
)
233+
assert len(resp["activities"]) == 2
234+
assert resp["activities"][0] == {
235+
"id": str(opened_gopa2.id),
236+
"type": OpenPeriodActivityType.OPENED.to_str(),
237+
"value": PriorityLevel(self.group.priority).to_str(),
238+
}
239+
assert resp["activities"][1] == {
240+
"id": str(closed_gopa2.id),
241+
"type": OpenPeriodActivityType.CLOSED.to_str(),
242+
"value": None,
243+
}
189244

190245
assert resp2["id"] == str(open_period.id)
191246
assert resp2["start"] == self.group.first_seen
@@ -195,6 +250,17 @@ def test_open_periods_unresolved_group(self) -> None:
195250
assert resp2["lastChecked"].replace(second=0, microsecond=0) == resolved_time.replace(
196251
second=0, microsecond=0
197252
)
253+
assert len(resp2["activities"]) == 2
254+
assert resp2["activities"][0] == {
255+
"id": str(self.opened_gopa.id),
256+
"type": OpenPeriodActivityType.OPENED.to_str(),
257+
"value": PriorityLevel(self.group.priority).to_str(),
258+
}
259+
assert resp2["activities"][1] == {
260+
"id": str(closed_gopa.id),
261+
"type": OpenPeriodActivityType.CLOSED.to_str(),
262+
"value": None,
263+
}
198264

199265
def test_open_periods_limit(self) -> None:
200266
self.group.status = GroupStatus.RESOLVED

0 commit comments

Comments
 (0)