From 40531424989fe55d9b206503a3484294d8567d62 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 2 Nov 2026 16:27:26 -0500 Subject: [PATCH] fix(apidocs): Correct schema declarations on unpublished endpoints These methods declare responses or request bodies that drf-spectacular can't describe correctly. The published build skips them, being unpublished, but a build that describes every method gets an invalid spec from the csrf endpoint, whose responses are keyed by field names, and wrong or untyped shapes from the rest: a request body sharing another's component name, a response type that isn't a TypedDict, a bare object response, and a TypedDict passed where an inline response serializer belongs. --- src/sentry/auth_v2/endpoints/csrf.py | 24 +++++++++---------- src/sentry/core/endpoints/scim/members.py | 2 +- src/sentry/monitors/serializers.py | 2 +- .../organization_detector_anomaly_data.py | 11 ++++++++- .../organization_test_fire_action.py | 5 +++- 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/sentry/auth_v2/endpoints/csrf.py b/src/sentry/auth_v2/endpoints/csrf.py index 2ec3b72689fd..1d1ef8c4a485 100644 --- a/src/sentry/auth_v2/endpoints/csrf.py +++ b/src/sentry/auth_v2/endpoints/csrf.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, TypedDict from django.middleware.csrf import rotate_token from django.utils.decorators import method_decorator @@ -13,11 +13,17 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint -from sentry.auth_v2.utils.session import SessionSerializer +from sentry.apidocs.utils import inline_sentry_response_serializer +from sentry.auth_v2.utils.session import SessionSerializer, SessionSerializerResponse from sentry.ratelimits.config import RateLimitConfig from sentry.types.ratelimit import RateLimit, RateLimitCategory +class CsrfTokenResponse(TypedDict): + detail: str + session: SessionSerializerResponse + + @control_silo_endpoint class CsrfTokenEndpoint(Endpoint): """ @@ -48,13 +54,10 @@ class CsrfTokenEndpoint(Endpoint): @extend_schema( operation_id="Retrieve the CSRF token in your session", parameters=[], - responses={ - "detail": "string", - "session": SessionSerializer, - }, + responses={200: inline_sentry_response_serializer("CsrfTokenResponse", CsrfTokenResponse)}, ) @method_decorator(ensure_csrf_cookie) - def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: + def get(self, request: Request, *args: Any, **kwargs: Any) -> Response[CsrfTokenResponse]: return self.respond( { "detail": "Set CSRF cookie", @@ -66,13 +69,10 @@ def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: @extend_schema( operation_id="Rotate the CSRF token in your session", parameters=[], - responses={ - "detail": "string", - "session": SessionSerializer, - }, + responses={200: inline_sentry_response_serializer("CsrfTokenResponse", CsrfTokenResponse)}, ) @method_decorator(ensure_csrf_cookie) - def put(self, request: Request, *args: Any, **kwargs: Any) -> Response: + def put(self, request: Request, *args: Any, **kwargs: Any) -> Response[CsrfTokenResponse]: rotate_token(request) if referrer := request.GET.get("referrer"): analytics.record( diff --git a/src/sentry/core/endpoints/scim/members.py b/src/sentry/core/endpoints/scim/members.py index 774e380f017a..7bc9753102f7 100644 --- a/src/sentry/core/endpoints/scim/members.py +++ b/src/sentry/core/endpoints/scim/members.py @@ -384,7 +384,7 @@ def delete( GlobalParams.member_id("The ID of the member to update."), ], request=inline_serializer( - "SCIMMemberProvision", fields={"sentryOrgRole": serializers.CharField()} + "SCIMMemberReplace", fields={"sentryOrgRole": serializers.CharField()} ), responses={ 201: OrganizationMemberSCIMSerializer, diff --git a/src/sentry/monitors/serializers.py b/src/sentry/monitors/serializers.py index 6c65f210ff6d..f864df901138 100644 --- a/src/sentry/monitors/serializers.py +++ b/src/sentry/monitors/serializers.py @@ -176,7 +176,7 @@ class MonitorSerializerResponse(TypedDict): owner: ActorSerializerResponse -class MonitorBulkEditResponse: +class MonitorBulkEditResponse(TypedDict): updated: list[MonitorSerializerResponse] errored: list[MonitorSerializerResponse] diff --git a/src/sentry/workflow_engine/endpoints/organization_detector_anomaly_data.py b/src/sentry/workflow_engine/endpoints/organization_detector_anomaly_data.py index 7aaba16124fe..12bf084b414b 100644 --- a/src/sentry/workflow_engine/endpoints/organization_detector_anomaly_data.py +++ b/src/sentry/workflow_engine/endpoints/organization_detector_anomaly_data.py @@ -1,4 +1,5 @@ import logging +from typing import TypedDict from drf_spectacular.utils import extend_schema from rest_framework.exceptions import PermissionDenied @@ -13,8 +14,10 @@ from sentry.api.utils import to_valid_int_id from sentry.apidocs.constants import RESPONSE_FORBIDDEN, RESPONSE_NOT_FOUND, RESPONSE_UNAUTHORIZED from sentry.apidocs.parameters import DetectorParams, GlobalParams +from sentry.apidocs.utils import inline_sentry_response_serializer from sentry.models.organization import Organization from sentry.seer.anomaly_detection.get_anomaly_data import get_anomaly_threshold_data_from_seer +from sentry.seer.anomaly_detection.types import AnomalyThresholdDataPoint from sentry.snuba.models import QuerySubscription from sentry.workflow_engine.models import Detector @@ -27,6 +30,10 @@ class SubscriptionNotFound(Exception): pass +class AnomalyThresholdDataResponse(TypedDict): + data: list[AnomalyThresholdDataPoint] + + @extend_schema(tags=["Workflows"]) @cell_silo_endpoint class OrganizationDetectorAnomalyDataEndpoint(OrganizationEndpoint): @@ -62,7 +69,9 @@ def _get_subscription_from_detector( operation_id="Retrieve Anomaly Detection Threshold Data for a Detector", parameters=[GlobalParams.ORG_ID_OR_SLUG, DetectorParams.DETECTOR_ID], responses={ - 200: object, + 200: inline_sentry_response_serializer( + "AnomalyThresholdDataResponse", AnomalyThresholdDataResponse + ), 401: RESPONSE_UNAUTHORIZED, 403: RESPONSE_FORBIDDEN, 404: RESPONSE_NOT_FOUND, diff --git a/src/sentry/workflow_engine/endpoints/organization_test_fire_action.py b/src/sentry/workflow_engine/endpoints/organization_test_fire_action.py index e7fde41aae08..1a154c6ad2f4 100644 --- a/src/sentry/workflow_engine/endpoints/organization_test_fire_action.py +++ b/src/sentry/workflow_engine/endpoints/organization_test_fire_action.py @@ -14,6 +14,7 @@ from sentry.api.serializers.rest_framework import CamelSnakeSerializer from sentry.apidocs.constants import RESPONSE_NOT_FOUND, RESPONSE_UNAUTHORIZED from sentry.apidocs.parameters import GlobalParams +from sentry.apidocs.utils import inline_sentry_response_serializer from sentry.constants import ObjectStatus from sentry.issues.action_log import ( action_context_scope, @@ -81,7 +82,9 @@ class OrganizationTestFireActionsEndpoint(OrganizationEndpoint): ], responses={ 200: None, - 400: TestFireActionErrorsResponse, + 400: inline_sentry_response_serializer( + "TestFireActionErrorsResponse", TestFireActionErrorsResponse + ), 401: RESPONSE_UNAUTHORIZED, 404: RESPONSE_NOT_FOUND, },