Skip to content

Commit 9142820

Browse files
authored
feat(uptime): Add serializer for uptime monitors and include them in the combined alert serializer (#74612)
This adds a serializer for uptime monitors, and also adds them to the combined alert serializer so that we can include them in the combined alerts endpoint results. Also adds in a feature flag for gating access to showing these in the api <!-- Describe your PR here. -->
1 parent d277417 commit 9142820

File tree

12 files changed

+171
-12
lines changed

12 files changed

+171
-12
lines changed

src/sentry/features/temporary.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,8 @@ def register_temporary_features(manager: FeatureManager):
467467
manager.add("organizations:uptime-automatic-hostname-detection", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE)
468468
# Enables automatic subscription creation in uptime
469469
manager.add("organizations:uptime-automatic-subscription-creation", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE)
470+
# Enabled returning uptime monitors from the rule api
471+
manager.add("organizations:uptime-rule-api", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE)
470472
# Enables uptime related settings for projects and orgs
471473
manager.add('organizations:uptime-settings', OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE)
472474
manager.add("organizations:use-metrics-layer", OrganizationFeature, FeatureHandlerStrategy.REMOTE)

src/sentry/incidents/endpoints/serializers/alert_rule.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from sentry.sentry_apps.services.app import app_service
3131
from sentry.sentry_apps.services.app.model import RpcSentryAppComponentContext
3232
from sentry.snuba.models import SnubaQueryEventType
33+
from sentry.uptime.models import ProjectUptimeSubscription
3334
from sentry.users.services.user import RpcUser
3435
from sentry.users.services.user.service import user_service
3536

@@ -395,6 +396,14 @@ def get_attrs(
395396
serialized_rule["id"]: serialized_rule for serialized_rule in serialized_issue_rules
396397
}
397398

399+
serialized_uptime_monitors = serialize(
400+
[x for x in item_list if isinstance(x, ProjectUptimeSubscription)],
401+
user=user,
402+
)
403+
serialized_uptime_monitor_map_by_id = {
404+
item["id"]: item for item in serialized_uptime_monitors
405+
}
406+
398407
for item in item_list:
399408
item_id = str(item.id)
400409
if item_id in serialized_alert_rule_map_by_id:
@@ -418,6 +427,9 @@ def get_attrs(
418427
elif item_id in serialized_issue_rule_map_by_id:
419428
# This is an issue alert rule
420429
results[item] = serialized_issue_rule_map_by_id[item_id]
430+
elif item_id in serialized_uptime_monitor_map_by_id:
431+
# This is an uptime monitor
432+
results[item] = serialized_uptime_monitor_map_by_id[item_id]
421433
else:
422434
logger.error(
423435
"Alert Rule found but dropped during serialization",
@@ -432,18 +444,18 @@ def get_attrs(
432444

433445
def serialize(
434446
self,
435-
obj: Rule | AlertRule,
447+
obj: Rule | AlertRule | ProjectUptimeSubscription,
436448
attrs: Mapping[Any, Any],
437449
user: User | RpcUser,
438450
**kwargs: Any,
439451
) -> MutableMapping[Any, Any]:
452+
updated_attrs = {**attrs}
440453
if isinstance(obj, AlertRule):
441-
alert_rule_attrs: MutableMapping[Any, Any] = {**attrs}
442-
alert_rule_attrs["type"] = "alert_rule"
443-
return alert_rule_attrs
454+
updated_attrs["type"] = "alert_rule"
444455
elif isinstance(obj, Rule):
445-
rule_attrs: MutableMapping[Any, Any] = {**attrs}
446-
rule_attrs["type"] = "rule"
447-
return rule_attrs
456+
updated_attrs["type"] = "rule"
457+
elif isinstance(obj, ProjectUptimeSubscription):
458+
updated_attrs["type"] = "uptime"
448459
else:
449460
raise AssertionError(f"Invalid rule to serialize: {type(obj)}")
461+
return updated_attrs

src/sentry/testutils/factories.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,13 @@
152152
from sentry.testutils.outbox import outbox_runner
153153
from sentry.testutils.silo import assume_test_silo_mode
154154
from sentry.types.activity import ActivityType
155+
from sentry.types.actor import Actor
155156
from sentry.types.region import Region, get_local_region, get_region_by_name
156157
from sentry.types.token import AuthTokenType
157158
from sentry.uptime.models import (
158159
ProjectUptimeSubscription,
159160
ProjectUptimeSubscriptionMode,
161+
UptimeStatus,
160162
UptimeSubscription,
161163
)
162164
from sentry.users.services.user import RpcUser
@@ -1950,9 +1952,24 @@ def create_project_uptime_subscription(
19501952
project: Project,
19511953
uptime_subscription: UptimeSubscription,
19521954
mode: ProjectUptimeSubscriptionMode,
1955+
name: str,
1956+
owner: Actor | None,
1957+
uptime_status: UptimeStatus,
19531958
):
1959+
owner_team_id = None
1960+
owner_user_id = None
1961+
if owner:
1962+
if owner.is_team:
1963+
owner_team_id = owner.id
1964+
elif owner.is_user:
1965+
owner_user_id = owner.id
1966+
19541967
return ProjectUptimeSubscription.objects.create(
19551968
uptime_subscription=uptime_subscription,
19561969
project=project,
19571970
mode=mode,
1971+
name=name,
1972+
owner_team_id=owner_team_id,
1973+
owner_user_id=owner_user_id,
1974+
uptime_status=uptime_status,
19581975
)

src/sentry/testutils/fixtures.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from sentry.models.project import Project
2323
from sentry.models.projecttemplate import ProjectTemplate
2424
from sentry.models.rule import Rule
25+
from sentry.models.team import Team
2526
from sentry.models.user import User
2627
from sentry.monitors.models import Monitor, MonitorType, ScheduleType
2728
from sentry.organizations.services.organization import RpcOrganization
@@ -34,9 +35,11 @@
3435
# all of the memoized fixtures are copypasta due to our inability to use pytest fixtures
3536
# on a per-class method basis
3637
from sentry.types.activity import ActivityType
38+
from sentry.types.actor import Actor
3739
from sentry.uptime.models import (
3840
ProjectUptimeSubscription,
3941
ProjectUptimeSubscriptionMode,
42+
UptimeStatus,
4043
UptimeSubscription,
4144
)
4245
from sentry.users.services.user import RpcUser
@@ -647,13 +650,23 @@ def create_project_uptime_subscription(
647650
project: Project | None = None,
648651
uptime_subscription: UptimeSubscription | None = None,
649652
mode=ProjectUptimeSubscriptionMode.AUTO_DETECTED_ACTIVE,
653+
name="Test Name",
654+
owner: User | Team | None = None,
655+
uptime_status=UptimeStatus.OK,
650656
) -> ProjectUptimeSubscription:
651657
if project is None:
652658
project = self.project
653659

654660
if uptime_subscription is None:
655661
uptime_subscription = self.create_uptime_subscription()
656-
return Factories.create_project_uptime_subscription(project, uptime_subscription, mode)
662+
return Factories.create_project_uptime_subscription(
663+
project,
664+
uptime_subscription,
665+
mode,
666+
name,
667+
Actor.from_object(owner) if owner else None,
668+
uptime_status,
669+
)
657670

658671
@pytest.fixture(autouse=True)
659672
def _init_insta_snapshot(self, insta_snapshot):

src/sentry/types/actor.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ class InvalidActor(ObjectDoesNotExist):
4040
pass
4141

4242
@classmethod
43-
def resolve_many(cls, actors: Sequence["Actor"]) -> list["Team | RpcUser"]:
43+
def resolve_many(
44+
cls, actors: Sequence["Actor"], filter_none: bool = True
45+
) -> list["Team | RpcUser | None"]:
4446
"""
4547
Resolve a list of actors in a batch to the Team/User the Actor references.
4648
@@ -64,7 +66,10 @@ def resolve_many(cls, actors: Sequence["Actor"]) -> list["Team | RpcUser"]:
6466
for team in Team.objects.filter(id__in=[t.id for t in actor_list]):
6567
results[(actor_type, team.id)] = team
6668

67-
return list(filter(None, [results.get((actor.actor_type, actor.id)) for actor in actors]))
69+
final_results = [results.get((actor.actor_type, actor.id)) for actor in actors]
70+
if filter_none:
71+
final_results = list(filter(None, final_results))
72+
return final_results
6873

6974
@classmethod
7075
def many_from_object(cls, objects: Iterable[ActorTarget]) -> list["Actor"]:

src/sentry/uptime/apps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ class Config(AppConfig):
55
name = "sentry.uptime"
66

77
def ready(self):
8-
pass
8+
from sentry.uptime.endpoints import serializers # NOQA

src/sentry/uptime/endpoints/__init__.py

Whitespace-only changes.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from collections.abc import MutableMapping, Sequence
2+
from typing import Any, TypedDict
3+
4+
from django.db.models import prefetch_related_objects
5+
6+
from sentry.api.serializers import Serializer, register, serialize
7+
from sentry.api.serializers.models.actor import ActorSerializer, ActorSerializerResponse
8+
from sentry.types.actor import Actor
9+
from sentry.uptime.models import ProjectUptimeSubscription
10+
11+
12+
class ProjectUptimeSubscriptionSerializerResponse(TypedDict):
13+
id: str
14+
projectSlug: str
15+
name: str
16+
status: int
17+
mode: int
18+
url: str
19+
intervalSeconds: int
20+
timeoutMs: int
21+
owner: ActorSerializerResponse
22+
23+
24+
@register(ProjectUptimeSubscription)
25+
class ProjectUptimeSubscriptionSerializer(Serializer):
26+
def __init__(self, expand=None):
27+
self.expand = expand
28+
29+
def get_attrs(
30+
self, item_list: Sequence[ProjectUptimeSubscription], user: Any, **kwargs: Any
31+
) -> MutableMapping[Any, Any]:
32+
prefetch_related_objects(item_list, "uptime_subscription", "project")
33+
owners = list(filter(None, [item.owner for item in item_list]))
34+
owners_serialized = serialize(
35+
Actor.resolve_many(owners, filter_none=False), user, ActorSerializer()
36+
)
37+
serialized_owner_lookup = {
38+
owner: serialized_owner for owner, serialized_owner in zip(owners, owners_serialized)
39+
}
40+
41+
return {
42+
item: {"owner": serialized_owner_lookup.get(item.owner) if item.owner else None}
43+
for item in item_list
44+
}
45+
46+
def serialize(
47+
self, obj: ProjectUptimeSubscription, attrs, user, **kwargs
48+
) -> ProjectUptimeSubscriptionSerializerResponse:
49+
return {
50+
"id": str(obj.id),
51+
"projectSlug": obj.project.slug,
52+
"name": obj.name,
53+
"status": obj.uptime_status,
54+
"mode": obj.mode,
55+
"url": obj.uptime_subscription.url,
56+
"intervalSeconds": obj.uptime_subscription.interval_seconds,
57+
"timeoutMs": obj.uptime_subscription.timeout_ms,
58+
"owner": attrs["owner"],
59+
}

src/sentry/uptime/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey
1212
from sentry.db.models.manager.base import BaseManager
1313
from sentry.remote_subscriptions.models import BaseRemoteSubscription
14+
from sentry.types.actor import Actor
1415

1516

1617
@region_silo_model
@@ -104,3 +105,7 @@ class Meta:
104105
),
105106
),
106107
]
108+
109+
@property
110+
def owner(self) -> Actor | None:
111+
return Actor.from_id(user_id=self.owner_user_id, team_id=self.owner_team_id)

tests/sentry/incidents/endpoints/serializers/test_alert_rule.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,16 +326,21 @@ def test_combined_serializer(self):
326326
}
327327
)
328328
other_alert_rule = self.create_alert_rule()
329+
uptime_monitor = self.create_project_uptime_subscription()
329330

330331
result = serialize(
331-
[alert_rule, issue_rule, other_alert_rule], serializer=CombinedRuleSerializer()
332+
[alert_rule, issue_rule, other_alert_rule, uptime_monitor],
333+
serializer=CombinedRuleSerializer(),
332334
)
333335

334336
self.assert_alert_rule_serialized(alert_rule, result[0])
335337
assert result[1]["id"] == str(issue_rule.id)
336338
assert result[1]["status"] == "active"
337339
assert not result[1]["snooze"]
338340
self.assert_alert_rule_serialized(other_alert_rule, result[2])
341+
serialized_uptime_monitor = serialize(uptime_monitor)
342+
serialized_uptime_monitor["type"] = "uptime"
343+
assert result[3] == serialized_uptime_monitor
339344

340345
def test_alert_snoozed(self):
341346
projects = [self.project, self.create_project()]

0 commit comments

Comments
 (0)