[do not merge] feat: Span streaming & new span API #5317
11 issues
code-review: Found 11 issues (1 high, 7 medium, 3 low)
High
AttributeError when calling set_transaction_name with NoOpStreamedSpan on scope - `sentry_sdk/scope.py:829-831`
The code calls self._span.segment.set_name(name) when self._span is an instance of StreamedSpan. However, NoOpStreamedSpan is a subclass of StreamedSpan that sets self.segment = None in its constructor. When self._span is a NoOpStreamedSpan, accessing self._span.segment.set_name(name) will raise AttributeError: 'NoneType' object has no attribute 'set_name'. The code should either check isinstance(self._span, NoOpStreamedSpan) first or use a null check on segment before calling methods on it.
Also found at:
sentry_sdk/traces.py:527-528
Medium
_running_size not updated when MAX_BEFORE_FLUSH triggers flush - `sentry_sdk/_span_batcher.py:68-70`
When size + 1 >= MAX_BEFORE_FLUSH on line 68, the code returns early without updating _running_size[span.trace_id]. The span has already been added to _span_buffer (line 66), but its size is never tracked. If the flush event doesn't clear all buffers immediately (e.g., due to threading), subsequent byte-based flush decisions will underestimate the actual buffer size, potentially causing memory growth beyond the intended limit.
Also found at:
sentry_sdk/integrations/redis/utils.py:152-160
StreamedSpan status hardcoded to ERROR, ignoring actual status parameter - `sentry_sdk/integrations/celery/__init__.py:104-107`
When scope.span is a StreamedSpan, the code always sets SpanStatus.ERROR regardless of the actual status parameter passed to _set_status. This loses the distinction between "aborted" (for control flow exceptions like Retry, Ignore, Reject) and "internal_error" (for actual failures). Since StreamedSpan.set_status() accepts Union[SpanStatus, str] and handles both types correctly, the status parameter should be passed through.
Also found at:
sentry_sdk/integrations/strawberry.py:314-320
UnboundLocalError possible when span_streaming is enabled and span setup fails - `sentry_sdk/integrations/celery/__init__.py:324-337`
In _wrap_tracer, the span_ctx variable is declared with a type annotation but not initialized. In the span_streaming branch, if start_span() succeeds but any subsequent setter method (set_origin, set_source, set_op) throws an exception, the exception is suppressed by capture_internal_exceptions() but span_ctx is never assigned. Since transaction is not None, the code proceeds to with span_ctx: (line 365), causing an UnboundLocalError that crashes the Celery task.
Missing test coverage for httpx integration with span streaming mode - `sentry_sdk/integrations/httpx.py:64-67`
The httpx integration now supports both legacy tracing and the new span streaming mode (trace_lifecycle: stream), but the existing tests in tests/integrations/httpx/test_httpx.py only test the legacy mode using start_transaction(). The new StreamedSpan code paths (lines 64-67, 80-88, 145-148, 161-168) are not covered by any integration tests, meaning bugs in span attribute setting or the streaming workflow won't be caught.
Spans leak when Redis command raises exception - `sentry_sdk/integrations/redis/_async_common.py:135`
In _sentry_execute_command, spans are opened via __enter__() but closed via __exit__(None, None, None) without a try/finally block. If await old_execute_command() raises an exception, both db_span and cache_span will never have __exit__ called, leaving them unclosed. This can cause memory leaks and incorrect tracing data (spans that are never finished).
Also found at:
sentry_sdk/integrations/redis/_sync_common.py:135-146
Missing parent_span in on_parse causes orphaned span in streaming mode - `sentry_sdk/integrations/strawberry.py:261-262`
In on_parse() for streaming mode (line 261-262), sentry_sdk.traces.start_span() is called without the parent_span=self.graphql_span argument, unlike on_validate() (line 239-240) which correctly passes it. This inconsistency means the parsing span will be parented to whatever span is currently active on the scope rather than explicitly to self.graphql_span, potentially causing incorrect span hierarchy in the trace.
Scope resolved but not used when capturing span - `sentry_sdk/traces.py:438`
In _end(), the scope variable is carefully resolved on lines 397-399 to use the passed scope, self._scope, or the current scope as fallback. However, on line 438, sentry_sdk.get_current_scope()._capture_span(self) ignores this resolved scope and fetches a fresh current scope instead. If the current scope has changed between entering and ending the span (e.g., in async contexts), this could cause the span to be captured with incorrect scope data.
Low
Unused import: json module imported but never used - `sentry_sdk/_span_batcher.py:1`
The json module is imported at line 1 but is not used anywhere in the file. All JSON serialization is done via PayloadRef(json=...) which doesn't require the json import. This adds unnecessary overhead during module loading.
Missing name check allows empty transaction names in baggage - `sentry_sdk/tracing_utils.py:816-820`
The new populate_from_segment method does not check if segment._name is truthy before setting sentry_items["transaction"], unlike the legacy populate_from_transaction which checks transaction.name first. If a segment has an empty string name, it will be included in baggage headers, whereas the legacy code would omit it. This is a subtle backwards compatibility issue that could affect downstream services parsing baggage.
Unused import of should_send_default_pii in create_streaming_span_decorator - `sentry_sdk/tracing_utils.py:1058`
The function create_streaming_span_decorator imports should_send_default_pii from sentry_sdk.scope at line 1058 but never uses it. This appears to be copy-paste from the original create_span_decorator function where it is used. While this doesn't cause runtime errors, it adds unnecessary overhead to each decorator invocation and indicates incomplete implementation - the decorator may be missing PII filtering that should be applied.
Duration: 32m 58s · Tokens: 13.2M in / 149.2k out · Cost: $16.35 (+extraction: $0.01, +merge: $0.01)
Annotations
Check failure on line 831 in sentry_sdk/scope.py
github-actions / warden: code-review
AttributeError when calling set_transaction_name with NoOpStreamedSpan on scope
The code calls `self._span.segment.set_name(name)` when `self._span` is an instance of `StreamedSpan`. However, `NoOpStreamedSpan` is a subclass of `StreamedSpan` that sets `self.segment = None` in its constructor. When `self._span` is a `NoOpStreamedSpan`, accessing `self._span.segment.set_name(name)` will raise `AttributeError: 'NoneType' object has no attribute 'set_name'`. The code should either check `isinstance(self._span, NoOpStreamedSpan)` first or use a null check on `segment` before calling methods on it.
Check failure on line 528 in sentry_sdk/traces.py
github-actions / warden: code-review
[VTF-C8W] AttributeError when calling set_transaction_name with NoOpStreamedSpan on scope (additional location)
The code calls `self._span.segment.set_name(name)` when `self._span` is an instance of `StreamedSpan`. However, `NoOpStreamedSpan` is a subclass of `StreamedSpan` that sets `self.segment = None` in its constructor. When `self._span` is a `NoOpStreamedSpan`, accessing `self._span.segment.set_name(name)` will raise `AttributeError: 'NoneType' object has no attribute 'set_name'`. The code should either check `isinstance(self._span, NoOpStreamedSpan)` first or use a null check on `segment` before calling methods on it.
Check warning on line 70 in sentry_sdk/_span_batcher.py
github-actions / warden: code-review
_running_size not updated when MAX_BEFORE_FLUSH triggers flush
When `size + 1 >= MAX_BEFORE_FLUSH` on line 68, the code returns early without updating `_running_size[span.trace_id]`. The span has already been added to `_span_buffer` (line 66), but its size is never tracked. If the flush event doesn't clear all buffers immediately (e.g., due to threading), subsequent byte-based flush decisions will underestimate the actual buffer size, potentially causing memory growth beyond the intended limit.
Check warning on line 160 in sentry_sdk/integrations/redis/utils.py
github-actions / warden: code-review
[ZA4-GL3] _running_size not updated when MAX_BEFORE_FLUSH triggers flush (additional location)
When `size + 1 >= MAX_BEFORE_FLUSH` on line 68, the code returns early without updating `_running_size[span.trace_id]`. The span has already been added to `_span_buffer` (line 66), but its size is never tracked. If the flush event doesn't clear all buffers immediately (e.g., due to threading), subsequent byte-based flush decisions will underestimate the actual buffer size, potentially causing memory growth beyond the intended limit.
Check warning on line 107 in sentry_sdk/integrations/celery/__init__.py
github-actions / warden: code-review
StreamedSpan status hardcoded to ERROR, ignoring actual status parameter
When `scope.span` is a `StreamedSpan`, the code always sets `SpanStatus.ERROR` regardless of the actual `status` parameter passed to `_set_status`. This loses the distinction between "aborted" (for control flow exceptions like Retry, Ignore, Reject) and "internal_error" (for actual failures). Since `StreamedSpan.set_status()` accepts `Union[SpanStatus, str]` and handles both types correctly, the status parameter should be passed through.
Check warning on line 320 in sentry_sdk/integrations/strawberry.py
github-actions / warden: code-review
[399-3R6] StreamedSpan status hardcoded to ERROR, ignoring actual status parameter (additional location)
When `scope.span` is a `StreamedSpan`, the code always sets `SpanStatus.ERROR` regardless of the actual `status` parameter passed to `_set_status`. This loses the distinction between "aborted" (for control flow exceptions like Retry, Ignore, Reject) and "internal_error" (for actual failures). Since `StreamedSpan.set_status()` accepts `Union[SpanStatus, str]` and handles both types correctly, the status parameter should be passed through.
Check warning on line 337 in sentry_sdk/integrations/celery/__init__.py
github-actions / warden: code-review
UnboundLocalError possible when span_streaming is enabled and span setup fails
In `_wrap_tracer`, the `span_ctx` variable is declared with a type annotation but not initialized. In the span_streaming branch, if `start_span()` succeeds but any subsequent setter method (`set_origin`, `set_source`, `set_op`) throws an exception, the exception is suppressed by `capture_internal_exceptions()` but `span_ctx` is never assigned. Since `transaction` is not None, the code proceeds to `with span_ctx:` (line 365), causing an `UnboundLocalError` that crashes the Celery task.
Check warning on line 67 in sentry_sdk/integrations/httpx.py
github-actions / warden: code-review
Missing test coverage for httpx integration with span streaming mode
The httpx integration now supports both legacy tracing and the new span streaming mode (`trace_lifecycle: stream`), but the existing tests in `tests/integrations/httpx/test_httpx.py` only test the legacy mode using `start_transaction()`. The new StreamedSpan code paths (lines 64-67, 80-88, 145-148, 161-168) are not covered by any integration tests, meaning bugs in span attribute setting or the streaming workflow won't be caught.
Check warning on line 135 in sentry_sdk/integrations/redis/_async_common.py
github-actions / warden: code-review
Spans leak when Redis command raises exception
In `_sentry_execute_command`, spans are opened via `__enter__()` but closed via `__exit__(None, None, None)` without a try/finally block. If `await old_execute_command()` raises an exception, both `db_span` and `cache_span` will never have `__exit__` called, leaving them unclosed. This can cause memory leaks and incorrect tracing data (spans that are never finished).
Check warning on line 146 in sentry_sdk/integrations/redis/_sync_common.py
github-actions / warden: code-review
[DU7-VFT] Spans leak when Redis command raises exception (additional location)
In `_sentry_execute_command`, spans are opened via `__enter__()` but closed via `__exit__(None, None, None)` without a try/finally block. If `await old_execute_command()` raises an exception, both `db_span` and `cache_span` will never have `__exit__` called, leaving them unclosed. This can cause memory leaks and incorrect tracing data (spans that are never finished).
Check warning on line 262 in sentry_sdk/integrations/strawberry.py
github-actions / warden: code-review
Missing parent_span in on_parse causes orphaned span in streaming mode
In `on_parse()` for streaming mode (line 261-262), `sentry_sdk.traces.start_span()` is called without the `parent_span=self.graphql_span` argument, unlike `on_validate()` (line 239-240) which correctly passes it. This inconsistency means the parsing span will be parented to whatever span is currently active on the scope rather than explicitly to `self.graphql_span`, potentially causing incorrect span hierarchy in the trace.
Check warning on line 438 in sentry_sdk/traces.py
github-actions / warden: code-review
Scope resolved but not used when capturing span
In `_end()`, the `scope` variable is carefully resolved on lines 397-399 to use the passed scope, `self._scope`, or the current scope as fallback. However, on line 438, `sentry_sdk.get_current_scope()._capture_span(self)` ignores this resolved scope and fetches a fresh current scope instead. If the current scope has changed between entering and ending the span (e.g., in async contexts), this could cause the span to be captured with incorrect scope data.