From b6107bab4a53bbb21b911d6d71abdf90e4b902fe Mon Sep 17 00:00:00 2001 From: Matt Quinn Date: Thu, 13 Aug 2026 16:38:20 -0400 Subject: [PATCH] fix(starlette): Set transaction name on current scope in sync handler The sync request/response handler passed the _isolation_ scope to `_set_transaction_name_and_source`, but the transaction/segment span lives on the _current_ scope. As a result the route-resolved name never reached the span for sync endpoints, which were instead named by the raw URL from the ASGI middleware (`transaction_info.source` of `url` rather than `route`). Async handlers already used the current scope and were unaffected. Pass the current scope (already computed above) so sync and async handlers behave identically: - streaming: the segment name / `sentry.segment.name.source` are route-based - static: the transaction event name / source are route-based For parametrized routes this also removes high-cardinality URL transaction names for sync endpoints. --- sentry_sdk/integrations/starlette.py | 2 +- .../integrations/starlette/test_starlette.py | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 7579c70c11..772ba7c29e 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -632,7 +632,7 @@ def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": request = args[0] _set_transaction_name_and_source( - sentry_scope, integration.transaction_style, request + current_scope, integration.transaction_style, request ) extractor = StarletteRequestExtractor(request) diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index e8198ce7b8..35e6f10c86 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -1499,6 +1499,47 @@ def test_active_thread_id_span_streaming(sentry_init, capture_items, endpoint): assert str(data["active"]) == segments[0]["attributes"]["thread.id"] +@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) +def test_transaction_name_span_streaming(sentry_init, capture_items, endpoint): + sentry_init( + auto_enabling_integrations=False, + integrations=[StarletteIntegration(transaction_style="url")], + traces_sample_rate=1.0, + trace_lifecycle="stream", + ) + app = starlette_app_factory() + + items = capture_items("span") + + client = TestClient(app) + response = client.get(endpoint) + assert response.status_code == 200 + + sentry_sdk.flush() + + segments = [item.payload for item in items if item.payload.get("is_segment")] + assert len(segments) == 1 + assert segments[0]["name"] == endpoint + assert segments[0]["attributes"]["sentry.segment.name.source"] == "route" + + +@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"]) +def test_transaction_name_static(sentry_init, capture_events, endpoint): + sentry_init( + integrations=[StarletteIntegration(transaction_style="url")], + traces_sample_rate=1.0, + ) + events = capture_events() + + client = TestClient(starlette_app_factory()) + response = client.get(endpoint) + assert response.status_code == 200 + + (transaction,) = [e for e in events if e.get("type") == "transaction"] + assert transaction["transaction"] == endpoint + assert transaction["transaction_info"] == {"source": "route"} + + def test_original_request_not_scrubbed(sentry_init, capture_events): sentry_init(integrations=[StarletteIntegration()])