[do not merge] feat: Span streaming & new span API #5551
15 issues
High
UnboundLocalError when Redis command raises exception - `sentry_sdk/integrations/redis/_sync_common.py:158`
When old_execute_command raises an exception, the finally block executes but value is never assigned. Line 158 calls _set_cache_data(cache_span, self, cache_properties, value) which will raise UnboundLocalError: local variable 'value' referenced before assignment. This masks the original Redis exception and breaks exception propagation.
Also found at:
sentry_sdk/integrations/redis/_async_common.py:145-156
StreamedSpan.set_status() does not exist - will raise AttributeError at runtime - `sentry_sdk/integrations/sqlalchemy.py:102`
The code calls span.set_status(SpanStatus.ERROR) on line 102 when span is a StreamedSpan instance. However, StreamedSpan does not have a set_status() method - it only has a status property with a setter. This will raise an AttributeError at runtime when a SQL error occurs in span streaming mode. The fix should use span.status = SpanStatus.ERROR instead of span.set_status(SpanStatus.ERROR).
Also found at:
sentry_sdk/integrations/celery/__init__.py:104-107sentry_sdk/api.py:532-538
AttributeError when accessing _segment.set_attribute on NoOpStreamedSpan - `sentry_sdk/integrations/strawberry.py:225-226`
When span streaming is enabled but a span is unsampled or ignored, start_streamed_span returns a NoOpStreamedSpan, which inherits from StreamedSpan. However, NoOpStreamedSpan._segment is set to None. At line 226, the code checks isinstance(self.graphql_span, StreamedSpan) which matches NoOpStreamedSpan, then calls self.graphql_span._segment.set_attribute(...). This will raise AttributeError: 'NoneType' object has no attribute 'set_attribute' when the span is not sampled.
StreamedSpan.set_status() method does not exist - will raise AttributeError - `sentry_sdk/integrations/sqlalchemy.py:102`
The code calls span.set_status(SpanStatus.ERROR) on a StreamedSpan instance at line 102, but StreamedSpan does not have a set_status() method. It only has a status property setter. This will cause an AttributeError at runtime when a SQL error occurs in streaming mode. Other integrations (e.g., tracing_utils.py:1140, celery/init.py:105) correctly use span.status = SpanStatus.ERROR for StreamedSpan.
Also found at:
sentry_sdk/integrations/celery/__init__.py:105
AttributeError when using legacy Span with new scope methods - `sentry_sdk/scope.py:586-610`
The code now calls _to_traceparent(), _to_baggage(), and _get_trace_context() on self.span, but self.span can be either a StreamedSpan (which has these private methods) or a legacy Span class (which only has to_traceparent(), to_baggage(), get_trace_context() without underscore prefix). When using the legacy non-streaming mode, calling these methods will raise AttributeError: 'Span' object has no attribute '_to_traceparent'.
Medium
Span streaming mode creates spans for HTTP methods that should be skipped - `sentry_sdk/integrations/asgi.py:238-241`
In the span streaming branch, when ty == "http" but the HTTP method is not in http_methods_to_capture, neither continue_trace() nor new_trace() is called (because the inner condition at line 223-226 fails), yet start_span() is still called unconditionally at line 238. In the legacy path, transaction would remain None and no span would be created. This behavioral difference means span streaming mode will create spans for HTTP requests that should be filtered out, potentially causing unexpected telemetry data.
Also found at:
sentry_sdk/integrations/httpx.py:116-118
Duplicate timestamp initialization code in __init__ - `sentry_sdk/traces.py:280-288`
The __init__ method contains two identical code blocks that initialize _start_timestamp, _timestamp, and _start_timestamp_monotonic_ns. Lines 280-288 set these values, then lines 290-298 set them again with identical code. This appears to be a merge/rebase error. While this doesn't cause runtime errors, it unnecessarily calls datetime.now() and nanosecond_time() twice, wasting CPU cycles and potentially causing slight timing inaccuracies if there's measurable time between the two calls.
Duplicate code recording lost event and scope restoration can both execute - `sentry_sdk/traces.py:623-631`
In NoOpStreamedSpan._end(), record_lost_event() is called every time _end() is invoked, regardless of whether the span has a scope or was previously ended. This means if _end() is called multiple times (e.g., once explicitly and once via context manager __exit__), multiple lost events will be recorded for the same span. The scope detachment logic correctly checks for _previous_span_on_scope existence, but the lost event recording has no such guard.
StreamedSpan not closed on error in Anthropic integration - `sentry_sdk/integrations/anthropic.py:572-574`
When using span streaming mode, if an exception occurs during the Anthropic API call, the StreamedSpan is never closed. The isinstance(span, Span) check on line 572 only handles the legacy Span type. For StreamedSpan, _capture_exception() sets span.status = SpanStatus.ERROR, but since the type check fails, span.__exit__() is never called. This results in resource leaks and lost span data when errors occur in streaming mode.
Also found at:
sentry_sdk/integrations/anthropic.py:610-612
HTTP status code not recorded in span streaming mode - `sentry_sdk/integrations/asgi.py:289-294`
In the span streaming path, when a StreamedSpan receives an HTTP response, only a simplified 'error'/'ok' status is set (lines 290-294). The actual HTTP status code is never recorded as an attribute. In contrast, the legacy Span path calls set_http_status() which records the status code as http.response.status_code. This results in loss of HTTP status code telemetry data when using the new span streaming mode.
Also found at:
sentry_sdk/integrations/httpx.py:116-119sentry_sdk/integrations/stdlib.py:175-177
...and 5 more
4 skills analyzed
| Skill | Findings | Duration | Cost |
|---|---|---|---|
| code-review | 6 | 53m 37s | $20.88 |
| find-bugs | 9 | 29m 35s | $33.28 |
| skill-scanner | 0 | 7m 47s | $8.73 |
| security-review | 0 | 32m 45s | $11.53 |
Duration: 123m 43s · Tokens: 50.6M in / 500.8k out · Cost: $74.54 (+extraction: $0.05, +merge: $0.01, +fix_gate: $0.03, +dedup: $0.03)