From bdb6501c98e4569d0ca5fb04ccafbc180ad85e7f Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 11 Aug 2026 08:58:54 +0200 Subject: [PATCH 1/9] feat: Set segment name on scope when span is NoOpStreamedSpan --- sentry_sdk/scope.py | 2 +- sentry_sdk/traces.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 09c580defa..33de972910 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -925,7 +925,7 @@ def streamed_span(self, span: "Optional[StreamedSpan]") -> None: # Also set _transaction and _transaction_info in streaming mode as this # is used for populating events and linking them to segments - if type(span) is StreamedSpan and span._is_segment(): + if isinstance(span, StreamedSpan) and span._is_segment(): self._transaction = span.name if span._attributes.get("sentry.segment.name.source"): self._transaction_info["source"] = str( diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index d8251a9101..5f3201525e 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -625,6 +625,7 @@ def _to_json(self) -> "SpanJSON": class NoOpStreamedSpan(StreamedSpan): __slots__ = ( + "_name", "_sampled", "_finished", "_unsampled_reason", @@ -632,6 +633,7 @@ class NoOpStreamedSpan(StreamedSpan): def __init__( self, + name: "str", segment: "Optional[StreamedSpan]" = None, trace_id: "Optional[str]" = None, parent_span_id: "Optional[str]" = None, @@ -643,6 +645,8 @@ def __init__( sample_rand: "Optional[float]" = None, sample_rate: "Optional[float]" = None, ) -> None: + self._name = name + self._span_id: "Optional[str]" = None self._sampled = sampled From e6dcbd5d960267be225287b0858e228f7f0b6af7 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 11 Aug 2026 09:01:22 +0200 Subject: [PATCH 2/9] provide argument --- sentry_sdk/scope.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 33de972910..836e4701e6 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1329,6 +1329,7 @@ def start_streamed_span( if sampled is False or sampled is None: return NoOpStreamedSpan( + name=name, scope=self, segment=None, trace_id=propagation_context.trace_id, @@ -1359,6 +1360,7 @@ def start_streamed_span( with new_scope(): if is_ignored_span(name, attributes): return NoOpStreamedSpan( + name=name, segment=parent_span._segment, trace_id=parent_span.trace_id, parent_span_id=parent_span.span_id, @@ -1368,6 +1370,7 @@ def start_streamed_span( if isinstance(parent_span, NoOpStreamedSpan): return NoOpStreamedSpan( + name=name, segment=parent_span._segment, trace_id=parent_span.trace_id, parent_span_id=parent_span.span_id, From 9fa191ff11dc66bf6aa9c085157f241694475f03 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 11 Aug 2026 09:05:11 +0200 Subject: [PATCH 3/9] . --- sentry_sdk/scope.py | 4 +++- sentry_sdk/traces.py | 10 +++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 836e4701e6..5145f28e18 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -926,7 +926,8 @@ def streamed_span(self, span: "Optional[StreamedSpan]") -> None: # Also set _transaction and _transaction_info in streaming mode as this # is used for populating events and linking them to segments if isinstance(span, StreamedSpan) and span._is_segment(): - self._transaction = span.name + if span.name is not None: + self._transaction = span.name if span._attributes.get("sentry.segment.name.source"): self._transaction_info["source"] = str( span._attributes["sentry.segment.name.source"] @@ -1309,6 +1310,7 @@ def start_streamed_span( if is_ignored_span(name, attributes): return NoOpStreamedSpan( + name=name, scope=self, segment=None, trace_id=propagation_context.trace_id, diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 5f3201525e..421e04c281 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -633,7 +633,7 @@ class NoOpStreamedSpan(StreamedSpan): def __init__( self, - name: "str", + name: "Optional[str]" = None, segment: "Optional[StreamedSpan]" = None, trace_id: "Optional[str]" = None, parent_span_id: "Optional[str]" = None, @@ -742,12 +742,8 @@ def status(self, status: "Union[SpanStatus, str]") -> None: pass @property - def name(self) -> str: - return "" - - @name.setter - def name(self, value: str) -> None: - pass + def name(self) -> "Optional[str]": + return self._name @property def active(self) -> bool: From b940d77c7eb51725f0f225531c54e17fae296b1e Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 11 Aug 2026 09:11:37 +0200 Subject: [PATCH 4/9] gate _attributes access --- sentry_sdk/scope.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 5145f28e18..77fa378940 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -928,7 +928,9 @@ def streamed_span(self, span: "Optional[StreamedSpan]") -> None: if isinstance(span, StreamedSpan) and span._is_segment(): if span.name is not None: self._transaction = span.name - if span._attributes.get("sentry.segment.name.source"): + if type(span) is StreamedSpan and span._attributes.get( + "sentry.segment.name.source" + ): self._transaction_info["source"] = str( span._attributes["sentry.segment.name.source"] ) From bd0f9299236d50b0535c8dbb2a1e086c79650cab Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 11 Aug 2026 09:20:24 +0200 Subject: [PATCH 5/9] . --- sentry_sdk/scope.py | 16 ++++++++++------ sentry_sdk/traces.py | 12 ++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 77fa378940..ea7a19862f 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -925,15 +925,19 @@ def streamed_span(self, span: "Optional[StreamedSpan]") -> None: # Also set _transaction and _transaction_info in streaming mode as this # is used for populating events and linking them to segments - if isinstance(span, StreamedSpan) and span._is_segment(): - if span.name is not None: - self._transaction = span.name - if type(span) is StreamedSpan and span._attributes.get( - "sentry.segment.name.source" - ): + if not isinstance(span, StreamedSpan) or not span._is_segment(): + return + + if type(span) is StreamedSpan: + self._transaction = span.name + if span._attributes.get("sentry.segment.name.source"): self._transaction_info["source"] = str( span._attributes["sentry.segment.name.source"] ) + return + + if type(span) is NoOpStreamedSpan and span._noop_name is not None: + self._transaction = span.name @property def profile(self) -> "Optional[Profile]": diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 421e04c281..1d22e3a7bf 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -625,7 +625,7 @@ def _to_json(self) -> "SpanJSON": class NoOpStreamedSpan(StreamedSpan): __slots__ = ( - "_name", + "_noop_name", "_sampled", "_finished", "_unsampled_reason", @@ -645,7 +645,7 @@ def __init__( sample_rand: "Optional[float]" = None, sample_rate: "Optional[float]" = None, ) -> None: - self._name = name + self._noop_name = name self._span_id: "Optional[str]" = None @@ -742,8 +742,12 @@ def status(self, status: "Union[SpanStatus, str]") -> None: pass @property - def name(self) -> "Optional[str]": - return self._name + def name(self) -> str: + return self._noop_name or "" + + @name.setter + def name(self, name: str) -> None: + self._noop_name = name @property def active(self) -> bool: From a675dda08d4631f235b94ef50995930db7879b59 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 11 Aug 2026 09:23:50 +0200 Subject: [PATCH 6/9] add test --- tests/integrations/django/test_basic.py | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index 93b5477010..03e62fe6ec 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -1293,6 +1293,56 @@ def test_transaction_style( assert event["transaction"] == expected_transaction +@pytest.mark.parametrize( + "transaction_style,client_url,expected_transaction,expected_source,expected_response", + [ + ( + "function_name", + "/message", + "tests.integrations.django.myapp.views.message", + "component", + b"ok", + ), + ("url", "/message", "/message", "route", b"ok"), + ("url", "/404", "/404", "url", b"404"), + ], +) +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_transaction_style_tracing_disabled( + sentry_init, + client, + capture_events, + capture_items, + transaction_style, + client_url, + expected_transaction, + expected_source, + expected_response, + span_streaming, +): + sentry_init( + integrations=[DjangoIntegration(transaction_style=transaction_style)], + send_default_pii=True, + trace_lifecycle="stream" if span_streaming else "static", + ) + if span_streaming: + items = capture_items("event") + + content, status, headers = unpack_werkzeug_response(client.get(client_url)) + assert content == expected_response + + (event,) = (item.payload for item in items if item.type == "event") + else: + events = capture_events() + + content, status, headers = unpack_werkzeug_response(client.get(client_url)) + assert content == expected_response + + (event,) = events + + assert event["transaction"] == expected_transaction + + @pytest.mark.parametrize("span_streaming", [True, False]) def test_request_body( sentry_init, From 00f076ca97f1feb6f3ef25d902b9abefc38bfb44 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 11 Aug 2026 09:31:18 +0200 Subject: [PATCH 7/9] . --- tests/tracing/test_span_streaming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index 3800a4badc..81cbeec7fb 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -799,7 +799,7 @@ def test_continue_trace_unsampled(sentry_init, capture_items): ... assert span.sampled is False - assert span.name == "" + assert span.name == "segment" assert span.trace_id == trace_id assert span.span_id != "0000000000000000" From 63e764a07c56e016e360c1d8576b99b338fd4d6f Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 11 Aug 2026 10:00:16 +0200 Subject: [PATCH 8/9] add integration test --- tests/tracing/test_integration_tests.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/tracing/test_integration_tests.py b/tests/tracing/test_integration_tests.py index 8bc380b8b6..b293d36134 100644 --- a/tests/tracing/test_integration_tests.py +++ b/tests/tracing/test_integration_tests.py @@ -91,6 +91,21 @@ def test_basic_span_streaming(sentry_init, capture_items, sample_rate): assert not items +def test_error_event_linked_without_performance_span_streaming( + sentry_init, capture_items +): + sentry_init(traces_sample_rate=None, trace_lifecycle="stream") + items = capture_items("event") + + with sentry_sdk.traces.start_span(name="no-op span"): + sentry_sdk.capture_message("hi") + + sentry_sdk.flush() + + (event,) = (item.payload for item in items) + assert event["transaction"] == "no-op span" + + @pytest.mark.parametrize("parent_sampled", [True, False, None]) @pytest.mark.parametrize("sample_rate", [0.0, 1.0]) def test_continue_trace(sentry_init, capture_envelopes, parent_sampled, sample_rate): From f4986d9731b1846fc428a3885d66264cb6fde63a Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 11 Aug 2026 13:46:11 +0200 Subject: [PATCH 9/9] re-use parent _name field --- sentry_sdk/scope.py | 2 +- sentry_sdk/traces.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index ea7a19862f..035afaf82b 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -936,7 +936,7 @@ def streamed_span(self, span: "Optional[StreamedSpan]") -> None: ) return - if type(span) is NoOpStreamedSpan and span._noop_name is not None: + if type(span) is NoOpStreamedSpan and span._name is not None: self._transaction = span.name @property diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 1d22e3a7bf..904a40bbc4 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -625,7 +625,6 @@ def _to_json(self) -> "SpanJSON": class NoOpStreamedSpan(StreamedSpan): __slots__ = ( - "_noop_name", "_sampled", "_finished", "_unsampled_reason", @@ -645,7 +644,7 @@ def __init__( sample_rand: "Optional[float]" = None, sample_rate: "Optional[float]" = None, ) -> None: - self._noop_name = name + self._name = name # type: ignore[assignment] self._span_id: "Optional[str]" = None @@ -743,11 +742,11 @@ def status(self, status: "Union[SpanStatus, str]") -> None: @property def name(self) -> str: - return self._noop_name or "" + return self._name or "" @name.setter def name(self, name: str) -> None: - self._noop_name = name + self._name = name @property def active(self) -> bool: