Skip to content

Commit 37c3ee8

Browse files
authored
feat: Add OpenAPI docs for monitor endpoints (#42850)
Refs #42283
1 parent ccb5b57 commit 37c3ee8

File tree

12 files changed

+261
-39
lines changed

12 files changed

+261
-39
lines changed

src/sentry/api/endpoints/monitor_checkin_details.py

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
from django.db import transaction
22
from django.utils import timezone
3-
from rest_framework import serializers
3+
from drf_spectacular.utils import extend_schema
44
from rest_framework.request import Request
55
from rest_framework.response import Response
66

77
from sentry.api.authentication import DSNAuthentication
88
from sentry.api.base import Endpoint, region_silo_endpoint
99
from sentry.api.bases.monitor import MonitorEndpoint, ProjectMonitorPermission
1010
from sentry.api.exceptions import ResourceDoesNotExist
11-
from sentry.api.fields.empty_integer import EmptyIntegerField
1211
from sentry.api.serializers import serialize
12+
from sentry.api.serializers.models.monitorcheckin import MonitorCheckInSerializerResponse
13+
from sentry.api.validators import MonitorCheckInValidator
14+
from sentry.apidocs.constants import (
15+
RESPONSE_ALREADY_REPORTED,
16+
RESPONSE_BAD_REQUEST,
17+
RESPONSE_FORBIDDEN,
18+
RESPONSE_NOTFOUND,
19+
RESPONSE_UNAUTHORIZED,
20+
)
21+
from sentry.apidocs.parameters import GLOBAL_PARAMS, MONITOR_PARAMS
22+
from sentry.apidocs.utils import inline_sentry_response_serializer
1323
from sentry.models import (
1424
CheckInStatus,
1525
Monitor,
@@ -22,21 +32,12 @@
2232
from sentry.utils.sdk import bind_organization_context, configure_scope
2333

2434

25-
class CheckInSerializer(serializers.Serializer):
26-
status = serializers.ChoiceField(
27-
choices=(
28-
("ok", CheckInStatus.OK),
29-
("error", CheckInStatus.ERROR),
30-
("in_progress", CheckInStatus.IN_PROGRESS),
31-
)
32-
)
33-
duration = EmptyIntegerField(required=False, allow_null=True)
34-
35-
3635
@region_silo_endpoint
36+
@extend_schema(tags=["Crons"])
3737
class MonitorCheckInDetailsEndpoint(Endpoint):
3838
authentication_classes = MonitorEndpoint.authentication_classes + (DSNAuthentication,)
3939
permission_classes = (ProjectMonitorPermission,)
40+
public = {"GET", "PUT"}
4041

4142
# TODO(dcramer): this code needs shared with other endpoints as its security focused
4243
# TODO(dcramer): this doesnt handle is_global roles
@@ -82,6 +83,23 @@ def convert_args(self, request: Request, monitor_id, checkin_id, *args, **kwargs
8283
kwargs.update({"checkin": checkin, "monitor": monitor, "project": project})
8384
return (args, kwargs)
8485

86+
@extend_schema(
87+
operation_id="Retrieve a check-in",
88+
parameters=[
89+
GLOBAL_PARAMS.ORG_SLUG,
90+
MONITOR_PARAMS.MONITOR_ID,
91+
MONITOR_PARAMS.CHECKIN_ID,
92+
],
93+
request=None,
94+
responses={
95+
201: inline_sentry_response_serializer(
96+
"MonitorCheckIn", MonitorCheckInSerializerResponse
97+
),
98+
401: RESPONSE_UNAUTHORIZED,
99+
403: RESPONSE_FORBIDDEN,
100+
404: RESPONSE_NOTFOUND,
101+
},
102+
)
85103
def get(self, request: Request, project, monitor, checkin) -> Response:
86104
"""
87105
Retrieve a check-in
@@ -100,6 +118,28 @@ def get(self, request: Request, project, monitor, checkin) -> Response:
100118

101119
return self.respond(serialize(checkin, request.user))
102120

121+
@extend_schema(
122+
operation_id="Update a check-in",
123+
parameters=[
124+
GLOBAL_PARAMS.ORG_SLUG,
125+
MONITOR_PARAMS.MONITOR_ID,
126+
MONITOR_PARAMS.CHECKIN_ID,
127+
],
128+
request=MonitorCheckInValidator,
129+
responses={
130+
200: inline_sentry_response_serializer(
131+
"MonitorCheckIn2", MonitorCheckInSerializerResponse
132+
),
133+
208: RESPONSE_ALREADY_REPORTED,
134+
400: RESPONSE_BAD_REQUEST,
135+
401: RESPONSE_UNAUTHORIZED,
136+
403: RESPONSE_FORBIDDEN,
137+
404: RESPONSE_NOTFOUND,
138+
},
139+
examples=[
140+
# OpenApiExample()
141+
],
142+
)
103143
def put(self, request: Request, project, monitor, checkin) -> Response:
104144
"""
105145
Update a check-in
@@ -115,7 +155,7 @@ def put(self, request: Request, project, monitor, checkin) -> Response:
115155
if checkin.status in CheckInStatus.FINISHED_VALUES:
116156
return self.respond(status=400)
117157

118-
serializer = CheckInSerializer(
158+
serializer = MonitorCheckInValidator(
119159
data=request.data, partial=True, context={"project": project, "request": request}
120160
)
121161
if not serializer.is_valid():

src/sentry/api/endpoints/monitor_checkins.py

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,58 @@
11
from __future__ import annotations
22

3+
from typing import List
4+
35
from django.db import transaction
4-
from rest_framework import serializers
6+
from drf_spectacular.utils import extend_schema
57
from rest_framework.request import Request
68
from rest_framework.response import Response
79

810
from sentry.api.authentication import DSNAuthentication
911
from sentry.api.base import region_silo_endpoint
1012
from sentry.api.bases.monitor import MonitorEndpoint
11-
from sentry.api.fields.empty_integer import EmptyIntegerField
1213
from sentry.api.paginator import OffsetPaginator
1314
from sentry.api.serializers import serialize
15+
from sentry.api.serializers.models.monitorcheckin import MonitorCheckInSerializerResponse
16+
from sentry.api.validators import MonitorCheckInValidator
17+
from sentry.apidocs.constants import (
18+
RESPONSE_BAD_REQUEST,
19+
RESPONSE_FORBIDDEN,
20+
RESPONSE_NOTFOUND,
21+
RESPONSE_UNAUTHORIZED,
22+
)
23+
from sentry.apidocs.parameters import GLOBAL_PARAMS, MONITOR_PARAMS
24+
from sentry.apidocs.utils import inline_sentry_response_serializer
1425
from sentry.models import CheckInStatus, Monitor, MonitorCheckIn, MonitorStatus, ProjectKey
1526

1627

17-
class CheckInSerializer(serializers.Serializer):
18-
status = serializers.ChoiceField(
19-
choices=(
20-
("ok", CheckInStatus.OK),
21-
("error", CheckInStatus.ERROR),
22-
("in_progress", CheckInStatus.IN_PROGRESS),
23-
)
24-
)
25-
duration = EmptyIntegerField(required=False, allow_null=True)
26-
27-
2828
@region_silo_endpoint
29+
@extend_schema(tags=["Crons"])
2930
class MonitorCheckInsEndpoint(MonitorEndpoint):
3031
authentication_classes = MonitorEndpoint.authentication_classes + (DSNAuthentication,)
31-
32+
public = {"GET", "POST"}
33+
34+
@extend_schema(
35+
operation_id="Retrieve check-ins for a monitor",
36+
parameters=[
37+
GLOBAL_PARAMS.ORG_SLUG,
38+
MONITOR_PARAMS.MONITOR_ID,
39+
MONITOR_PARAMS.CHECKIN_ID,
40+
],
41+
responses={
42+
200: inline_sentry_response_serializer(
43+
"CheckInList", List[MonitorCheckInSerializerResponse]
44+
),
45+
401: RESPONSE_UNAUTHORIZED,
46+
403: RESPONSE_FORBIDDEN,
47+
404: RESPONSE_NOTFOUND,
48+
},
49+
)
3250
def get(
3351
self, request: Request, project, monitor, organization_slug: str | None = None
3452
) -> Response:
3553
"""
36-
Retrieve check-ins for an monitor
37-
`````````````````````````````````
54+
Retrieve check-ins for a monitor
55+
````````````````````````````````
3856
3957
:pparam string monitor_id: the id of the monitor.
4058
:auth: required
@@ -57,6 +75,27 @@ def get(
5775
paginator_cls=OffsetPaginator,
5876
)
5977

78+
@extend_schema(
79+
operation_id="Create a new check-in",
80+
parameters=[
81+
GLOBAL_PARAMS.ORG_SLUG,
82+
MONITOR_PARAMS.MONITOR_ID,
83+
MONITOR_PARAMS.CHECKIN_ID,
84+
],
85+
request=MonitorCheckInValidator,
86+
responses={
87+
200: inline_sentry_response_serializer(
88+
"MonitorCheckIn", MonitorCheckInSerializerResponse
89+
),
90+
201: inline_sentry_response_serializer(
91+
"MonitorCheckIn", MonitorCheckInSerializerResponse
92+
),
93+
400: RESPONSE_BAD_REQUEST,
94+
401: RESPONSE_UNAUTHORIZED,
95+
403: RESPONSE_FORBIDDEN,
96+
404: RESPONSE_NOTFOUND,
97+
},
98+
)
6099
def post(
61100
self, request: Request, project, monitor, organization_slug: str | None = None
62101
) -> Response:
@@ -66,6 +105,8 @@ def post(
66105
67106
:pparam string monitor_id: the id of the monitor.
68107
:auth: required
108+
109+
Note: If a DSN is utilized for authentication, the response will be limited in details.
69110
"""
70111
if organization_slug:
71112
if project.organization.slug != organization_slug:
@@ -74,7 +115,7 @@ def post(
74115
if monitor.status in [MonitorStatus.PENDING_DELETION, MonitorStatus.DELETION_IN_PROGRESS]:
75116
return self.respond(status=404)
76117

77-
serializer = CheckInSerializer(
118+
serializer = MonitorCheckInValidator(
78119
data=request.data, context={"project": project, "request": request}
79120
)
80121
if not serializer.is_valid():

src/sentry/api/endpoints/monitor_details.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,45 @@
11
from __future__ import annotations
22

33
from django.db import transaction
4+
from drf_spectacular.utils import extend_schema
45
from rest_framework.request import Request
56
from rest_framework.response import Response
67

78
from sentry import audit_log
89
from sentry.api.base import region_silo_endpoint
910
from sentry.api.bases.monitor import MonitorEndpoint
1011
from sentry.api.serializers import serialize
12+
from sentry.api.serializers.models.monitor import MonitorSerializerResponse
1113
from sentry.api.validators import MonitorValidator
14+
from sentry.apidocs.constants import (
15+
RESPONSE_ACCEPTED,
16+
RESPONSE_FORBIDDEN,
17+
RESPONSE_NOTFOUND,
18+
RESPONSE_UNAUTHORIZED,
19+
)
20+
from sentry.apidocs.parameters import GLOBAL_PARAMS, MONITOR_PARAMS
21+
from sentry.apidocs.utils import inline_sentry_response_serializer
1222
from sentry.models import Monitor, MonitorStatus, ScheduledDeletion
1323

1424

1525
@region_silo_endpoint
26+
@extend_schema(tags=["Crons"])
1627
class MonitorDetailsEndpoint(MonitorEndpoint):
28+
public = {"GET", "PUT", "DELETE"}
29+
30+
@extend_schema(
31+
operation_id="Retrieve a monitor",
32+
parameters=[
33+
GLOBAL_PARAMS.ORG_SLUG,
34+
MONITOR_PARAMS.MONITOR_ID,
35+
],
36+
responses={
37+
200: inline_sentry_response_serializer("Monitor", MonitorSerializerResponse),
38+
401: RESPONSE_UNAUTHORIZED,
39+
403: RESPONSE_FORBIDDEN,
40+
404: RESPONSE_NOTFOUND,
41+
},
42+
)
1743
def get(
1844
self, request: Request, project, monitor, organization_slug: str | None = None
1945
) -> Response:
@@ -30,6 +56,20 @@ def get(
3056

3157
return self.respond(serialize(monitor, request.user))
3258

59+
@extend_schema(
60+
operation_id="Update a monitor",
61+
parameters=[
62+
GLOBAL_PARAMS.ORG_SLUG,
63+
MONITOR_PARAMS.MONITOR_ID,
64+
],
65+
request=MonitorValidator,
66+
responses={
67+
200: inline_sentry_response_serializer("Monitor", MonitorSerializerResponse),
68+
401: RESPONSE_UNAUTHORIZED,
69+
403: RESPONSE_FORBIDDEN,
70+
404: RESPONSE_NOTFOUND,
71+
},
72+
)
3373
def put(
3474
self, request: Request, project, monitor, organization_slug: str | None = None
3575
) -> Response:
@@ -87,6 +127,20 @@ def put(
87127

88128
return self.respond(serialize(monitor, request.user))
89129

130+
@extend_schema(
131+
operation_id="Delete a monitor",
132+
parameters=[
133+
GLOBAL_PARAMS.ORG_SLUG,
134+
MONITOR_PARAMS.MONITOR_ID,
135+
],
136+
request=MonitorValidator,
137+
responses={
138+
202: RESPONSE_ACCEPTED,
139+
401: RESPONSE_UNAUTHORIZED,
140+
403: RESPONSE_FORBIDDEN,
141+
404: RESPONSE_NOTFOUND,
142+
},
143+
)
90144
def delete(
91145
self, request: Request, project, monitor, organization_slug: str | None = None
92146
) -> Response:

src/sentry/api/serializers/models/monitor.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
from datetime import datetime
2+
from typing import Any
3+
4+
from typing_extensions import TypedDict
5+
16
from sentry.api.serializers import Serializer, register, serialize
27
from sentry.models import Monitor, Project
38

9+
from .project import ProjectSerializerResponse
10+
411

512
@register(Monitor)
613
class MonitorSerializer(Serializer):
@@ -33,3 +40,15 @@ def serialize(self, obj, attrs, user):
3340
"dateCreated": obj.date_added,
3441
"project": attrs["project"],
3542
}
43+
44+
45+
class MonitorSerializerResponse(TypedDict):
46+
id: str
47+
name: str
48+
status: str
49+
type: str
50+
config: Any
51+
dateCreated: datetime
52+
lastCheckIn: datetime
53+
nextCheckIn: datetime
54+
project: ProjectSerializerResponse

src/sentry/api/serializers/models/monitorcheckin.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from datetime import datetime
2+
3+
from typing_extensions import TypedDict
4+
15
from sentry.api.serializers import Serializer, register
26
from sentry.models import MonitorCheckIn
37

@@ -11,3 +15,10 @@ def serialize(self, obj, attrs, user):
1115
"duration": obj.duration,
1216
"dateCreated": obj.date_added,
1317
}
18+
19+
20+
class MonitorCheckInSerializerResponse(TypedDict):
21+
id: str
22+
status: str
23+
duration: int
24+
dateCreated: datetime

src/sentry/api/serializers/rest_framework/project.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
from drf_spectacular.utils import extend_schema_field
12
from rest_framework import serializers
23

34
from sentry.models import Project
45

56
ValidationError = serializers.ValidationError
67

78

9+
@extend_schema_field(str)
810
class ProjectField(serializers.Field):
911
def __init__(self, scope="project:write"):
1012
self.scope = scope

0 commit comments

Comments
 (0)