[do not merge] feat: Span streaming & new span API #5317
9 issues
code-review: Found 9 issues (2 high, 4 medium, 3 low)
High
UnboundLocalError when Redis command raises an exception - `sentry_sdk/integrations/redis/_sync_common.py:148`
When old_execute_command raises an exception, the variable value is never assigned, but the finally block on line 148 attempts to use it in _set_cache_data(cache_span, self, cache_properties, value). This will raise an UnboundLocalError: local variable 'value' referenced before assignment, masking the original Redis exception and breaking error handling.
Also found at:
sentry_sdk/integrations/redis/_async_common.py:120-135
StreamedSpan created but never started - spans will be silently discarded - `sentry_sdk/integrations/strawberry.py:192-208`
When span streaming is enabled, sentry_sdk.traces.start_span() returns a StreamedSpan that requires .start() or a with block to properly initialize. In on_operation(), the span is created (line 192-194) but .start() is never called. When self.graphql_span.finish() is later called (line 234), StreamedSpan.__exit__() tries to access _context_manager_state which was never set by __enter__(). This raises an AttributeError that's silently caught by capture_internal_exceptions(), preventing the span from being sent to Sentry. Compare with graphene.py which correctly calls .start() after creating the streaming span.
Also found at:
sentry_sdk/integrations/strawberry.py:239-246sentry_sdk/integrations/strawberry.py:261-266
Medium
Spans are serialized twice: once for size estimation and once during flush - `sentry_sdk/_span_batcher.py:77-81`
The _estimate_size() method calls _to_transport_format() on every span added (unless count-based flush triggers first), and then _flush() calls _to_transport_format() again for all buffered spans. This means each span's attributes are serialized twice via serialize_attribute(), which iterates through all attribute values and creates dictionary structures. For spans with many attributes, this duplication adds unnecessary CPU overhead in a performance-sensitive code path.
StreamedSpan status always set to ERROR, ignoring aborted status for control flow exceptions - `sentry_sdk/integrations/celery/__init__.py:104-105`
The modified _set_status function always sets SpanStatus.ERROR for StreamedSpan regardless of the status argument. This causes Celery control flow exceptions (Retry, Ignore, Reject) that should be marked as "aborted" to be incorrectly marked as errors. While the new SpanStatus enum only has OK and ERROR values, hardcoding ERROR loses the semantic distinction between intentional control flow and actual errors, potentially causing misleading trace data.
NoOpStreamedSpan missing scope parameter prevents span context manager from working correctly - `sentry_sdk/scope.py:1273`
At line 1273, NoOpStreamedSpan() is created without the scope parameter, unlike lines 1237 and 1255 which pass scope=self. When _scope is None in NoOpStreamedSpan, the __enter__ method returns early without setting scope.span = self, and __exit__ won't restore the old span. This breaks context manager behavior for ignored child spans - the ignored span won't be tracked in the scope, causing inconsistent span hierarchy when nested spans are used.
Docstring references non-existent `op` parameter causing potential runtime errors - `sentry_sdk/traces.py:807`
The docstring example references @trace(op="custom") but the function signature only accepts name and attributes parameters. Users following this documentation will get a TypeError: trace() got an unexpected keyword argument 'op' at runtime. This is a backwards compatibility concern as users migrating from the old API may expect the op parameter to work.
Low
Unused import: json module is imported but never used - `sentry_sdk/_span_batcher.py:1`
The json module is imported on line 1 but is never used in this file. The json keyword appearing on lines 24, 127, and 132 are either string literals or keyword arguments to PayloadRef, not uses of the json module.
Span name 'subprocess popen' is misleading for wait operation - `sentry_sdk/integrations/stdlib.py:323`
The span name for sentry_patched_popen_wait is set to 'subprocess popen' but this function wraps Popen.wait(), not Popen.__init__(). This will make traces confusing as the wait spans will be mislabeled. The communicate function correctly uses 'subprocess communicate'.
Unused import in create_streaming_span_decorator - `sentry_sdk/tracing_utils.py:1058`
The function imports should_send_default_pii at line 1058 but never uses it. This appears to be copied from the similar create_span_decorator function which actually uses should_send_default_pii for PII handling. This is dead code that may indicate incomplete implementation - the streaming decorator may be missing PII handling logic that exists in the non-streaming version.
Duration: 26m 57s · Tokens: 14.4M in / 158.3k out · Cost: $19.03 (+extraction: $0.02, +merge: $0.00)
Annotations
Check failure on line 148 in sentry_sdk/integrations/redis/_sync_common.py
github-actions / warden: code-review
UnboundLocalError when Redis command raises an exception
When `old_execute_command` raises an exception, the variable `value` is never assigned, but the `finally` block on line 148 attempts to use it in `_set_cache_data(cache_span, self, cache_properties, value)`. This will raise an `UnboundLocalError: local variable 'value' referenced before assignment`, masking the original Redis exception and breaking error handling.
Check failure on line 135 in sentry_sdk/integrations/redis/_async_common.py
github-actions / warden: code-review
[3YJ-L2U] UnboundLocalError when Redis command raises an exception (additional location)
When `old_execute_command` raises an exception, the variable `value` is never assigned, but the `finally` block on line 148 attempts to use it in `_set_cache_data(cache_span, self, cache_properties, value)`. This will raise an `UnboundLocalError: local variable 'value' referenced before assignment`, masking the original Redis exception and breaking error handling.
Check failure on line 208 in sentry_sdk/integrations/strawberry.py
github-actions / warden: code-review
StreamedSpan created but never started - spans will be silently discarded
When span streaming is enabled, `sentry_sdk.traces.start_span()` returns a `StreamedSpan` that requires `.start()` or a `with` block to properly initialize. In `on_operation()`, the span is created (line 192-194) but `.start()` is never called. When `self.graphql_span.finish()` is later called (line 234), `StreamedSpan.__exit__()` tries to access `_context_manager_state` which was never set by `__enter__()`. This raises an `AttributeError` that's silently caught by `capture_internal_exceptions()`, preventing the span from being sent to Sentry. Compare with `graphene.py` which correctly calls `.start()` after creating the streaming span.
Check failure on line 246 in sentry_sdk/integrations/strawberry.py
github-actions / warden: code-review
[DXZ-RF5] StreamedSpan created but never started - spans will be silently discarded (additional location)
When span streaming is enabled, `sentry_sdk.traces.start_span()` returns a `StreamedSpan` that requires `.start()` or a `with` block to properly initialize. In `on_operation()`, the span is created (line 192-194) but `.start()` is never called. When `self.graphql_span.finish()` is later called (line 234), `StreamedSpan.__exit__()` tries to access `_context_manager_state` which was never set by `__enter__()`. This raises an `AttributeError` that's silently caught by `capture_internal_exceptions()`, preventing the span from being sent to Sentry. Compare with `graphene.py` which correctly calls `.start()` after creating the streaming span.
Check failure on line 266 in sentry_sdk/integrations/strawberry.py
github-actions / warden: code-review
[DXZ-RF5] StreamedSpan created but never started - spans will be silently discarded (additional location)
When span streaming is enabled, `sentry_sdk.traces.start_span()` returns a `StreamedSpan` that requires `.start()` or a `with` block to properly initialize. In `on_operation()`, the span is created (line 192-194) but `.start()` is never called. When `self.graphql_span.finish()` is later called (line 234), `StreamedSpan.__exit__()` tries to access `_context_manager_state` which was never set by `__enter__()`. This raises an `AttributeError` that's silently caught by `capture_internal_exceptions()`, preventing the span from being sent to Sentry. Compare with `graphene.py` which correctly calls `.start()` after creating the streaming span.
Check warning on line 81 in sentry_sdk/_span_batcher.py
github-actions / warden: code-review
Spans are serialized twice: once for size estimation and once during flush
The `_estimate_size()` method calls `_to_transport_format()` on every span added (unless count-based flush triggers first), and then `_flush()` calls `_to_transport_format()` again for all buffered spans. This means each span's attributes are serialized twice via `serialize_attribute()`, which iterates through all attribute values and creates dictionary structures. For spans with many attributes, this duplication adds unnecessary CPU overhead in a performance-sensitive code path.
Check warning on line 105 in sentry_sdk/integrations/celery/__init__.py
github-actions / warden: code-review
StreamedSpan status always set to ERROR, ignoring aborted status for control flow exceptions
The modified `_set_status` function always sets `SpanStatus.ERROR` for `StreamedSpan` regardless of the `status` argument. This causes Celery control flow exceptions (Retry, Ignore, Reject) that should be marked as "aborted" to be incorrectly marked as errors. While the new `SpanStatus` enum only has OK and ERROR values, hardcoding ERROR loses the semantic distinction between intentional control flow and actual errors, potentially causing misleading trace data.
Check warning on line 1273 in sentry_sdk/scope.py
github-actions / warden: code-review
NoOpStreamedSpan missing scope parameter prevents span context manager from working correctly
At line 1273, `NoOpStreamedSpan()` is created without the `scope` parameter, unlike lines 1237 and 1255 which pass `scope=self`. When `_scope` is None in NoOpStreamedSpan, the `__enter__` method returns early without setting `scope.span = self`, and `__exit__` won't restore the old span. This breaks context manager behavior for ignored child spans - the ignored span won't be tracked in the scope, causing inconsistent span hierarchy when nested spans are used.
Check warning on line 807 in sentry_sdk/traces.py
github-actions / warden: code-review
Docstring references non-existent `op` parameter causing potential runtime errors
The docstring example references `@trace(op="custom")` but the function signature only accepts `name` and `attributes` parameters. Users following this documentation will get a `TypeError: trace() got an unexpected keyword argument 'op'` at runtime. This is a backwards compatibility concern as users migrating from the old API may expect the `op` parameter to work.