[do not merge] feat: Span streaming & new span API #5551
13 issues
High
StreamedSpan.set_status() method does not exist - will cause AttributeError - `sentry_sdk/integrations/sqlalchemy.py:102`
The code calls span.set_status(SpanStatus.ERROR) on a StreamedSpan instance, but StreamedSpan only has a status property setter, not a set_status() method. This will raise an AttributeError at runtime when a SQL error occurs in streaming mode. The legacy Span class in tracing.py has set_status() method, but StreamedSpan in traces.py does not.
Also found at:
sentry_sdk/integrations/celery/__init__.py:104-105
NoOpStreamedSpan will crash when _get_trace_context() is called due to None _segment - `sentry_sdk/traces.py:586`
The NoOpStreamedSpan.__init__ sets self._segment = None (line 586), but the class inherits _get_trace_context() and _dynamic_sampling_context() from StreamedSpan without overriding them. When _get_trace_context() is called (e.g., from scope.get_trace_context() when a NoOpStreamedSpan is the active span), it will invoke _dynamic_sampling_context() which accesses self._segment._get_baggage(). This causes an AttributeError: 'NoneType' object has no attribute '_get_baggage' since _segment is None.
API signature mismatch causes TypeError in streaming mode - `sentry_sdk/ai/utils.py:542`
The get_start_span_function() returns sentry_sdk.traces.start_span when in streaming mode, but this function has an incompatible signature. sentry_sdk.traces.start_span(name, attributes=..., parent_span=..., active=...) requires name as the first positional argument and does not accept op or origin parameters. However, all callers (e.g., Anthropic, LiteLLM, MCP, Google GenAI integrations) invoke the returned function with (op=..., name=..., origin=...). This will raise a TypeError: start_span() got an unexpected keyword argument 'op' when span streaming is enabled.
Also found at:
sentry_sdk/integrations/anthropic.py:610-612
StreamedSpan lacks set_status() method, causing AttributeError - `sentry_sdk/integrations/sqlalchemy.py:102`
The code at line 102 calls span.set_status(SpanStatus.ERROR) when the span is a StreamedSpan. However, StreamedSpan does not have a set_status() method - it only has a status property setter. This will cause an AttributeError at runtime when SQLAlchemy encounters a database error while span streaming is enabled. Other integrations (e.g., celery, tracing_utils) correctly use span.status = SpanStatus.ERROR for StreamedSpan.
NoOpStreamedSpan._segment=None causes AttributeError when capturing events - `sentry_sdk/traces.py:586`
The NoOpStreamedSpan class sets self._segment = None at line 586 but inherits _dynamic_sampling_context() and _get_trace_context() methods from StreamedSpan that access self._segment._get_baggage() without null checks. When an error/log is captured while a NoOpStreamedSpan is active, scope.get_trace_context() calls span._get_trace_context(), which calls _dynamic_sampling_context(), resulting in AttributeError: 'NoneType' object has no attribute '_get_baggage'.
Medium
Streaming mode creates spans for HTTP methods that should be skipped - `sentry_sdk/integrations/asgi.py:238-241`
When ty == "http" but method not in self.http_methods_to_capture, the legacy path correctly skips span creation by keeping transaction = None. However, the streaming path unconditionally calls start_span() at line 238, creating spans for HTTP methods that should be excluded per http_methods_to_capture. Additionally, neither continue_trace() nor new_trace() is called in this case, which may cause trace context issues.
Also found at:
sentry_sdk/integrations/httpx.py:113-118sentry_sdk/integrations/stdlib.py:175-177
Missing exception handling causes spans to leak if Redis command fails - `sentry_sdk/integrations/redis/_async_common.py:145`
The _sentry_execute_command async function manually calls __enter__() on spans but 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 unclosed. This contrasts with the sync version in _sync_common.py which properly uses try...finally. Leaked spans may cause incorrect trace hierarchies and resource leaks.
Also found at:
sentry_sdk/integrations/redis/_sync_common.py:158
New public API method lacks test coverage - `sentry_sdk/scope.py:1348-1349`
The new set_propagation_context method is a public API added to the Scope class but has no corresponding tests. While it delegates to generate_propagation_context, tests should verify the method behaves correctly and is accessible from expected entry points.
Span streaming ignores http_methods_to_capture filter, creating spans for requests that should be skipped - `sentry_sdk/integrations/asgi.py:238-241`
In the span streaming path (lines 218-241), a span is always created via sentry_sdk.traces.start_span() at line 238, regardless of whether the HTTP method is in self.http_methods_to_capture. When ty == "http" but method not in self.http_methods_to_capture (e.g., HEAD or OPTIONS requests), the inner condition at lines 223-225 is false, but no early return or guard prevents span creation. The legacy non-streaming path correctly sets transaction = None and uses nullcontext() to skip such requests. This causes unwanted spans to be created and sent to Sentry for HTTP methods that the user explicitly configured to ignore.
StreamedSpan status always set to ERROR regardless of input status value - `sentry_sdk/integrations/celery/__init__.py:104-107`
The _set_status function ignores the status parameter for StreamedSpan and always sets SpanStatus.ERROR. When status="aborted" (used for Celery control flow exceptions like Retry, Ignore, Reject), this incorrectly marks the span as an error. Control flow exceptions are not actual errors - the legacy Span code correctly sets status to "aborted" which doesn't indicate failure, while the new code forces ERROR status, potentially inflating error metrics for normal Celery task control flow.
...and 3 more
4 skills analyzed
| Skill | Findings | Duration | Cost |
|---|---|---|---|
| code-review | 5 | 54m 19s | $21.93 |
| find-bugs | 8 | 35m 41s | $34.41 |
| skill-scanner | 0 | 15m 58s | $9.19 |
| security-review | 0 | 10m 29s | $11.95 |
Duration: 116m 27s · Tokens: 51.3M in / 519.5k out · Cost: $77.59 (+extraction: $0.06, +merge: $0.01, +fix_gate: $0.02, +dedup: $0.03)