From 9811845f3a7af2bcb1d564a413b3cacecad8cfbe Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 1 Sep 2026 09:15:41 +0200 Subject: [PATCH 1/5] feat(fastapi): Add http.route attribute --- sentry_sdk/consts.py | 6 ++++++ sentry_sdk/integrations/asgi.py | 3 +++ sentry_sdk/integrations/fastapi.py | 2 ++ sentry_sdk/scope.py | 1 + 4 files changed, 12 insertions(+) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 82a9f27ec8..c9323623d0 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -878,6 +878,12 @@ class SPANDATA: Example: GET """ + HTTP_ROUTE = "http.route" + """ + The matched route, that is, the path template used to match the request. + Example: /users/{id} + """ + HTTP_QUERY = "http.query" """ The Query string present in the URL. diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index d594c3504d..a0f1c4d819 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -412,6 +412,9 @@ async def _sentry_wrapped_send( span.set_attribute( "sentry.segment.name.source", source ) + http_route = getattr(sentry_sdk.get_current_scope(), "_http_route", None) + if http_route is not None: + span.set_attribute(SPANDATA.HTTP_ROUTE, http_route) finally: _asgi_middleware_applied.set(False) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index dc408797e3..d40ce76c77 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -79,6 +79,8 @@ def _set_transaction_name_and_source( if path is not None: name = path + scope._http_route = route + if not name: name = _DEFAULT_TRANSACTION_NAME source = TransactionSource.ROUTE diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 9587ed84fd..7ce13a093f 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -240,6 +240,7 @@ class Scope: "_last_event_id", "_flags", "_attributes", + "_http_route", ) def __init__( From f900aee9090553be7f8f92bfb535da58c104ff21 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 1 Sep 2026 09:35:02 +0200 Subject: [PATCH 2/5] . --- sentry_sdk/integrations/asgi.py | 2 +- sentry_sdk/integrations/fastapi.py | 2 +- sentry_sdk/scope.py | 2 ++ tests/integrations/fastapi/test_fastapi.py | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index a0f1c4d819..3236f1db4c 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -412,7 +412,7 @@ async def _sentry_wrapped_send( span.set_attribute( "sentry.segment.name.source", source ) - http_route = getattr(sentry_sdk.get_current_scope(), "_http_route", None) + http_route = sentry_scope._http_route if http_route is not None: span.set_attribute(SPANDATA.HTTP_ROUTE, http_route) finally: diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index d40ce76c77..5281d803d8 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -79,7 +79,7 @@ def _set_transaction_name_and_source( if path is not None: name = path - scope._http_route = route + sentry_sdk.get_isolation_scope()._http_route = name if not name: name = _DEFAULT_TRANSACTION_NAME diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 7ce13a093f..071a3f865b 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -254,6 +254,7 @@ def __init__( self._error_processors: "List[ErrorProcessor]" = [] self._name: "Optional[str]" = None + self._http_route: "Optional[str]" = None self._propagation_context: "Optional[PropagationContext]" = None self._n_breadcrumbs_truncated: int = 0 self._gen_ai_original_message_count: "Dict[str, int]" = {} @@ -279,6 +280,7 @@ def __copy__(self) -> "Scope": rv.client = self.client rv._level = self._level rv._name = self._name + rv._http_route = self._http_route rv._fingerprint = self._fingerprint rv._transaction = self._transaction rv._transaction_info = self._transaction_info.copy() diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 4a1df70d7c..8e5d7f5ca9 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -883,6 +883,7 @@ async def get_user(user_id: int): segment = segments[0] assert segment["name"] == "/api/users/{user_id}" assert segment["attributes"]["sentry.segment.name.source"] == "route" + assert segment["attributes"]["http.route"] == "/api/users/{user_id}" else: (transaction_envelope,) = envelopes transaction_event = transaction_envelope.get_transaction_event() From 04559a4a9309ea4f929792cdb8a25ae4b98feae7 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 1 Sep 2026 09:35:51 +0200 Subject: [PATCH 3/5] . --- sentry_sdk/scope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 071a3f865b..c572782de5 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -254,7 +254,7 @@ def __init__( self._error_processors: "List[ErrorProcessor]" = [] self._name: "Optional[str]" = None - self._http_route: "Optional[str]" = None + self._http_route: "Optional[str]" = None self._propagation_context: "Optional[PropagationContext]" = None self._n_breadcrumbs_truncated: int = 0 self._gen_ai_original_message_count: "Dict[str, int]" = {} From e149db8ab827d53317fc44e8bab57d45ce0903a4 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 1 Sep 2026 13:55:40 +0200 Subject: [PATCH 4/5] store span on scope instead of attribute --- sentry_sdk/integrations/asgi.py | 5 ++--- sentry_sdk/integrations/fastapi.py | 4 +++- sentry_sdk/scope.py | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 3236f1db4c..32940004ce 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -281,6 +281,7 @@ async def _run_app( attributes=attributes, parent_span=None, ) + sentry_scope.get_current_scope()._server_segment_span = segment else: sentry_sdk.traces.new_trace() @@ -292,6 +293,7 @@ async def _run_app( attributes=attributes, parent_span=None, ) + sentry_scope.get_current_scope()._server_segment_span = segment span_ctx = segment or nullcontext() @@ -412,9 +414,6 @@ async def _sentry_wrapped_send( span.set_attribute( "sentry.segment.name.source", source ) - http_route = sentry_scope._http_route - if http_route is not None: - span.set_attribute(SPANDATA.HTTP_ROUTE, http_route) finally: _asgi_middleware_applied.set(False) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 5281d803d8..6f43f515b3 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -79,7 +79,9 @@ def _set_transaction_name_and_source( if path is not None: name = path - sentry_sdk.get_isolation_scope()._http_route = name + server_span = sentry_sdk.get_current_scope()._server_segment_span + if server_span is not None: + server_span.set_attribute(SPANDATA.HTTP_ROUTE, name) if not name: name = _DEFAULT_TRANSACTION_NAME diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index c572782de5..4d547a3f48 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -230,6 +230,7 @@ class Scope: "_error_processors", "_should_capture", "_span", + "_server_segment_span", "_session", "_attachments", "_force_auto_session_tracking", @@ -240,7 +241,6 @@ class Scope: "_last_event_id", "_flags", "_attributes", - "_http_route", ) def __init__( @@ -254,11 +254,12 @@ def __init__( self._error_processors: "List[ErrorProcessor]" = [] self._name: "Optional[str]" = None - self._http_route: "Optional[str]" = None self._propagation_context: "Optional[PropagationContext]" = None self._n_breadcrumbs_truncated: int = 0 self._gen_ai_original_message_count: "Dict[str, int]" = {} + self._server_segment_span: "Optional[StreamedSpan]" = None + self.client: "sentry_sdk.client.BaseClient" = NonRecordingClient() if client is not None: @@ -280,7 +281,6 @@ def __copy__(self) -> "Scope": rv.client = self.client rv._level = self._level rv._name = self._name - rv._http_route = self._http_route rv._fingerprint = self._fingerprint rv._transaction = self._transaction rv._transaction_info = self._transaction_info.copy() @@ -299,6 +299,7 @@ def __copy__(self) -> "Scope": rv._should_capture = self._should_capture rv._span = self._span + rv._server_segment_span = self._server_segment_span rv._session = self._session rv._force_auto_session_tracking = self._force_auto_session_tracking rv._attachments = self._attachments.copy() From f633dc4e3e89514b991ab856f1178122e8fd83f8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 1 Sep 2026 13:57:09 +0200 Subject: [PATCH 5/5] run pre-commit --- sentry_sdk/integrations/asgi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 32940004ce..17ffbaacf7 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -293,7 +293,9 @@ async def _run_app( attributes=attributes, parent_span=None, ) - sentry_scope.get_current_scope()._server_segment_span = segment + sentry_scope.get_current_scope()._server_segment_span = ( + segment + ) span_ctx = segment or nullcontext()