Browse Source

feat(crons): Serialize active incident onto monitor environment (#66989)

Serializes an active incident onto each monitor environment which may
contain information about broken detections that happened during that
incident
David Wang 11 months ago
parent
commit
96a58512e0

+ 72 - 2
src/sentry/monitors/serializers.py

@@ -11,7 +11,59 @@ from sentry.monitors.utils import fetch_associated_groups
 from sentry.monitors.validators import IntervalNames
 
 from ..models import Environment
-from .models import Monitor, MonitorCheckIn, MonitorEnvironment, MonitorStatus
+from .models import (
+    Monitor,
+    MonitorCheckIn,
+    MonitorEnvBrokenDetection,
+    MonitorEnvironment,
+    MonitorIncident,
+    MonitorStatus,
+)
+
+
+class MonitorEnvBrokenDetectionSerializerResponse(TypedDict):
+    userNotifiedTimestamp: datetime
+    environmentMutedTimestamp: datetime
+
+
+@register(MonitorEnvBrokenDetection)
+class MonitorEnvBrokenDetectionSerializer(Serializer):
+    def serialize(self, obj, attrs, user, **kwargs) -> MonitorEnvBrokenDetectionSerializerResponse:
+        return {
+            "userNotifiedTimestamp": obj.user_notified_timestamp,
+            "environmentMutedTimestamp": obj.env_muted_timestamp,
+        }
+
+
+class MonitorIncidentSerializerResponse(TypedDict):
+    startingTimestamp: datetime
+    resolvingTimestamp: datetime
+    brokenNotice: MonitorEnvBrokenDetectionSerializerResponse | None
+
+
+@register(MonitorIncident)
+class MonitorIncidentSerializer(Serializer):
+    def get_attrs(
+        self, item_list: Sequence[Any], user: Any, **kwargs: Any
+    ) -> MutableMapping[Any, Any]:
+        broken_detections = list(
+            MonitorEnvBrokenDetection.objects.filter(monitor_incident__in=item_list)
+        )
+        serialized_broken_detections = {
+            detection.monitor_incident_id: serialized
+            for serialized, detection in zip(serialize(broken_detections, user), broken_detections)
+        }
+        return {
+            incident: {"broken_detection": serialized_broken_detections.get(incident.id)}
+            for incident in item_list
+        }
+
+    def serialize(self, obj, attrs, user, **kwargs) -> MonitorIncidentSerializerResponse:
+        return {
+            "startingTimestamp": obj.starting_timestamp,
+            "resolvingTimestamp": obj.resolving_timestamp,
+            "brokenNotice": attrs["broken_detection"],
+        }
 
 
 class MonitorEnvironmentSerializerResponse(TypedDict):
@@ -22,6 +74,7 @@ class MonitorEnvironmentSerializerResponse(TypedDict):
     lastCheckIn: datetime
     nextCheckIn: datetime
     nextCheckInLatest: datetime
+    activeIncident: MonitorIncidentSerializerResponse | None
 
 
 @register(MonitorEnvironment)
@@ -34,8 +87,24 @@ class MonitorEnvironmentSerializer(Serializer):
         ]
         environments = {env.id: env for env in Environment.objects.filter(id__in=env_ids)}
 
+        active_incidents = list(
+            MonitorIncident.objects.filter(
+                monitor_environment__in=item_list,
+                resolving_checkin=None,
+            )
+        )
+        serialized_incidents = {
+            incident.monitor_environment_id: serialized_incident
+            for incident, serialized_incident in zip(
+                active_incidents, serialize(active_incidents, user)
+            )
+        }
+
         return {
-            monitor_env: {"environment": environments[monitor_env.environment_id]}
+            monitor_env: {
+                "environment": environments[monitor_env.environment_id],
+                "active_incident": serialized_incidents.get(monitor_env.id),
+            }
             for monitor_env in item_list
         }
 
@@ -48,6 +117,7 @@ class MonitorEnvironmentSerializer(Serializer):
             "lastCheckIn": obj.last_checkin,
             "nextCheckIn": obj.next_checkin,
             "nextCheckInLatest": obj.next_checkin_latest,
