From 282ba352292a7e7187ed1a3f428e9fb3a3971b04 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 30 Mar 2026 14:56:20 +0200 Subject: [PATCH 01/15] feat(asgi): Migrate away from event processor in span first --- sentry_sdk/integrations/_asgi_common.py | 30 ++++++++++++ sentry_sdk/integrations/asgi.py | 65 +++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index a8022c6bb1..dc36119f60 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -12,6 +12,7 @@ from typing import Union from typing_extensions import Literal + from sentry_sdk._types import Attributes from sentry_sdk.utils import AnnotatedValue @@ -105,3 +106,32 @@ def _get_request_data(asgi_scope: "Any") -> "Dict[str, Any]": request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} return request_data + + +def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]": + """ + Return attributes related to the HTTP request from the ASGI scope. + """ + attributes: "Attributes" = {} + + ty = asgi_scope["type"] + if ty in ("http", "websocket"): + if asgi_scope.get("method"): + attributes["http.request.method"] = asgi_scope["method"].upper() + + headers = _filter_headers(_get_headers(asgi_scope)) + # TODO[span-first]: Correctly merge headers if duplicate + for header, value in headers.items(): + attributes[f"http.request.headers.{header.lower()}"] = [value] + + attributes["http.query"] = _get_query(asgi_scope) + + attributes["url.full"] = _get_url( + asgi_scope, "http" if ty == "http" else "ws", headers.get("host") + ) + + client = asgi_scope.get("client") + if client and should_send_default_pii(): + attributes["client.address"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} + + return attributes diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 2294781f05..a85450f937 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -15,6 +15,7 @@ from sentry_sdk.consts import OP from sentry_sdk.integrations._asgi_common import ( _get_headers, + _get_request_attributes, _get_request_data, _get_url, ) @@ -23,7 +24,11 @@ nullcontext, ) from sentry_sdk.sessions import track_session -from sentry_sdk.traces import StreamedSpan +from sentry_sdk.traces import ( + StreamedSpan, + SegmentSource, + SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, +) from sentry_sdk.tracing import ( SOURCE_FOR_STYLE, Transaction, @@ -40,6 +45,7 @@ _get_installed_modules, reraise, capture_internal_exceptions, + qualname_from_function, ) from typing import TYPE_CHECKING @@ -235,7 +241,7 @@ async def _run_app( transaction_source, "value", transaction_source ), "sentry.origin": self.span_origin, - "asgi.type": ty, + "network.protocol.name": ty, } if ty in ("http", "websocket"): @@ -301,6 +307,9 @@ async def _run_app( else nullcontext() ) + for attribute, value in _get_request_attributes(scope): + sentry_scope.set_attribute(attribute, value) + with span_ctx as span: try: @@ -329,13 +338,24 @@ async def _sentry_wrapped_send( return await send(event) if asgi_version == 2: - return await self.app(scope)( + result = await self.app(scope)( receive, _sentry_wrapped_send ) else: - return await self.app( + result = await self.app( scope, receive, _sentry_wrapped_send ) + + with capture_internal_exceptions(): + name, source = self._get_segment_name_and_source( + self.transaction_style, scope + ) + if isinstance(span, StreamedSpan): + span.name = name + span.set_attribute("sentry.span.source", source) + + return result + except Exception as exc: suppress_chained_exceptions = ( sentry_sdk.get_client() @@ -424,3 +444,40 @@ def _get_transaction_name_and_source( return name, source return name, source + + def _get_segment_name_and_source( + self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any" + ) -> "Tuple[str, str]": + name = None + source = SEGMENT_SOURCE_FOR_STYLE[segment_style] + ty = asgi_scope.get("type") + + if segment_style == "endpoint": + endpoint = asgi_scope.get("endpoint") + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our generic transaction name. + if endpoint: + name = qualname_from_function(endpoint) or "" + else: + name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None) + source = SegmentSource.URL.value + + elif segment_style == "url": + # FastAPI includes the route object in the scope to let Sentry extract the + # path from it for the transaction name + route = asgi_scope.get("route") + if route: + path = getattr(route, "path", None) + if path is not None: + name = path + else: + name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None) + source = SegmentSource.URL.value + + if name is None: + name = _DEFAULT_TRANSACTION_NAME + source = SegmentSource.ROUTE.value + return name, source + + return name, source From e2484bd32ba035f674e7185ab2d3e224fba67cf4 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 30 Mar 2026 15:13:07 +0200 Subject: [PATCH 02/15] fixes --- sentry_sdk/integrations/_asgi_common.py | 2 +- sentry_sdk/integrations/asgi.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index dc36119f60..4c84b691a5 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -132,6 +132,6 @@ def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]": client = asgi_scope.get("client") if client and should_send_default_pii(): - attributes["client.address"] = {"REMOTE_ADDR": _get_ip(asgi_scope)} + attributes["client.address"] = _get_ip(asgi_scope) return attributes diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index a85450f937..e942995ac2 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -307,7 +307,7 @@ async def _run_app( else nullcontext() ) - for attribute, value in _get_request_attributes(scope): + for attribute, value in _get_request_attributes(scope).items(): sentry_scope.set_attribute(attribute, value) with span_ctx as span: @@ -449,7 +449,7 @@ def _get_segment_name_and_source( self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any" ) -> "Tuple[str, str]": name = None - source = SEGMENT_SOURCE_FOR_STYLE[segment_style] + source = SEGMENT_SOURCE_FOR_STYLE[segment_style].value ty = asgi_scope.get("type") if segment_style == "endpoint": From 5c3174f0bc94c5bd25ebe27361fc594b7cda73b5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 30 Mar 2026 15:26:38 +0200 Subject: [PATCH 03/15] . --- sentry_sdk/integrations/asgi.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index e942995ac2..3fb732e387 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -338,24 +338,14 @@ async def _sentry_wrapped_send( return await send(event) if asgi_version == 2: - result = await self.app(scope)( + return await self.app(scope)( receive, _sentry_wrapped_send ) else: - result = await self.app( + return await self.app( scope, receive, _sentry_wrapped_send ) - with capture_internal_exceptions(): - name, source = self._get_segment_name_and_source( - self.transaction_style, scope - ) - if isinstance(span, StreamedSpan): - span.name = name - span.set_attribute("sentry.span.source", source) - - return result - except Exception as exc: suppress_chained_exceptions = ( sentry_sdk.get_client() @@ -370,6 +360,15 @@ async def _sentry_wrapped_send( with capture_internal_exceptions(): self._capture_request_exception(exc) reraise(*exc_info) + + finally: + with capture_internal_exceptions(): + name, source = self._get_segment_name_and_source( + self.transaction_style, scope + ) + if isinstance(span, StreamedSpan): + span.name = name + span.set_attribute("sentry.span.source", source) finally: _asgi_middleware_applied.set(False) From b270e5ab28588111511a7dcbefb825c71adfe294 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 1 Apr 2026 11:27:09 +0200 Subject: [PATCH 04/15] . --- sentry_sdk/integrations/_asgi_common.py | 3 +- tests/conftest.py | 23 +++++++ tests/integrations/asgi/test_asgi.py | 80 +++++++++++++++++++------ tests/tracing/test_span_streaming.py | 26 +------- 4 files changed, 89 insertions(+), 43 deletions(-) diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index 4c84b691a5..a4ce86d5e3 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -120,9 +120,8 @@ def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]": attributes["http.request.method"] = asgi_scope["method"].upper() headers = _filter_headers(_get_headers(asgi_scope)) - # TODO[span-first]: Correctly merge headers if duplicate for header, value in headers.items(): - attributes[f"http.request.headers.{header.lower()}"] = [value] + attributes[f"http.request.header.{header.lower()}"] = value attributes["http.query"] = _get_query(asgi_scope) diff --git a/tests/conftest.py b/tests/conftest.py index 71f2431aac..c723229ed7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1212,6 +1212,29 @@ def werkzeug_set_cookie(client, servername, key, value): client.set_cookie(key, value) +def envelopes_to_spans(envelopes): + res: "list[dict[str, Any]]" = [] + for envelope in envelopes: + for item in envelope.items: + if item.type == "span": + for span_json in item.payload.json["items"]: + span = { + "start_timestamp": span_json["start_timestamp"], + "end_timestamp": span_json.get("end_timestamp"), + "trace_id": span_json["trace_id"], + "span_id": span_json["span_id"], + "name": span_json["name"], + "status": span_json["status"], + "is_segment": span_json["is_segment"], + "parent_span_id": span_json.get("parent_span_id"), + "attributes": { + k: v["value"] for (k, v) in span_json["attributes"].items() + }, + } + res.append(span) + return res + + @contextmanager def patch_start_tracing_child( fake_transaction_is_none: bool = False, diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index ec2796c140..76e8824c60 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -6,6 +6,7 @@ from sentry_sdk.tracing import TransactionSource from sentry_sdk.integrations._asgi_common import _get_ip, _get_headers from sentry_sdk.integrations.asgi import SentryAsgiMiddleware, _looks_like_asgi3 +from tests.conftest import envelopes_to_spans from async_asgi_testclient import TestClient @@ -164,34 +165,79 @@ def test_invalid_transaction_style(asgi3_app): @pytest.mark.asyncio +@pytest.mark.parametrize( + ("span_streaming", "send_default_pii"), + [[False, True], [False, True]], +) async def test_capture_transaction( sentry_init, asgi3_app, capture_events, + capture_envelopes, + span_streaming, + send_default_pii, ): - sentry_init(send_default_pii=True, traces_sample_rate=1.0) + sentry_init( + send_default_pii=send_default_pii, + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) app = SentryAsgiMiddleware(asgi3_app) async with TestClient(app) as client: - events = capture_events() + if span_streaming: + envelopes = capture_envelopes() + else: + events = capture_events() await client.get("/some_url?somevalue=123") - (transaction_event,) = events + sentry_sdk.flush() - assert transaction_event["type"] == "transaction" - assert transaction_event["transaction"] == "/some_url" - assert transaction_event["transaction_info"] == {"source": "url"} - assert transaction_event["contexts"]["trace"]["op"] == "http.server" - assert transaction_event["request"] == { - "headers": { - "host": "localhost", - "remote-addr": "127.0.0.1", - "user-agent": "ASGI-Test-Client", - }, - "method": "GET", - "query_string": "somevalue=123", - "url": "http://localhost/some_url", - } + if span_streaming: + spans = envelopes_to_spans(envelopes) + assert len(spans) == 1 + (span,) = spans + + assert span["is_segment"] is True + assert span["name"] == "/some_url" + + assert span["attributes"]["sentry.span.source"] == "url" + assert span["attributes"]["sentry.op"] == "http.server" + + if send_default_pii: + assert span["attributes"]["client.address"] == "127.0.0.1" + else: + assert "client.address" not in span["attributes"] + + assert span["attributes"]["http.request.method"] == "GET" + assert span["attributes"]["url.full"] == "http://localhost/some_url" + assert span["attributes"]["http.query"] == "somevalue=123" + assert span["attributes"]["http.request.protocol.name"] == "http" + assert span["attributes"]["http.request.header.host"] == "localhost" + assert span["attributes"]["http.request.header.remote-addr"] == "127.0.0.1" + assert ( + span["attributes"]["http.request.header.user-agent"] == "ASGI-Test-Client" + ) + + else: + (transaction_event,) = events + + assert transaction_event["type"] == "transaction" + assert transaction_event["transaction"] == "/some_url" + assert transaction_event["transaction_info"] == {"source": "url"} + assert transaction_event["contexts"]["trace"]["op"] == "http.server" + assert transaction_event["request"] == { + "headers": { + "host": "localhost", + "remote-addr": "127.0.0.1", + "user-agent": "ASGI-Test-Client", + }, + "method": "GET", + "query_string": "somevalue=123", + "url": "http://localhost/some_url", + } @pytest.mark.asyncio diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index 21c3d26ea3..458c486e3f 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -2,7 +2,6 @@ import re import sys import time -from typing import Any from unittest import mock import pytest @@ -10,35 +9,14 @@ import sentry_sdk from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.traces import NoOpStreamedSpan, SpanStatus, StreamedSpan +from tests.conftest import envelopes_to_spans + minimum_python_38 = pytest.mark.skipif( sys.version_info < (3, 8), reason="Asyncio tests need Python >= 3.8" ) -def envelopes_to_spans(envelopes): - res: "list[dict[str, Any]]" = [] - for envelope in envelopes: - for item in envelope.items: - if item.type == "span": - for span_json in item.payload.json["items"]: - span = { - "start_timestamp": span_json["start_timestamp"], - "end_timestamp": span_json.get("end_timestamp"), - "trace_id": span_json["trace_id"], - "span_id": span_json["span_id"], - "name": span_json["name"], - "status": span_json["status"], - "is_segment": span_json["is_segment"], - "parent_span_id": span_json.get("parent_span_id"), - "attributes": { - k: v["value"] for (k, v) in span_json["attributes"].items() - }, - } - res.append(span) - return res - - def test_start_span(sentry_init, capture_envelopes): sentry_init( traces_sample_rate=1.0, From e93fd1ba7ebb8a79869211093e95e88d2418c794 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 1 Apr 2026 12:31:30 +0200 Subject: [PATCH 05/15] . --- tests/integrations/asgi/test_asgi.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 76e8824c60..e117db12ba 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -166,8 +166,8 @@ def test_invalid_transaction_style(asgi3_app): @pytest.mark.asyncio @pytest.mark.parametrize( - ("span_streaming", "send_default_pii"), - [[False, True], [False, True]], + ("span_streaming"), + (True, False), ) async def test_capture_transaction( sentry_init, @@ -175,10 +175,8 @@ async def test_capture_transaction( capture_events, capture_envelopes, span_streaming, - send_default_pii, ): sentry_init( - send_default_pii=send_default_pii, traces_sample_rate=1.0, _experiments={ "trace_lifecycle": "stream" if span_streaming else "static", @@ -206,15 +204,10 @@ async def test_capture_transaction( assert span["attributes"]["sentry.span.source"] == "url" assert span["attributes"]["sentry.op"] == "http.server" - if send_default_pii: - assert span["attributes"]["client.address"] == "127.0.0.1" - else: - assert "client.address" not in span["attributes"] - - assert span["attributes"]["http.request.method"] == "GET" assert span["attributes"]["url.full"] == "http://localhost/some_url" + assert span["attributes"]["network.protocol.name"] == "http" + assert span["attributes"]["http.request.method"] == "GET" assert span["attributes"]["http.query"] == "somevalue=123" - assert span["attributes"]["http.request.protocol.name"] == "http" assert span["attributes"]["http.request.header.host"] == "localhost" assert span["attributes"]["http.request.header.remote-addr"] == "127.0.0.1" assert ( From 7cde97355f13e14da40c122a12fe37067d77ecf7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 2 Apr 2026 13:30:55 +0200 Subject: [PATCH 06/15] capture_items --- tests/conftest.py | 42 ++++++++++++++++ tests/integrations/asgi/test_asgi.py | 74 ++++++++++++++++++++-------- 2 files changed, 95 insertions(+), 21 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c723229ed7..85de20737b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ import brotli import gzip import io +from dataclasses import dataclass from threading import Thread from contextlib import contextmanager from http.server import BaseHTTPRequestHandler, HTTPServer @@ -320,6 +321,47 @@ def append_envelope(envelope): return inner +@dataclass +class UnwrappedItem: + type: str + payload: dict + + +@pytest.fixture +def capture_items(monkeypatch): + """Capture envelope payload, unfurling individual items.""" + + def inner(types=None): + telemetry = [] + test_client = sentry_sdk.get_client() + old_capture_envelope = test_client.transport.capture_envelope + + def append_envelope(envelope): + for item in envelope: + if types is None or item.type not in types: + continue + + if item.type in ("metric", "log", "span"): + for json in item.payload.json["items"]: + t = {k: v for k, v in json.items() if k != "attributes"} + t["attributes"] = { + k: v["value"] for k, v in json["attributes"].items() + } + telemetry.append(UnwrappedItem(type=item.type, payload=t)) + else: + telemetry.append( + UnwrappedItem(type=item.type, payload=item.payload.json) + ) + + return old_capture_envelope(envelope) + + monkeypatch.setattr(test_client.transport, "capture_envelope", append_envelope) + + return telemetry + + return inner + + @pytest.fixture def capture_record_lost_event_calls(monkeypatch): def inner(): diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index e117db12ba..b5f13a5aad 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -6,7 +6,6 @@ from sentry_sdk.tracing import TransactionSource from sentry_sdk.integrations._asgi_common import _get_ip, _get_headers from sentry_sdk.integrations.asgi import SentryAsgiMiddleware, _looks_like_asgi3 -from tests.conftest import envelopes_to_spans from async_asgi_testclient import TestClient @@ -166,17 +165,18 @@ def test_invalid_transaction_style(asgi3_app): @pytest.mark.asyncio @pytest.mark.parametrize( - ("span_streaming"), - (True, False), + "span_streaming", + [True, False], ) async def test_capture_transaction( sentry_init, asgi3_app, capture_events, - capture_envelopes, + capture_items, span_streaming, ): sentry_init( + send_default_pii=True, traces_sample_rate=1.0, _experiments={ "trace_lifecycle": "stream" if span_streaming else "static", @@ -186,7 +186,7 @@ async def test_capture_transaction( async with TestClient(app) as client: if span_streaming: - envelopes = capture_envelopes() + items = capture_items(["span"]) else: events = capture_events() await client.get("/some_url?somevalue=123") @@ -194,9 +194,8 @@ async def test_capture_transaction( sentry_sdk.flush() if span_streaming: - spans = envelopes_to_spans(envelopes) - assert len(spans) == 1 - (span,) = spans + assert len(items) == 1 + span = items[0].payload assert span["is_segment"] is True assert span["name"] == "/some_url" @@ -234,24 +233,48 @@ async def test_capture_transaction( @pytest.mark.asyncio +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) async def test_capture_transaction_with_error( sentry_init, asgi3_app_with_error, capture_events, + capture_items, DictionaryContaining, # noqa: N803 + span_streaming, ): - sentry_init(send_default_pii=True, traces_sample_rate=1.0) + sentry_init( + send_default_pii=True, + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) + app = SentryAsgiMiddleware(asgi3_app_with_error) - events = capture_events() + if span_streaming: + items = capture_items(["event", "span"]) + else: + events = capture_events() + with pytest.raises(ZeroDivisionError): async with TestClient(app) as client: await client.get("/some_url") - ( - error_event, - transaction_event, - ) = events + sentry_sdk.flush() + + if span_streaming: + assert len(items) == 2 + assert items[0].type == "event" + assert items[1].type == "span" + + error_event = items[0].payload + span_item = items[1].payload + else: + (error_event, transaction_event) = events assert error_event["transaction"] == "/some_url" assert error_event["transaction_info"] == {"source": "url"} @@ -261,13 +284,22 @@ async def test_capture_transaction_with_error( assert error_event["exception"]["values"][0]["mechanism"]["handled"] is False assert error_event["exception"]["values"][0]["mechanism"]["type"] == "asgi" - assert transaction_event["type"] == "transaction" - assert transaction_event["contexts"]["trace"] == DictionaryContaining( - error_event["contexts"]["trace"] - ) - assert transaction_event["contexts"]["trace"]["status"] == "internal_error" - assert transaction_event["transaction"] == error_event["transaction"] - assert transaction_event["request"] == error_event["request"] + if span_streaming: + assert span_item["trace_id"] == error_event["contexts"]["trace"]["trace_id"] + assert span_item["span_id"] == error_event["contexts"]["trace"]["span_id"] + assert span_item.get("parent_span_id") == error_event["contexts"]["trace"].get( + "parent_span_id" + ) + assert span_item["status"] == "error" + + else: + assert transaction_event["type"] == "transaction" + assert transaction_event["contexts"]["trace"] == DictionaryContaining( + error_event["contexts"]["trace"] + ) + assert transaction_event["contexts"]["trace"]["status"] == "internal_error" + assert transaction_event["transaction"] == error_event["transaction"] + assert transaction_event["request"] == error_event["request"] @pytest.mark.asyncio From df3f3af5751fff053464b85018c36be41d99ac8f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 2 Apr 2026 13:37:22 +0200 Subject: [PATCH 07/15] . --- tests/conftest.py | 25 +------------------------ tests/integrations/asgi/test_asgi.py | 4 ++-- tests/tracing/test_span_streaming.py | 25 ++++++++++++++++++++++++- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 85de20737b..1d16cec5fb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -331,7 +331,7 @@ class UnwrappedItem: def capture_items(monkeypatch): """Capture envelope payload, unfurling individual items.""" - def inner(types=None): + def inner(*types): telemetry = [] test_client = sentry_sdk.get_client() old_capture_envelope = test_client.transport.capture_envelope @@ -1254,29 +1254,6 @@ def werkzeug_set_cookie(client, servername, key, value): client.set_cookie(key, value) -def envelopes_to_spans(envelopes): - res: "list[dict[str, Any]]" = [] - for envelope in envelopes: - for item in envelope.items: - if item.type == "span": - for span_json in item.payload.json["items"]: - span = { - "start_timestamp": span_json["start_timestamp"], - "end_timestamp": span_json.get("end_timestamp"), - "trace_id": span_json["trace_id"], - "span_id": span_json["span_id"], - "name": span_json["name"], - "status": span_json["status"], - "is_segment": span_json["is_segment"], - "parent_span_id": span_json.get("parent_span_id"), - "attributes": { - k: v["value"] for (k, v) in span_json["attributes"].items() - }, - } - res.append(span) - return res - - @contextmanager def patch_start_tracing_child( fake_transaction_is_none: bool = False, diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index b5f13a5aad..c8bc3cc1ca 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -186,7 +186,7 @@ async def test_capture_transaction( async with TestClient(app) as client: if span_streaming: - items = capture_items(["span"]) + items = capture_items("span") else: events = capture_events() await client.get("/some_url?somevalue=123") @@ -256,7 +256,7 @@ async def test_capture_transaction_with_error( app = SentryAsgiMiddleware(asgi3_app_with_error) if span_streaming: - items = capture_items(["event", "span"]) + items = capture_items("event", "span") else: events = capture_events() diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index 458c486e3f..445c8cfb99 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -3,13 +3,13 @@ import sys import time from unittest import mock +from typing import Any import pytest import sentry_sdk from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.traces import NoOpStreamedSpan, SpanStatus, StreamedSpan -from tests.conftest import envelopes_to_spans minimum_python_38 = pytest.mark.skipif( @@ -17,6 +17,29 @@ ) +def envelopes_to_spans(envelopes): + res: "list[dict[str, Any]]" = [] + for envelope in envelopes: + for item in envelope.items: + if item.type == "span": + for span_json in item.payload.json["items"]: + span = { + "start_timestamp": span_json["start_timestamp"], + "end_timestamp": span_json.get("end_timestamp"), + "trace_id": span_json["trace_id"], + "span_id": span_json["span_id"], + "name": span_json["name"], + "status": span_json["status"], + "is_segment": span_json["is_segment"], + "parent_span_id": span_json.get("parent_span_id"), + "attributes": { + k: v["value"] for (k, v) in span_json["attributes"].items() + }, + } + res.append(span) + return res + + def test_start_span(sentry_init, capture_envelopes): sentry_init( traces_sample_rate=1.0, From babb3bdcee89ab267f298ecd52722a5e687ea4f7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 2 Apr 2026 13:38:56 +0200 Subject: [PATCH 08/15] . --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1d16cec5fb..c052ddcd35 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -338,7 +338,7 @@ def inner(*types): def append_envelope(envelope): for item in envelope: - if types is None or item.type not in types: + if types and item.type not in types: continue if item.type in ("metric", "log", "span"): From 75429dc9df629c4dd6fb6a7de7ff7ac5ad2788c9 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 2 Apr 2026 13:50:05 +0200 Subject: [PATCH 09/15] no annotatedvalues --- sentry_sdk/integrations/_asgi_common.py | 2 +- sentry_sdk/integrations/_wsgi_common.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index a4ce86d5e3..24d550d09a 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -119,7 +119,7 @@ def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]": if asgi_scope.get("method"): attributes["http.request.method"] = asgi_scope["method"].upper() - headers = _filter_headers(_get_headers(asgi_scope)) + headers = _filter_headers(_get_headers(asgi_scope), use_annotated_value=False) for header, value in headers.items(): attributes[f"http.request.header.{header.lower()}"] = value diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 688e965be4..8a23f77e83 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -3,6 +3,7 @@ from copy import deepcopy import sentry_sdk +from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import AnnotatedValue, logger @@ -211,16 +212,18 @@ def _is_json_content_type(ct: "Optional[str]") -> bool: def _filter_headers( headers: "Mapping[str, str]", + use_annotated_value: bool = True, ) -> "Mapping[str, Union[AnnotatedValue, str]]": if should_send_default_pii(): return headers + if use_annotated_value: + substitute = AnnotatedValue.removed_because_over_size_limit() + else: + substitute = SENSITIVE_DATA_SUBSTITUTE + return { - k: ( - v - if k.upper().replace("-", "_") not in SENSITIVE_HEADERS - else AnnotatedValue.removed_because_over_size_limit() - ) + k: (v if k.upper().replace("-", "_") not in SENSITIVE_HEADERS else substitute) for k, v in headers.items() } From d9d76745e9d5f05d5449b601aa84ad047086127a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 2 Apr 2026 14:50:13 +0200 Subject: [PATCH 10/15] . --- tests/integrations/asgi/test_asgi.py | 71 ++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index c8bc3cc1ca..286e95e0cc 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -303,35 +303,78 @@ async def test_capture_transaction_with_error( @pytest.mark.asyncio +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) async def test_has_trace_if_performance_enabled( sentry_init, asgi3_app_with_error_and_msg, capture_events, + capture_items, + span_streaming, ): - sentry_init(traces_sample_rate=1.0) + sentry_init( + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg) with pytest.raises(ZeroDivisionError): async with TestClient(app) as client: - events = capture_events() + if span_streaming: + items = capture_items("event", "span") + else: + events = capture_events() await client.get("/") - msg_event, error_event, transaction_event = events + sentry_sdk.flush() - assert msg_event["contexts"]["trace"] - assert "trace_id" in msg_event["contexts"]["trace"] + if span_streaming: + for item in items: + print(item) + print() + msg_event, error_event, span = items - assert error_event["contexts"]["trace"] - assert "trace_id" in error_event["contexts"]["trace"] + assert msg_event.type == "event" + msg_event = msg_event.payload + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] - assert transaction_event["contexts"]["trace"] - assert "trace_id" in transaction_event["contexts"]["trace"] + assert error_event.type == "event" + error_event = error_event.payload + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] - assert ( - error_event["contexts"]["trace"]["trace_id"] - == transaction_event["contexts"]["trace"]["trace_id"] - == msg_event["contexts"]["trace"]["trace_id"] - ) + assert span.type == "span" + span = span.payload + assert span["trace_id"] is not None + + assert ( + error_event["contexts"]["trace"]["trace_id"] + == msg_event["contexts"]["trace"]["trace_id"] + == span["trace_id"] + ) + + else: + msg_event, error_event, transaction_event = events + + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] + + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] + + assert transaction_event["contexts"]["trace"] + assert "trace_id" in transaction_event["contexts"]["trace"] + + assert ( + error_event["contexts"]["trace"]["trace_id"] + == transaction_event["contexts"]["trace"]["trace_id"] + == msg_event["contexts"]["trace"]["trace_id"] + ) @pytest.mark.asyncio From 32e4191c85434261beadeb882bfc42748eb021c8 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 2 Apr 2026 14:54:58 +0200 Subject: [PATCH 11/15] . --- tests/integrations/asgi/test_asgi.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 286e95e0cc..d27395e0b6 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -333,9 +333,6 @@ async def test_has_trace_if_performance_enabled( sentry_sdk.flush() if span_streaming: - for item in items: - print(item) - print() msg_event, error_event, span = items assert msg_event.type == "event" From 5cecf9782dd54f28236f7c206f4b2be38e1804a3 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 2 Apr 2026 15:14:44 +0200 Subject: [PATCH 12/15] more tests --- tests/integrations/asgi/test_asgi.py | 274 ++++++++++++++++++++++----- 1 file changed, 222 insertions(+), 52 deletions(-) diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index d27395e0b6..c59243fc7c 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -397,13 +397,24 @@ async def test_has_trace_if_performance_disabled( assert "trace_id" in error_event["contexts"]["trace"] +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) @pytest.mark.asyncio async def test_trace_from_headers_if_performance_enabled( sentry_init, asgi3_app_with_error_and_msg, capture_events, + capture_items, + span_streaming, ): - sentry_init(traces_sample_rate=1.0) + sentry_init( + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg) trace_id = "582b43a4192642f0b136d5159a501701" @@ -411,23 +422,50 @@ async def test_trace_from_headers_if_performance_enabled( with pytest.raises(ZeroDivisionError): async with TestClient(app) as client: - events = capture_events() + if span_streaming: + items = capture_items("event", "span") + else: + events = capture_events() await client.get("/", headers={"sentry-trace": sentry_trace_header}) - msg_event, error_event, transaction_event = events + sentry_sdk.flush() - assert msg_event["contexts"]["trace"] - assert "trace_id" in msg_event["contexts"]["trace"] + if span_streaming: + msg_event, error_event, span = items - assert error_event["contexts"]["trace"] - assert "trace_id" in error_event["contexts"]["trace"] + assert msg_event.type == "event" + msg_event = msg_event.payload + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] - assert transaction_event["contexts"]["trace"] - assert "trace_id" in transaction_event["contexts"]["trace"] + assert error_event.type == "event" + error_event = error_event.payload + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] - assert msg_event["contexts"]["trace"]["trace_id"] == trace_id - assert error_event["contexts"]["trace"]["trace_id"] == trace_id - assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id + assert span.type == "span" + span = span.payload + assert span["trace_id"] is not None + + assert msg_event["contexts"]["trace"]["trace_id"] == trace_id + assert error_event["contexts"]["trace"]["trace_id"] == trace_id + assert span["trace_id"] == trace_id + + else: + msg_event, error_event, transaction_event = events + + assert msg_event["contexts"]["trace"] + assert "trace_id" in msg_event["contexts"]["trace"] + + assert error_event["contexts"]["trace"] + assert "trace_id" in error_event["contexts"]["trace"] + + assert transaction_event["contexts"]["trace"] + assert "trace_id" in transaction_event["contexts"]["trace"] + + assert msg_event["contexts"]["trace"]["trace_id"] == trace_id + assert error_event["contexts"]["trace"]["trace_id"] == trace_id + assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id @pytest.mark.asyncio @@ -459,10 +497,25 @@ async def test_trace_from_headers_if_performance_disabled( @pytest.mark.asyncio -async def test_websocket(sentry_init, asgi3_ws_app, capture_events, request): - sentry_init(send_default_pii=True, traces_sample_rate=1.0) - - events = capture_events() +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) +async def test_websocket( + sentry_init, + asgi3_ws_app, + capture_events, + capture_items, + request, + span_streaming, +): + sentry_init( + send_default_pii=True, + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) asgi3_ws_app = SentryAsgiMiddleware(asgi3_ws_app) @@ -470,21 +523,48 @@ async def test_websocket(sentry_init, asgi3_ws_app, capture_events, request): with pytest.raises(ValueError): client = TestClient(asgi3_ws_app) + if span_streaming: + items = capture_items("event", "span") + else: + events = capture_events() async with client.websocket_connect(request_url) as ws: await ws.receive_text() - msg_event, error_event, transaction_event = events + sentry_sdk.flush() + + if span_streaming: + msg_event, error_event, span = items + + assert msg_event.type == "event" + msg_event = msg_event.payload + assert msg_event["transaction"] == request_url + assert msg_event["transaction_info"] == {"source": "url"} + assert msg_event["message"] == "Some message to the world!" + + assert error_event.type == "event" + error_event = error_event.payload + (exc,) = error_event["exception"]["values"] + assert exc["type"] == "ValueError" + assert exc["value"] == "Oh no" + + assert span.type == "span" + span = span.payload + assert span["name"] == request_url + assert span["attributes"]["sentry.span.source"] == "url" + + else: + msg_event, error_event, transaction_event = events - assert msg_event["transaction"] == request_url - assert msg_event["transaction_info"] == {"source": "url"} - assert msg_event["message"] == "Some message to the world!" + assert msg_event["transaction"] == request_url + assert msg_event["transaction_info"] == {"source": "url"} + assert msg_event["message"] == "Some message to the world!" - (exc,) = error_event["exception"]["values"] - assert exc["type"] == "ValueError" - assert exc["value"] == "Oh no" + (exc,) = error_event["exception"]["values"] + assert exc["type"] == "ValueError" + assert exc["value"] == "Oh no" - assert transaction_event["transaction"] == request_url - assert transaction_event["transaction_info"] == {"source": "url"} + assert transaction_event["transaction"] == request_url + assert transaction_event["transaction_info"] == {"source": "url"} @pytest.mark.asyncio @@ -542,17 +622,29 @@ async def test_auto_session_tracking_with_aggregates( ), ], ) +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) @pytest.mark.asyncio async def test_transaction_style( sentry_init, asgi3_app, capture_events, + capture_items, url, transaction_style, expected_transaction, expected_source, + span_streaming, ): - sentry_init(send_default_pii=True, traces_sample_rate=1.0) + sentry_init( + send_default_pii=True, + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style) scope = { @@ -562,13 +654,26 @@ async def test_transaction_style( } async with TestClient(app, scope=scope) as client: - events = capture_events() + if span_streaming: + items = capture_items("span") + else: + events = capture_events() await client.get(url) - (transaction_event,) = events + sentry_sdk.flush() + + if span_streaming: + assert len(items) == 1 + span = items[0].payload + + assert span["name"] == expected_transaction + assert span["attributes"]["sentry.span.source"] == expected_source + + else: + (transaction_event,) = events - assert transaction_event["transaction"] == expected_transaction - assert transaction_event["transaction_info"] == {"source": expected_source} + assert transaction_event["transaction"] == expected_transaction + assert transaction_event["transaction_info"] == {"source": expected_source} def mock_asgi2_app(): @@ -733,6 +838,10 @@ def test_get_headers(): ), ], ) +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) async def test_transaction_name( sentry_init, request_url, @@ -741,28 +850,46 @@ async def test_transaction_name( expected_transaction_source, asgi3_app, capture_envelopes, + capture_items, + span_streaming, ): """ Tests that the transaction name is something meaningful. """ sentry_init( traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, ) - envelopes = capture_envelopes() - app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style) async with TestClient(app) as client: + if span_streaming: + items = capture_items("span") + else: + envelopes = capture_envelopes() await client.get(request_url) - (transaction_envelope,) = envelopes - transaction_event = transaction_envelope.get_transaction_event() + sentry_sdk.flush() + + if span_streaming: + assert len(items) == 1 + span = items[0].payload - assert transaction_event["transaction"] == expected_transaction_name - assert ( - transaction_event["transaction_info"]["source"] == expected_transaction_source - ) + assert span["name"] == expected_transaction_name + assert span["attributes"]["sentry.span.source"] == expected_transaction_source + + else: + (transaction_envelope,) = envelopes + transaction_event = transaction_envelope.get_transaction_event() + + assert transaction_event["transaction"] == expected_transaction_name + assert ( + transaction_event["transaction_info"]["source"] + == expected_transaction_source + ) @pytest.mark.asyncio @@ -783,6 +910,10 @@ async def test_transaction_name( ), ], ) +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) async def test_transaction_name_in_traces_sampler( sentry_init, request_url, @@ -790,6 +921,7 @@ async def test_transaction_name_in_traces_sampler( expected_transaction_name, expected_transaction_source, asgi3_app, + span_streaming, ): """ Tests that a custom traces_sampler has a meaningful transaction name. @@ -797,17 +929,28 @@ async def test_transaction_name_in_traces_sampler( """ def dummy_traces_sampler(sampling_context): - assert ( - sampling_context["transaction_context"]["name"] == expected_transaction_name - ) - assert ( - sampling_context["transaction_context"]["source"] - == expected_transaction_source - ) + if span_streaming: + assert sampling_context["span_context"]["name"] == expected_transaction_name + assert ( + sampling_context["span_context"]["attributes"]["sentry.span.source"] + == expected_transaction_source + ) + else: + assert ( + sampling_context["transaction_context"]["name"] + == expected_transaction_name + ) + assert ( + sampling_context["transaction_context"]["source"] + == expected_transaction_source + ) sentry_init( traces_sampler=dummy_traces_sampler, traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, ) app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style) @@ -817,17 +960,44 @@ def dummy_traces_sampler(sampling_context): @pytest.mark.asyncio +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) async def test_custom_transaction_name( - sentry_init, asgi3_custom_transaction_app, capture_events + sentry_init, + asgi3_custom_transaction_app, + capture_events, + capture_items, + span_streaming, ): - sentry_init(traces_sample_rate=1.0) - events = capture_events() + sentry_init( + traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) app = SentryAsgiMiddleware(asgi3_custom_transaction_app) async with TestClient(app) as client: + if span_streaming: + items = capture_items("span") + else: + events = capture_events() await client.get("/test") - (transaction_event,) = events - assert transaction_event["type"] == "transaction" - assert transaction_event["transaction"] == "foobar" - assert transaction_event["transaction_info"] == {"source": "custom"} + sentry_sdk.flush() + + if span_streaming: + assert len(items) == 1 + span = items[0].payload + + assert span["is_segment"] is True + assert span["name"] == "foobar" + assert span["attributes"]["sentry.span.source"] == "custom" + + else: + (transaction_event,) = events + assert transaction_event["type"] == "transaction" + assert transaction_event["transaction"] == "foobar" + assert transaction_event["transaction_info"] == {"source": "custom"} From 7fb48630b9bd462303d6d5c316540ed80039fb79 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 2 Apr 2026 18:24:14 +0200 Subject: [PATCH 13/15] . --- sentry_sdk/integrations/asgi.py | 23 +++++++++++++++++------ sentry_sdk/traces.py | 2 +- tests/conftest.py | 7 ++++++- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 3fb732e387..e8287bc55c 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -362,13 +362,24 @@ async def _sentry_wrapped_send( reraise(*exc_info) finally: - with capture_internal_exceptions(): - name, source = self._get_segment_name_and_source( - self.transaction_style, scope + if isinstance(span, StreamedSpan): + already_set = ( + span is not None + and span.name != _DEFAULT_TRANSACTION_NAME + and span.get_attributes().get("sentry.span.source") + in [ + SegmentSource.COMPONENT.value, + SegmentSource.ROUTE.value, + SegmentSource.CUSTOM.value, + ] ) - if isinstance(span, StreamedSpan): - span.name = name - span.set_attribute("sentry.span.source", source) + with capture_internal_exceptions(): + if not already_set: + name, source = self._get_segment_name_and_source( + self.transaction_style, scope + ) + span.name = name + span.set_attribute("sentry.span.source", source) finally: _asgi_middleware_applied.set(False) diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 944e17e5d7..f44ef71f5b 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -259,6 +259,7 @@ def __init__( self._name: str = name self._active: bool = active self._attributes: "Attributes" = {} + if attributes: for attribute, value in attributes.items(): self.set_attribute(attribute, value) @@ -287,7 +288,6 @@ def __init__( self._span_id: "Optional[str]" = None self._status = SpanStatus.OK.value - self.set_attribute("sentry.span.source", SegmentSource.CUSTOM.value) self._update_active_thread() diff --git a/tests/conftest.py b/tests/conftest.py index c052ddcd35..2828dbc733 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -329,7 +329,12 @@ class UnwrappedItem: @pytest.fixture def capture_items(monkeypatch): - """Capture envelope payload, unfurling individual items.""" + """ + Capture envelope payload, unfurling individual items. + + Makes it easier to work with both events and attribute-based telemetry in + one test. + """ def inner(*types): telemetry = [] From 7490fe3a0dbb2566350a75f41e9908c517a782c0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 2 Apr 2026 18:38:21 +0200 Subject: [PATCH 14/15] . --- tests/integrations/asgi/test_asgi.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index c59243fc7c..7f44c9d00a 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -863,18 +863,19 @@ async def test_transaction_name( }, ) + if span_streaming: + items = capture_items("span") + else: + envelopes = capture_envelopes() + app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style) async with TestClient(app) as client: - if span_streaming: - items = capture_items("span") - else: - envelopes = capture_envelopes() await client.get(request_url) - sentry_sdk.flush() - if span_streaming: + sentry_sdk.flush() + assert len(items) == 1 span = items[0].payload From c11e8f16d8388bbc2ede14de51f5219cc2cd47e8 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 2 Apr 2026 18:41:21 +0200 Subject: [PATCH 15/15] . --- sentry_sdk/integrations/_asgi_common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index 24d550d09a..6e9d9d2881 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -123,7 +123,9 @@ def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]": for header, value in headers.items(): attributes[f"http.request.header.{header.lower()}"] = value - attributes["http.query"] = _get_query(asgi_scope) + query = _get_query(asgi_scope) + if query: + attributes["http.query"] = query attributes["url.full"] = _get_url( asgi_scope, "http" if ty == "http" else "ws", headers.get("host")