From 5eddc69d6013e9fa66b5e71253be9f59a4c932b0 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 2 Sep 2026 14:30:42 +0200 Subject: [PATCH 1/2] feat(starlette): Add http.route attribute --- sentry_sdk/integrations/starlette.py | 58 +++++++++++++++---- .../integrations/starlette/test_starlette.py | 5 +- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 418badb680..a82d1cfedb 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -541,10 +541,22 @@ async def _wrap_async_handler( request = args[0] + route_path, name_source = _http_route_and_source_from_router(request.scope) + + server_span = sentry_sdk.get_current_scope()._server_segment_span + if ( + server_span is not None + and route_path is not None + and name_source == TransactionSource.ROUTE + ): + server_span.set_attribute(SPANDATA.HTTP_ROUTE, route_path) + _set_transaction_name_and_source( sentry_sdk.get_current_scope(), integration.transaction_style, - request, + endpoint=request.scope.get("endpoint"), + route_path=route_path, + name_source=name_source, ) sentry_scope = sentry_sdk.get_isolation_scope() @@ -647,8 +659,24 @@ def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": request = args[0] + route_path, name_source = _http_route_and_source_from_router( + request.scope + ) + + server_span = sentry_sdk.get_current_scope()._server_segment_span + if ( + server_span is not None + and route_path is not None + and name_source == TransactionSource.ROUTE + ): + server_span.set_attribute(SPANDATA.HTTP_ROUTE, route_path) + _set_transaction_name_and_source( - current_scope, integration.transaction_style, request + current_scope, + integration.transaction_style, + endpoint=request.scope.get("endpoint"), + route_path=route_path, + name_source=name_source, ) extractor = StarletteRequestExtractor(request) @@ -843,7 +871,7 @@ async def json(self: "StarletteRequestExtractor") -> "Optional[Dict[str, Any]]": return None -def _transaction_name_and_source_from_router( +def _http_route_and_source_from_router( scope: "StarletteScope", ) -> "Tuple[Optional[str], TransactionSource]": router = scope.get("router") @@ -864,18 +892,20 @@ def _transaction_name_and_source_from_router( def _set_transaction_name_and_source( - scope: "sentry_sdk.Scope", transaction_style: str, request: "Any" + scope: "sentry_sdk.Scope", + transaction_style: str, + endpoint: "Optional[Callable[..., Any]]", + route_path: "Optional[str]", + name_source: "TransactionSource", ) -> None: name = None source = SOURCE_FOR_STYLE[transaction_style] - if transaction_style == "endpoint": - endpoint = request.scope.get("endpoint") - if endpoint: - name = transaction_from_function(endpoint) or None + if transaction_style == "endpoint" and endpoint: + name = transaction_from_function(endpoint) or None elif transaction_style == "url": - name, source = _transaction_name_and_source_from_router(request.scope) + name, source = route_path, name_source if name is None: name = _DEFAULT_TRANSACTION_NAME @@ -894,6 +924,14 @@ def _get_transaction_from_middleware( name = transaction_from_function(app.__class__) source = TransactionSource.COMPONENT elif integration.transaction_style == "url": - name, source = _transaction_name_and_source_from_router(asgi_scope) + route_path, name_source = _http_route_and_source_from_router(asgi_scope) + + server_span = sentry_sdk.get_current_scope()._server_segment_span + if ( + server_span is not None + and route_path is not None + and name_source == TransactionSource.ROUTE + ): + server_span.set_attribute(SPANDATA.HTTP_ROUTE, route_path) return name, source diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 88339a8c2f..70b3940552 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -1638,9 +1638,7 @@ def test_active_thread_id_span_streaming(sentry_init, capture_items, endpoint): @pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) -def test_segment_name_is_route_resolved_name_span_streaming( - sentry_init, capture_items, endpoint -): +def test_http_route_span_streaming(sentry_init, capture_items, endpoint): sentry_init( auto_enabling_integrations=False, integrations=[StarletteIntegration(transaction_style="url")], @@ -1661,6 +1659,7 @@ def test_segment_name_is_route_resolved_name_span_streaming( assert len(segments) == 1 assert segments[0]["name"] == endpoint assert segments[0]["attributes"]["sentry.segment.name.source"] == "route" + assert segments[0]["attributes"]["http.route"] == endpoint @pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) From 0c2513196f882dad980e8779c4941ed98bb3b03d Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 2 Sep 2026 14:35:57 +0200 Subject: [PATCH 2/2] . --- sentry_sdk/integrations/starlette.py | 29 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index a82d1cfedb..a9ed8f2f73 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -184,8 +184,20 @@ async def _create_span_call( if integration is None: return await old_call(app, scope, receive, send, **kwargs) + route_path, name_source = _http_route_and_source_from_router(scope) + + server_span = sentry_sdk.get_current_scope()._server_segment_span + if ( + server_span is not None + and route_path is not None + and name_source == TransactionSource.ROUTE + ): + server_span.set_attribute(SPANDATA.HTTP_ROUTE, route_path) + # Update transaction name with middleware name - name, source = _get_transaction_from_middleware(app, scope, integration) + name, source = _get_transaction_from_middleware( + app, integration, route_path=route_path, name_source=name_source + ) if name is not None: sentry_sdk.get_current_scope().set_transaction_name( @@ -915,7 +927,10 @@ def _set_transaction_name_and_source( def _get_transaction_from_middleware( - app: "Any", asgi_scope: "Dict[str, Any]", integration: "StarletteIntegration" + app: "Any", + integration: "StarletteIntegration", + route_path: "Optional[str]", + name_source: "TransactionSource", ) -> "Tuple[Optional[str], Optional[str]]": name = None source = None @@ -924,14 +939,6 @@ def _get_transaction_from_middleware( name = transaction_from_function(app.__class__) source = TransactionSource.COMPONENT elif integration.transaction_style == "url": - route_path, name_source = _http_route_and_source_from_router(asgi_scope) - - server_span = sentry_sdk.get_current_scope()._server_segment_span - if ( - server_span is not None - and route_path is not None - and name_source == TransactionSource.ROUTE - ): - server_span.set_attribute(SPANDATA.HTTP_ROUTE, route_path) + name, source = route_path, name_source return name, source