From 86b06562dd1db54425e8f56d1f669f124f12dbc7 Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:57:29 -0800 Subject: [PATCH 1/5] add endpoint servers to track regional api calls --- src/sentry/api/base.py | 1 + src/sentry/api/endpoints/seer_models.py | 4 ++++ src/sentry/apidocs/hooks.py | 17 +++++++++++++++++ 3 files changed, 22 insertions(+) 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..11b199663b72cd 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] = { @@ -224,6 +235,12 @@ def _validate_request_body( def custom_postprocessing_hook(result: Any, generator: Any, **kwargs: Any) -> Any: _fix_issue_paths(result) + # 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 + # Fetch schema component references schema_components = result["components"]["schemas"] From 04a0838e67dff2caee6ce19a94d548c252130118 Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:05:13 -0800 Subject: [PATCH 2/5] assign servers before fixing path names --- src/sentry/apidocs/hooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/apidocs/hooks.py b/src/sentry/apidocs/hooks.py index 11b199663b72cd..b46f48bd61e0cc 100644 --- a/src/sentry/apidocs/hooks.py +++ b/src/sentry/apidocs/hooks.py @@ -233,14 +233,14 @@ def _validate_request_body( def custom_postprocessing_hook(result: Any, generator: Any, **kwargs: Any) -> Any: - _fix_issue_paths(result) - # 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 schema_components = result["components"]["schemas"] From f79f32f9e9dcdb6281e6b4fd6d500722e15b9bbf Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:57:16 -0800 Subject: [PATCH 3/5] fix watch command to allow for testing --- api-docs/watch.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api-docs/watch.ts b/api-docs/watch.ts index 8d4129a22c4bbd..f51cd7aec2c369 100644 --- a/api-docs/watch.ts +++ b/api-docs/watch.ts @@ -1,9 +1,12 @@ import {spawn} from 'node:child_process'; -import {join} from 'node:path'; +import {dirname, join} from 'node:path'; import {stderr, stdout} from 'node:process'; +import {fileURLToPath} from 'node:url'; import sane from 'sane'; +const __dirname = dirname(fileURLToPath(import.meta.url)); + const watcherPy = sane(join(__dirname, '../src/sentry')); const watcherJson = sane(join(__dirname, '../api-docs')); From b166d447e9879cf1128acb3e5ad3d0748217fcd7 Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:21:29 -0800 Subject: [PATCH 4/5] Revert watch fix because its frontend --- api-docs/watch.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api-docs/watch.ts b/api-docs/watch.ts index f51cd7aec2c369..8d4129a22c4bbd 100644 --- a/api-docs/watch.ts +++ b/api-docs/watch.ts @@ -1,12 +1,9 @@ import {spawn} from 'node:child_process'; -import {dirname, join} from 'node:path'; +import {join} from 'node:path'; import {stderr, stdout} from 'node:process'; -import {fileURLToPath} from 'node:url'; import sane from 'sane'; -const __dirname = dirname(fileURLToPath(import.meta.url)); - const watcherPy = sane(join(__dirname, '../src/sentry')); const watcherJson = sane(join(__dirname, '../api-docs')); From 920e870f7c4af50fc0c3a3f6b12c002a21ecbf27 Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:36:35 -0800 Subject: [PATCH 5/5] add unit test --- tests/apidocs/test_hooks.py | 45 ++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) 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):