From cab203e5059b0f7817a083a032d900e74f8057cc Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 13 Jul 2026 15:54:49 +0200 Subject: [PATCH 1/2] fix(tracing): Skip child span creation in streaming path when no current span (django) When span streaming is enabled and there is no current span, Django integrations should not create new root segments for child-span operations. This adds a guard check using `sentry_sdk.traces.get_current_span()` before creating spans in the streaming path. Co-Authored-By: Claude Opus 4.6 --- sentry_sdk/integrations/django/__init__.py | 6 ++++++ sentry_sdk/integrations/django/asgi.py | 2 ++ sentry_sdk/integrations/django/caching.py | 2 ++ sentry_sdk/integrations/django/middleware.py | 2 ++ sentry_sdk/integrations/django/signals_handlers.py | 4 +++- sentry_sdk/integrations/django/tasks.py | 2 ++ sentry_sdk/integrations/django/templates.py | 8 ++++++-- sentry_sdk/integrations/django/views.py | 4 ++++ 8 files changed, 27 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 361b60079d..2c561164e6 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -724,6 +724,8 @@ def connect(self: "BaseDatabaseWrapper") -> None: span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return real_connect(self) with sentry_sdk.traces.start_span( name="connect", attributes={ @@ -750,6 +752,8 @@ def _commit(self: "BaseDatabaseWrapper") -> None: span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return real_commit(self) with sentry_sdk.traces.start_span( name=SPANNAME.DB_COMMIT, attributes={ @@ -776,6 +780,8 @@ def _rollback(self: "BaseDatabaseWrapper") -> None: span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return real_rollback(self) with sentry_sdk.traces.start_span( name=SPANNAME.DB_ROLLBACK, attributes={ diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index 43faffb5be..785fa2af34 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -191,6 +191,8 @@ async def sentry_wrapped_callback( return await callback(request, *args, **kwargs) if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return await callback(request, *args, **kwargs) with sentry_sdk.traces.start_span( name=request.resolver_match.view_name, attributes={ diff --git a/sentry_sdk/integrations/django/caching.py b/sentry_sdk/integrations/django/caching.py index faf1803c11..2cfc0cd2e1 100644 --- a/sentry_sdk/integrations/django/caching.py +++ b/sentry_sdk/integrations/django/caching.py @@ -61,6 +61,8 @@ def _instrument_call( span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return original_method(*args, **kwargs) with sentry_sdk.traces.start_span( name=description, attributes={ diff --git a/sentry_sdk/integrations/django/middleware.py b/sentry_sdk/integrations/django/middleware.py index a14ec96ff5..01ed8962e0 100644 --- a/sentry_sdk/integrations/django/middleware.py +++ b/sentry_sdk/integrations/django/middleware.py @@ -84,6 +84,8 @@ def _check_middleware_span( span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) middleware_span: "Union[Span, StreamedSpan]" if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return None middleware_span = sentry_sdk.traces.start_span( name=description, attributes={ diff --git a/sentry_sdk/integrations/django/signals_handlers.py b/sentry_sdk/integrations/django/signals_handlers.py index 7140ead782..711e74b441 100644 --- a/sentry_sdk/integrations/django/signals_handlers.py +++ b/sentry_sdk/integrations/django/signals_handlers.py @@ -68,6 +68,8 @@ def wrapper(*args: "Any", **kwargs: "Any") -> "Any": sentry_sdk.get_client().options ) if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return receiver(*args, **kwargs) with sentry_sdk.traces.start_span( name=signal_name, attributes={ @@ -75,7 +77,7 @@ def wrapper(*args: "Any", **kwargs: "Any") -> "Any": "sentry.origin": DjangoIntegration.origin, SPANDATA.CODE_FUNCTION_NAME: signal_name, }, - ) as span: + ): return receiver(*args, **kwargs) else: with sentry_sdk.start_span( diff --git a/sentry_sdk/integrations/django/tasks.py b/sentry_sdk/integrations/django/tasks.py index 5e23c258fb..303040d042 100644 --- a/sentry_sdk/integrations/django/tasks.py +++ b/sentry_sdk/integrations/django/tasks.py @@ -35,6 +35,8 @@ def _sentry_enqueue(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return old_task_enqueue(self, *args, **kwargs) with sentry_sdk.traces.start_span( name=name, attributes={ diff --git a/sentry_sdk/integrations/django/templates.py b/sentry_sdk/integrations/django/templates.py index 5ab89d4a74..1c0986dd9e 100644 --- a/sentry_sdk/integrations/django/templates.py +++ b/sentry_sdk/integrations/django/templates.py @@ -64,13 +64,15 @@ def patch_templates() -> None: def rendered_content(self: "SimpleTemplateResponse") -> str: span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return real_rendered_content.fget(self) with sentry_sdk.traces.start_span( name=_get_template_name_description(self.template_name), attributes={ "sentry.op": OP.TEMPLATE_RENDER, "sentry.origin": DjangoIntegration.origin, }, - ) as span: + ): return real_rendered_content.fget(self) else: with sentry_sdk.start_span( @@ -109,13 +111,15 @@ def render( span_streaming = has_span_streaming_enabled(client.options) if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return real_render(request, template_name, context, *args, **kwargs) with sentry_sdk.traces.start_span( name=_get_template_name_description(template_name), attributes={ "sentry.op": OP.TEMPLATE_RENDER, "sentry.origin": DjangoIntegration.origin, }, - ) as span: + ): return real_render(request, template_name, context, *args, **kwargs) else: with sentry_sdk.start_span( diff --git a/sentry_sdk/integrations/django/views.py b/sentry_sdk/integrations/django/views.py index cf3012a75e..b24e4df45b 100644 --- a/sentry_sdk/integrations/django/views.py +++ b/sentry_sdk/integrations/django/views.py @@ -34,6 +34,8 @@ def patch_views() -> None: def sentry_patched_render(self: "SimpleTemplateResponse") -> "Any": span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return old_render(self) with sentry_sdk.traces.start_span( name="serialize response", attributes={ @@ -108,6 +110,8 @@ def sentry_wrapped_callback(request: "Any", *args: "Any", **kwargs: "Any") -> "A return callback(request, *args, **kwargs) if span_streaming: + if sentry_sdk.traces.get_current_span() is None: + return callback(request, *args, **kwargs) with sentry_sdk.traces.start_span( name=request.resolver_match.view_name, attributes={ From 07cda75cf510f607c00381e1da66c72ffe0d57f6 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 13 Jul 2026 16:59:52 +0200 Subject: [PATCH 2/2] fix(tests): Update span indices in http_method_custom tests The no-current-span guards in signal handlers now correctly prevent orphan root segments for GET requests (not in http_methods_to_capture), reducing the total span count and shifting the http.server span indices. Co-Authored-By: Claude Opus 4.6 --- tests/integrations/django/asgi/test_asgi.py | 4 ++-- tests/integrations/django/test_basic.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index 5c2389e0f6..81af3f758c 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -1027,8 +1027,8 @@ async def test_transaction_http_method_custom( sentry_sdk.flush() spans = [item.payload for item in items] - assert spans[10]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "OPTIONS" - assert spans[16]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "HEAD" + assert spans[5]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "OPTIONS" + assert spans[11]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "HEAD" else: events = capture_events() diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index 79e179b93f..cf8dd6be75 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -2370,8 +2370,8 @@ def test_transaction_http_method_custom( sentry_sdk.flush() spans = [item.payload for item in items] - assert spans[4]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "OPTIONS" - assert spans[7]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "HEAD" + assert spans[2]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "OPTIONS" + assert spans[5]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "HEAD" else: events = capture_events()