[do not merge] feat: Span streaming & new span API #5317
8 issues
code-review: Found 8 issues (4 high, 2 medium, 2 low)
High
API signature mismatch causes runtime TypeError when streaming is enabled - `sentry_sdk/ai/utils.py:542`
The get_start_span_function() returns sentry_sdk.traces.start_span when streaming mode is enabled, but this function only accepts name, attributes, and parent_span parameters. All callers (in litellm, anthropic, google_genai, langchain, mcp, pydantic_ai, openai_agents integrations) pass additional kwargs like op=... and origin=... which will cause TypeError: start_span() got an unexpected keyword argument 'op'. This breaks all AI integrations when span streaming is enabled.
Also found at:
sentry_sdk/integrations/celery/__init__.py:330-337
UnboundLocalError when Redis command raises exception - `sentry_sdk/integrations/redis/_sync_common.py:148`
In the finally block (line 143-150), the code references value when calling _set_cache_data(cache_span, self, cache_properties, value) on line 148. However, if old_execute_command on line 142 raises an exception, value is never assigned, causing an UnboundLocalError. This will crash the Redis instrumentation when any Redis operation fails, breaking error handling.
Also found at:
sentry_sdk/integrations/redis/_async_common.py:135-146
StreamedSpan created but never started before finish() is called - `sentry_sdk/integrations/strawberry.py:192-208`
The graphql_span is created via sentry_sdk.traces.start_span() in span streaming mode (line 192) but is never started before yield. When self.graphql_span.finish() is called at line 234, it will trigger __exit__ which accesses self._context_manager_state - an attribute that is only set by __enter__/start(). This will cause an AttributeError: 'StreamedSpan' object has no attribute '_context_manager_state' at runtime when span streaming is enabled.
Also found at:
sentry_sdk/integrations/strawberry.py:239-246sentry_sdk/integrations/strawberry.py:261-266
Type annotation `dict[str, Any]` incompatible with Python 3.6-3.8 - `sentry_sdk/tracing_utils.py:478`
The type annotation dict[str, Any] uses PEP 585 syntax which is only available in Python 3.9+. However, the SDK supports Python >= 3.6 (as declared in setup.py). On Python 3.6-3.8, this will cause a TypeError at runtime when the annotation is evaluated. The rest of the codebase consistently uses Dict[str, Any] from the typing module (e.g., lines 384, 453, 536 in this file).
Medium
Source code information lost for StreamedSpan due to modification after span.end() - `sentry_sdk/integrations/stdlib.py:183-189`
In streaming mode, span.end() captures and queues the span for sending via scope._capture_span(self) before returning. The add_http_request_source() call on line 189 happens after span.end() completes, so any attributes it sets (source code information like code.lineno, code.filepath, etc.) will not be included in the sent span data. This is different from the legacy path where spans are only sent when the transaction finishes.
NoOpStreamedSpan records lost event even when span was never active - `sentry_sdk/traces.py:714-718`
In NoOpStreamedSpan.__exit__, the lost event is recorded via transport.record_lost_event() (lines 714-718) before checking if self._scope is None (line 720). When a NoOpStreamedSpan is created with scope=None, the __enter__ method returns early without doing anything, but __exit__ will still record a lost event. This leads to incorrect metrics, as spans that were never 'started' (because they had no scope) will be counted as lost.
Low
Unused json import adds unnecessary dependency - `sentry_sdk/_span_batcher.py:1`
The json module is imported on line 1 but never used anywhere in the file. The json={...} on line 132 is a parameter name for PayloadRef, not a use of the json module. This is dead code that should be removed.
Span captured before _finished flag is set, allowing duplicate span capture - `sentry_sdk/traces.py:441-444`
In _end(), the span is captured via scope._capture_span(self) on line 442 before self._finished = True is set on line 444. If _capture_span triggers any code that could call _end() again (e.g., through callbacks or async operations), the span could be captured multiple times before the _finished flag prevents it. The _finished check should happen at the start of _end() and be set before capture to ensure proper idempotency.
Duration: 34m 26s · Tokens: 14.1M in / 161.1k out · Cost: $18.82 (+extraction: $0.02, +merge: $0.00)
Annotations
Check failure on line 542 in sentry_sdk/ai/utils.py
github-actions / warden: code-review
API signature mismatch causes runtime TypeError when streaming is enabled
The `get_start_span_function()` returns `sentry_sdk.traces.start_span` when streaming mode is enabled, but this function only accepts `name`, `attributes`, and `parent_span` parameters. All callers (in litellm, anthropic, google_genai, langchain, mcp, pydantic_ai, openai_agents integrations) pass additional kwargs like `op=...` and `origin=...` which will cause `TypeError: start_span() got an unexpected keyword argument 'op'`. This breaks all AI integrations when span streaming is enabled.
Check failure on line 337 in sentry_sdk/integrations/celery/__init__.py
github-actions / warden: code-review
[3PL-QKR] API signature mismatch causes runtime TypeError when streaming is enabled (additional location)
The `get_start_span_function()` returns `sentry_sdk.traces.start_span` when streaming mode is enabled, but this function only accepts `name`, `attributes`, and `parent_span` parameters. All callers (in litellm, anthropic, google_genai, langchain, mcp, pydantic_ai, openai_agents integrations) pass additional kwargs like `op=...` and `origin=...` which will cause `TypeError: start_span() got an unexpected keyword argument 'op'`. This breaks all AI integrations when span streaming is enabled.
Check failure on line 148 in sentry_sdk/integrations/redis/_sync_common.py
github-actions / warden: code-review
UnboundLocalError when Redis command raises exception
In the `finally` block (line 143-150), the code references `value` when calling `_set_cache_data(cache_span, self, cache_properties, value)` on line 148. However, if `old_execute_command` on line 142 raises an exception, `value` is never assigned, causing an `UnboundLocalError`. This will crash the Redis instrumentation when any Redis operation fails, breaking error handling.
Check failure on line 146 in sentry_sdk/integrations/redis/_async_common.py
github-actions / warden: code-review
[KYX-6X4] UnboundLocalError when Redis command raises exception (additional location)
In the `finally` block (line 143-150), the code references `value` when calling `_set_cache_data(cache_span, self, cache_properties, value)` on line 148. However, if `old_execute_command` on line 142 raises an exception, `value` is never assigned, causing an `UnboundLocalError`. This will crash the Redis instrumentation when any Redis operation fails, breaking error handling.
Check failure on line 208 in sentry_sdk/integrations/strawberry.py
github-actions / warden: code-review
StreamedSpan created but never started before finish() is called
The `graphql_span` is created via `sentry_sdk.traces.start_span()` in span streaming mode (line 192) but is never started before `yield`. When `self.graphql_span.finish()` is called at line 234, it will trigger `__exit__` which accesses `self._context_manager_state` - an attribute that is only set by `__enter__`/`start()`. This will cause an `AttributeError: 'StreamedSpan' object has no attribute '_context_manager_state'` at runtime when span streaming is enabled.
Check failure on line 246 in sentry_sdk/integrations/strawberry.py
github-actions / warden: code-review
[UUA-HKE] StreamedSpan created but never started before finish() is called (additional location)
The `graphql_span` is created via `sentry_sdk.traces.start_span()` in span streaming mode (line 192) but is never started before `yield`. When `self.graphql_span.finish()` is called at line 234, it will trigger `__exit__` which accesses `self._context_manager_state` - an attribute that is only set by `__enter__`/`start()`. This will cause an `AttributeError: 'StreamedSpan' object has no attribute '_context_manager_state'` at runtime when span streaming is enabled.
Check failure on line 266 in sentry_sdk/integrations/strawberry.py
github-actions / warden: code-review
[UUA-HKE] StreamedSpan created but never started before finish() is called (additional location)
The `graphql_span` is created via `sentry_sdk.traces.start_span()` in span streaming mode (line 192) but is never started before `yield`. When `self.graphql_span.finish()` is called at line 234, it will trigger `__exit__` which accesses `self._context_manager_state` - an attribute that is only set by `__enter__`/`start()`. This will cause an `AttributeError: 'StreamedSpan' object has no attribute '_context_manager_state'` at runtime when span streaming is enabled.
Check failure on line 478 in sentry_sdk/tracing_utils.py
github-actions / warden: code-review
Type annotation `dict[str, Any]` incompatible with Python 3.6-3.8
The type annotation `dict[str, Any]` uses PEP 585 syntax which is only available in Python 3.9+. However, the SDK supports Python >= 3.6 (as declared in setup.py). On Python 3.6-3.8, this will cause a `TypeError` at runtime when the annotation is evaluated. The rest of the codebase consistently uses `Dict[str, Any]` from the `typing` module (e.g., lines 384, 453, 536 in this file).
Check warning on line 189 in sentry_sdk/integrations/stdlib.py
github-actions / warden: code-review
Source code information lost for StreamedSpan due to modification after span.end()
In streaming mode, `span.end()` captures and queues the span for sending via `scope._capture_span(self)` before returning. The `add_http_request_source()` call on line 189 happens after `span.end()` completes, so any attributes it sets (source code information like `code.lineno`, `code.filepath`, etc.) will not be included in the sent span data. This is different from the legacy path where spans are only sent when the transaction finishes.
Check warning on line 718 in sentry_sdk/traces.py
github-actions / warden: code-review
NoOpStreamedSpan records lost event even when span was never active
In `NoOpStreamedSpan.__exit__`, the lost event is recorded via `transport.record_lost_event()` (lines 714-718) before checking if `self._scope is None` (line 720). When a `NoOpStreamedSpan` is created with `scope=None`, the `__enter__` method returns early without doing anything, but `__exit__` will still record a lost event. This leads to incorrect metrics, as spans that were never 'started' (because they had no scope) will be counted as lost.