[do not merge] feat: Span streaming & new span API #5317
15 issues
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
StreamedSpan never started/activated in on_operation, on_validate, and on_parse methods - `sentry_sdk/integrations/strawberry.py:191-208`
When span streaming is enabled, sentry_sdk.traces.start_span() creates a StreamedSpan but does NOT automatically activate it. The span must be explicitly started via .start() or used as a context manager (with span:). In on_operation(), on_validate(), and on_parse(), the created StreamedSpan objects are never started/entered, causing: (1) spans to never be set as the current scope's span, (2) sampling decisions to never be made for segment spans, (3) child spans to not properly inherit from parent spans. Compare with graphene.py line 158 which correctly calls _graphql_span.start() after creating the span.
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.
StreamedSpan instances with INTERNAL_ERROR status are never cleaned up - `sentry_sdk/integrations/anthropic.py:572-574`
The code checks isinstance(span, Span) before calling span.__exit__() for error cleanup. However, when streaming mode is enabled (_experiments={"trace_lifecycle": "stream"}), get_start_span_function() returns a function that creates StreamedSpan instances instead of Span. Since StreamedSpan is not a subclass of Span, this check will always be False for streamed spans, causing the error cleanup code to be silently skipped. This may lead to spans with INTERNAL_ERROR status not being properly closed when using streaming mode.
Also found at:
sentry_sdk/integrations/anthropic.py:610-612
Spans not closed on exception in async Redis execute_command - `sentry_sdk/integrations/redis/_async_common.py:135-137`
The _sentry_execute_command function in the async Redis client uses manual __enter__() and __exit__() calls for spans, but unlike the sync version in _sync_common.py, it lacks a try/finally block around await old_execute_command(). If the Redis command raises an exception, db_span.__exit__() and cache_span.__exit__() are never called, leaving spans open. This causes span leakage and incorrect tracing data. The sync version correctly uses try/finally to ensure spans are always closed.
Also found at:
sentry_sdk/integrations/redis/_sync_common.py:143-150
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.
...and 5 more
4 skills analyzed
| Skill | Findings | Duration | Cost |
|---|---|---|---|
| code-review | 9 | 26m 57s | $19.01 |
| find-bugs | 6 | 39m 8s | $30.92 |
| skill-scanner | 0 | 43m 44s | $7.93 |
| security-review | 0 | 8m 51s | $11.49 |
Duration: 118m 40s · Tokens: 46.0M in / 455.6k out · Cost: $69.41 (+extraction: $0.05, +merge: $0.01, +dedup: $0.00)