From 2bf6634a8676f68d7a6921cfe4c0a4947008e3cb Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 16 Mar 2026 16:12:38 +0100 Subject: [PATCH 1/4] feat: Make ASGI support span first --- sentry_sdk/integrations/asgi.py | 101 +++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 27 deletions(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 6983af89ed..d6d1e1ed38 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -22,10 +22,13 @@ nullcontext, ) from sentry_sdk.sessions import track_session +from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import ( SOURCE_FOR_STYLE, + Transaction, TransactionSource, ) +from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( ContextVar, event_from_exception, @@ -35,17 +38,19 @@ transaction_from_function, _get_installed_modules, ) -from sentry_sdk.tracing import Transaction from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any + from typing import ContextManager from typing import Dict from typing import Optional from typing import Tuple + from typing import Union - from sentry_sdk._types import Event, Hint + from sentry_sdk._types import Attributes, Event, Hint + from sentry_sdk.tracing import Span _asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") @@ -185,6 +190,9 @@ async def _run_app( self._capture_lifespan_exception(exc) raise exc from None + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) + _asgi_middleware_applied.set(True) try: with sentry_sdk.isolation_scope() as sentry_scope: @@ -204,48 +212,87 @@ async def _run_app( ) method = scope.get("method", "").upper() - transaction = None - if ty in ("http", "websocket"): - if ty == "websocket" or method in self.http_methods_to_capture: - transaction = continue_trace( - _get_headers(scope), - op="{}.server".format(ty), + + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" + if span_streaming: + segment: "Optional[StreamedSpan]" = None + attributes: "Attributes" = {} + sentry_scope.set_custom_sampling_context({"asgi_scope": scope}) + if ty in ("http", "websocket"): + if ( + ty == "websocket" + or method in self.http_methods_to_capture + ): + sentry_sdk.traces.continue_trace(_get_headers(scope)) + attributes["sentry.op"] = f"{ty}.server" + else: + sentry_sdk.traces.new_trace() + attributes["sentry.op"] = OP.HTTP_SERVER + + attributes["sentry.span.source"] = getattr( + transaction_source, "value", transaction_source + ) + attributes["sentry.origin"] = self.span_origin + attributes["asgi.type"] = ty + segment = sentry_sdk.traces.start_span( + name=transaction_name, attributes=attributes + ) + span_ctx = segment or nullcontext() + + else: + transaction = None + if ty in ("http", "websocket"): + if ( + ty == "websocket" + or method in self.http_methods_to_capture + ): + transaction = continue_trace( + _get_headers(scope), + op="{}.server".format(ty), + name=transaction_name, + source=transaction_source, + origin=self.span_origin, + ) + else: + transaction = Transaction( + op=OP.HTTP_SERVER, name=transaction_name, source=transaction_source, origin=self.span_origin, ) - else: - transaction = Transaction( - op=OP.HTTP_SERVER, - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) - if transaction: - transaction.set_tag("asgi.type", ty) + if transaction: + transaction.set_tag("asgi.type", ty) - transaction_context = ( - sentry_sdk.start_transaction( - transaction, - custom_sampling_context={"asgi_scope": scope}, + span_ctx = ( + sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"asgi_scope": scope}, + ) + if transaction is not None + else nullcontext() ) - if transaction is not None - else nullcontext() - ) - with transaction_context: + + with span_ctx as span: try: async def _sentry_wrapped_send( event: "Dict[str, Any]", ) -> "Any": - if transaction is not None: + if span is not None: is_http_response = ( event.get("type") == "http.response.start" and "status" in event ) if is_http_response: - transaction.set_http_status(event["status"]) + if isinstance(span, StreamedSpan): + span.status = ( + "error" + if event["status"] >= 400 + else "ok" + ) + else: + span.set_http_status(event["status"]) return await send(event) From a4dbe50776c5667228a3375eca515236634c4c52 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 16 Mar 2026 16:28:00 +0100 Subject: [PATCH 2/4] . --- sentry_sdk/integrations/asgi.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index d6d1e1ed38..1644d7632f 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -291,6 +291,10 @@ async def _sentry_wrapped_send( if event["status"] >= 400 else "ok" ) + span.set_attribute( + "http.response.status_code", + event["status"], + ) else: span.set_http_status(event["status"]) From 9328e2458fc2d2e6c7b07ac87e65b29111d0786a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 18 Mar 2026 15:08:53 +0100 Subject: [PATCH 3/4] . --- sentry_sdk/integrations/asgi.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 1644d7632f..cfdec78676 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -216,8 +216,15 @@ async def _run_app( span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" if span_streaming: segment: "Optional[StreamedSpan]" = None - attributes: "Attributes" = {} + attributes: "Attributes" = { + "sentry.span.source": getattr( + transaction_source, "value", transaction_source + ), + "sentry.origin": self.span_origin, + "asgi.type": ty, + } sentry_scope.set_custom_sampling_context({"asgi_scope": scope}) + if ty in ("http", "websocket"): if ( ty == "websocket" @@ -225,18 +232,16 @@ async def _run_app( ): sentry_sdk.traces.continue_trace(_get_headers(scope)) attributes["sentry.op"] = f"{ty}.server" + segment = sentry_sdk.traces.start_span( + name=transaction_name, attributes=attributes + ) else: sentry_sdk.traces.new_trace() attributes["sentry.op"] = OP.HTTP_SERVER + segment = sentry_sdk.traces.start_span( + name=transaction_name, attributes=attributes + ) - attributes["sentry.span.source"] = getattr( - transaction_source, "value", transaction_source - ) - attributes["sentry.origin"] = self.span_origin - attributes["asgi.type"] = ty - segment = sentry_sdk.traces.start_span( - name=transaction_name, attributes=attributes - ) span_ctx = segment or nullcontext() else: From 200fb994070a61f86b28b5cd3c5fbc6b26d7ae61 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 19 Mar 2026 10:10:23 +0100 Subject: [PATCH 4/4] . --- sentry_sdk/integrations/asgi.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index cfdec78676..da87f8b2fb 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -223,7 +223,6 @@ async def _run_app( "sentry.origin": self.span_origin, "asgi.type": ty, } - sentry_scope.set_custom_sampling_context({"asgi_scope": scope}) if ty in ("http", "websocket"): if ( @@ -231,12 +230,22 @@ async def _run_app( or method in self.http_methods_to_capture ): sentry_sdk.traces.continue_trace(_get_headers(scope)) + + sentry_scope.set_custom_sampling_context( + {"asgi_scope": scope} + ) + attributes["sentry.op"] = f"{ty}.server" segment = sentry_sdk.traces.start_span( name=transaction_name, attributes=attributes ) else: sentry_sdk.traces.new_trace() + + sentry_scope.set_custom_sampling_context( + {"asgi_scope": scope} + ) + attributes["sentry.op"] = OP.HTTP_SERVER segment = sentry_sdk.traces.start_span( name=transaction_name, attributes=attributes