[do not merge] feat: Span streaming & new span API #5551
8 issues
find-bugs: Found 8 issues (3 high, 5 medium)
High
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
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.
StreamedSpan missing HTTP status code attribute in httpx integration - `sentry_sdk/integrations/httpx.py:116-118`
When using span streaming mode, the httpx integration sets span status and reason but does not set the http.response.status_code attribute. The legacy span path calls span.set_http_status(rv.status_code) which sets SPANDATA.HTTP_STATUS_CODE. This causes data inconsistency between streaming and non-streaming modes, and breadcrumb creation logic in tracing_utils.py may not work correctly since it checks for this attribute.
Also found at:
sentry_sdk/integrations/httpx.py:199-201sentry_sdk/integrations/stdlib.py:176-177
Spans not closed on exception in async Redis client - `sentry_sdk/integrations/redis/_async_common.py:145-147`
In _sentry_execute_command, if old_execute_command raises an exception, both db_span.__exit__() and cache_span.__exit__() are never called. This causes spans to remain open indefinitely, leading to span leaks and incorrect timing data. The sync version in _sync_common.py correctly uses try/finally to ensure spans are always closed. This affects all async Redis operations when an exception occurs.
Redis spans not marked as errors when commands fail - `sentry_sdk/integrations/redis/_sync_common.py:153-160`
When old_execute_command raises an exception, the finally block calls db_span.__exit__(None, None, None) and cache_span.__exit__(None, None, None) with no exception info. Both StreamedSpan.__exit__ and Span.__exit__ check if value is not None to set error status. Since None is always passed, spans won't be marked with error status when Redis operations fail, leading to incorrect telemetry data showing successful spans for failed operations.
Duration: 35m 41s · Tokens: 23.9M in / 201.1k out · Cost: $34.46 (+extraction: $0.04, +merge: $0.00, +fix_gate: $0.01)
Annotations
Check failure on line 542 in sentry_sdk/ai/utils.py
sentry-warden / warden: find-bugs
API signature mismatch causes TypeError in streaming mode
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.
Check failure on line 612 in sentry_sdk/integrations/anthropic.py
sentry-warden / warden: find-bugs
[QQ5-H5F] API signature mismatch causes TypeError in streaming mode (additional location)
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.
Check failure on line 102 in sentry_sdk/integrations/sqlalchemy.py
sentry-warden / warden: find-bugs
StreamedSpan lacks set_status() method, causing AttributeError
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.
Check failure on line 586 in sentry_sdk/traces.py
sentry-warden / warden: find-bugs
NoOpStreamedSpan._segment=None causes AttributeError when capturing events
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'`.
Check warning on line 241 in sentry_sdk/integrations/asgi.py
sentry-warden / warden: find-bugs
Span streaming ignores http_methods_to_capture filter, creating spans for requests that should be skipped
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.
Check warning on line 107 in sentry_sdk/integrations/celery/__init__.py
sentry-warden / warden: find-bugs
StreamedSpan status always set to ERROR regardless of input status value
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.
Check warning on line 118 in sentry_sdk/integrations/httpx.py
sentry-warden / warden: find-bugs
StreamedSpan missing HTTP status code attribute in httpx integration
When using span streaming mode, the httpx integration sets span status and reason but does not set the `http.response.status_code` attribute. The legacy span path calls `span.set_http_status(rv.status_code)` which sets `SPANDATA.HTTP_STATUS_CODE`. This causes data inconsistency between streaming and non-streaming modes, and breadcrumb creation logic in `tracing_utils.py` may not work correctly since it checks for this attribute.
Check warning on line 201 in sentry_sdk/integrations/httpx.py
sentry-warden / warden: find-bugs
[FGV-K7L] StreamedSpan missing HTTP status code attribute in httpx integration (additional location)
When using span streaming mode, the httpx integration sets span status and reason but does not set the `http.response.status_code` attribute. The legacy span path calls `span.set_http_status(rv.status_code)` which sets `SPANDATA.HTTP_STATUS_CODE`. This causes data inconsistency between streaming and non-streaming modes, and breadcrumb creation logic in `tracing_utils.py` may not work correctly since it checks for this attribute.
Check warning on line 177 in sentry_sdk/integrations/stdlib.py
sentry-warden / warden: find-bugs
[FGV-K7L] StreamedSpan missing HTTP status code attribute in httpx integration (additional location)
When using span streaming mode, the httpx integration sets span status and reason but does not set the `http.response.status_code` attribute. The legacy span path calls `span.set_http_status(rv.status_code)` which sets `SPANDATA.HTTP_STATUS_CODE`. This causes data inconsistency between streaming and non-streaming modes, and breadcrumb creation logic in `tracing_utils.py` may not work correctly since it checks for this attribute.
Check warning on line 147 in sentry_sdk/integrations/redis/_async_common.py
sentry-warden / warden: find-bugs
Spans not closed on exception in async Redis client
In `_sentry_execute_command`, if `old_execute_command` raises an exception, both `db_span.__exit__()` and `cache_span.__exit__()` are never called. This causes spans to remain open indefinitely, leading to span leaks and incorrect timing data. The sync version in `_sync_common.py` correctly uses try/finally to ensure spans are always closed. This affects all async Redis operations when an exception occurs.
Check warning on line 160 in sentry_sdk/integrations/redis/_sync_common.py
sentry-warden / warden: find-bugs
Redis spans not marked as errors when commands fail
When `old_execute_command` raises an exception, the `finally` block calls `db_span.__exit__(None, None, None)` and `cache_span.__exit__(None, None, None)` with no exception info. Both `StreamedSpan.__exit__` and `Span.__exit__` check if `value is not None` to set error status. Since `None` is always passed, spans won't be marked with error status when Redis operations fail, leading to incorrect telemetry data showing successful spans for failed operations.