+            "activeIncident": attrs["active_incident"],
         }
 
 

+ 51 - 1
tests/sentry/monitors/endpoints/test_base_monitor_details.py

@@ -1,4 +1,4 @@
-from datetime import timedelta
+from datetime import datetime, timedelta, timezone
 from unittest.mock import patch
 
 import pytest
@@ -13,7 +13,9 @@ from sentry.monitors.models import (
     CheckInStatus,
     Monitor,
     MonitorCheckIn,
+    MonitorEnvBrokenDetection,
     MonitorEnvironment,
+    MonitorIncident,
     ScheduleType,
 )
 from sentry.monitors.utils import get_timeout_at
@@ -69,6 +71,7 @@ class BaseMonitorDetailsTest(MonitorTestCase):
                 "lastCheckIn": jungle.last_checkin,
                 "nextCheckIn": jungle.next_checkin,
                 "nextCheckInLatest": jungle.next_checkin_latest,
+                "activeIncident": None,
             },
             {
                 "name": prod_env,
@@ -78,6 +81,7 @@ class BaseMonitorDetailsTest(MonitorTestCase):
                 "lastCheckIn": prod.last_checkin,
                 "nextCheckIn": prod.next_checkin,
                 "nextCheckInLatest": prod.next_checkin_latest,
+                "activeIncident": None,
             },
         ]
 
@@ -93,6 +97,7 @@ class BaseMonitorDetailsTest(MonitorTestCase):
                 "lastCheckIn": prod.last_checkin,
                 "nextCheckIn": prod.next_checkin,
                 "nextCheckInLatest": prod.next_checkin_latest,
+                "activeIncident": None,
             }
         ]
 
@@ -108,6 +113,51 @@ class BaseMonitorDetailsTest(MonitorTestCase):
         assert issue_alert_rule is not None
         assert issue_alert_rule["environment"] is not None
 
+    def test_with_active_incident_and_detection(self):
+        monitor = self._create_monitor()
+        monitor_env = self._create_monitor_environment(monitor)
+
+        resp = self.get_success_response(self.organization.slug, monitor.slug)
+        assert resp.data["environments"][0]["activeIncident"] is None
+
+        starting_timestamp = datetime(2023, 12, 15, tzinfo=timezone.utc)
+        monitor_incident = MonitorIncident.objects.create(
+            monitor=monitor, monitor_environment=monitor_env, starting_timestamp=starting_timestamp
+        )
+        detection_timestamp = datetime(2024, 1, 1, tzinfo=timezone.utc)
+        MonitorEnvBrokenDetection.objects.create(
+            monitor_incident=monitor_incident, user_notified_timestamp=detection_timestamp
+        )
+
+        resp = self.get_success_response(self.organization.slug, monitor.slug)
+        assert resp.data["environments"][0]["activeIncident"] == {
+            "startingTimestamp": monitor_incident.starting_timestamp,
+            "resolvingTimestamp": monitor_incident.resolving_timestamp,
+            "brokenNotice": {
+                "userNotifiedTimestamp": detection_timestamp,
+                "environmentMutedTimestamp": None,
+            },
+        }
+
+    def test_with_active_incident_no_detection(self):
+        monitor = self._create_monitor()
+        monitor_env = self._create_monitor_environment(monitor)
+
+        resp = self.get_success_response(self.organization.slug, monitor.slug)
+        assert resp.data["environments"][0]["activeIncident"] is None
+
+        starting_timestamp = datetime(2023, 12, 15, tzinfo=timezone.utc)
+        monitor_incident = MonitorIncident.objects.create(
+            monitor=monitor, monitor_environment=monitor_env, starting_timestamp=starting_timestamp
+        )
+
+        resp = self.get_success_response(self.organization.slug, monitor.slug)
+        assert resp.data["environments"][0]["activeIncident"] == {
+            "startingTimestamp": monitor_incident.starting_timestamp,
+            "resolvingTimestamp": monitor_incident.resolving_timestamp,
+            "brokenNotice": None,
+        }
+
 
 @freeze_time()
 class BaseUpdateMonitorTest(MonitorTestCase):