diff --git a/src/sentry/api/base.py b/src/sentry/api/base.py index e7e66d1d602ed8..cb735c79ea7f4a 100644 --- a/src/sentry/api/base.py +++ b/src/sentry/api/base.py @@ -227,6 +227,7 @@ class Endpoint(APIView): publish_status: dict[HTTP_METHOD_NAME, ApiPublishStatus] = {} rate_limits: RateLimitConfig = DEFAULT_RATE_LIMIT_CONFIG enforce_rate_limit: bool = settings.SENTRY_RATELIMITER_ENABLED + servers: list[dict[str, Any]] | None = None def build_cursor_link(self, request: HttpRequest, name: str, cursor: Cursor) -> str: if request.GET.get("cursor") is None: diff --git a/src/sentry/api/endpoints/seer_models.py b/src/sentry/api/endpoints/seer_models.py index 06cca95dc2722b..de9cca17e784df 100644 --- a/src/sentry/api/endpoints/seer_models.py +++ b/src/sentry/api/endpoints/seer_models.py @@ -50,6 +50,7 @@ class SeerModelsEndpoint(Endpoint): } owner = ApiOwner.ML_AI permission_classes = () + servers = [{"url": "https://{region}.sentry.io"}] enforce_rate_limit = True rate_limits = RateLimitConfig( @@ -72,6 +73,9 @@ def get(self, request: Request) -> Response: Returns the list of AI models that are currently used in production in Seer. This endpoint does not require authentication and can be used to discover which models Seer uses. + + Requests to this endpoint should use the region-specific domain + eg. `us.sentry.io` or `de.sentry.io` """ cached_data = cache.get(SEER_MODELS_CACHE_KEY) if cached_data is not None: diff --git a/src/sentry/apidocs/hooks.py b/src/sentry/apidocs/hooks.py index 4acea4cb0de22c..b46f48bd61e0cc 100644 --- a/src/sentry/apidocs/hooks.py +++ b/src/sentry/apidocs/hooks.py @@ -103,10 +103,21 @@ class CustomGenerator(SchemaGenerator): endpoint_inspector_cls = CustomEndpointEnumerator +# Collected during preprocessing, used in postprocessing +_ENDPOINT_SERVERS: dict[str, list[dict[str, Any]]] = {} + + def custom_preprocessing_hook(endpoints: Any) -> Any: # TODO: organize method, rename + _ENDPOINT_SERVERS.clear() + filtered = [] ownership_data: dict[ApiOwner, dict] = {} for path, path_regex, method, callback in endpoints: + # Collect servers from endpoint class for postprocessing + endpoint_servers = getattr(callback.view_class, "servers", None) + if endpoint_servers is not None: + _ENDPOINT_SERVERS[path] = endpoint_servers + owner_team = callback.view_class.owner if owner_team not in ownership_data: ownership_data[owner_team] = { @@ -222,6 +233,12 @@ def _validate_request_body( def custom_postprocessing_hook(result: Any, generator: Any, **kwargs: Any) -> Any: + # Add servers override from endpoint class definitions + for path, servers in _ENDPOINT_SERVERS.items(): + if path in result["paths"]: + for method_info in result["paths"][path].values(): + method_info["servers"] = servers + _fix_issue_paths(result) # Fetch schema component references diff --git a/tests/apidocs/test_hooks.py b/tests/apidocs/test_hooks.py index 68d2193e1482b9..f6e288a3507488 100644 --- a/tests/apidocs/test_hooks.py +++ b/tests/apidocs/test_hooks.py @@ -1,6 +1,49 @@ from unittest import TestCase -from sentry.apidocs.hooks import custom_postprocessing_hook +from sentry.apidocs.hooks import _ENDPOINT_SERVERS, custom_postprocessing_hook + + +class EndpointServersTest(TestCase): + def setUp(self) -> None: + _ENDPOINT_SERVERS.clear() + + def tearDown(self) -> None: + _ENDPOINT_SERVERS.clear() + + def test_servers_applied_to_endpoint(self) -> None: + """Test that servers from _ENDPOINT_SERVERS are applied to matching paths.""" + _ENDPOINT_SERVERS["/api/0/seer/models/"] = [{"url": "https://{region}.sentry.io"}] + + result = { + "components": {"schemas": {}}, + "paths": { + "/api/0/seer/models/": { + "get": { + "tags": ["Seer"], + "description": "Get models", + "operationId": "get models", + "parameters": [], + } + }, + "/api/0/other/endpoint/": { + "get": { + "tags": ["Events"], + "description": "Other endpoint", + "operationId": "get other", + "parameters": [], + } + }, + }, + } + + processed = custom_postprocessing_hook(result, None) + + # Servers should be applied to the matching endpoint + assert processed["paths"]["/api/0/seer/models/"]["get"]["servers"] == [ + {"url": "https://{region}.sentry.io"} + ] + # Servers should NOT be applied to non-matching endpoint + assert "servers" not in processed["paths"]["/api/0/other/endpoint/"]["get"] class FixIssueRoutesTest(TestCase):