[do not merge] feat: Span streaming & new span API #5317
8 issues
find-bugs: Found 8 issues (1 high, 5 medium, 2 low)
High
StreamedSpan is only imported for type checking but used at runtime - `sentry_sdk/integrations/graphene.py:167`
StreamedSpan is imported under TYPE_CHECKING (line 30) but is used in an isinstance() check at runtime (line 167). When span streaming mode is enabled, executing a GraphQL query will raise NameError: name 'StreamedSpan' is not defined when the code reaches the finally block.
Also found at:
sentry_sdk/integrations/anthropic.py:572-574
Medium
Byte size tracking skipped when count-based flush threshold is reached - `sentry_sdk/_span_batcher.py:68-70`
When size + 1 >= self.MAX_BEFORE_FLUSH (line 68), the span is added to _span_buffer at line 66 but the function returns at line 70 without updating _running_size. This means the byte size of that span is never tracked. If the flush thread is delayed or another span arrives before the flush completes, the byte-size tracking will undercount the actual buffer size, potentially allowing more data to accumulate than intended before a byte-based flush.
Also found at:
sentry_sdk/integrations/celery/__init__.py:330-337
Async httpx client has inconsistent baggage handling that can cause duplicate sentry items - `sentry_sdk/integrations/httpx.py:186-192`
The async httpx client uses inline baggage handling (lines 186-192) that differs from the sync client's add_sentry_baggage_to_headers() helper. The async implementation appends baggage with += without first stripping existing sentry items, while the sync client properly strips them. This can cause duplicate sentry baggage items when outgoing requests already have sentry-prefixed baggage, potentially causing parsing issues or exceeding header size limits on downstream services.
Spans not properly closed on exception in async Redis execute_command - `sentry_sdk/integrations/redis/_async_common.py:120-146`
The _sentry_execute_command function manually calls __enter__() and __exit__() on db_span and cache_span, but if await old_execute_command() raises an exception, neither span's __exit__ method is called. This causes span leakage (spans never sent), scope corruption (the old span is never restored via scope.span = old_span in __exit__), and memory leaks. The fix is to wrap the command execution in a try/finally block that ensures both spans are properly exited.
Also found at:
sentry_sdk/integrations/redis/_sync_common.py:135
Async resolve method in StreamedSpan path missing set_op() and set_origin() calls - `sentry_sdk/integrations/strawberry.py:314-321`
The async resolve method for StreamedSpan (lines 314-321) does not call set_op(OP.GRAPHQL_RESOLVE) or set_origin(StrawberryIntegration.origin), while the sync resolve method (lines 352-362) correctly calls both. This inconsistency means async GraphQL resolver spans in streaming mode will be missing operation type and origin metadata, causing data loss in tracing.
Computed scope variable is unused in _end(), causing incorrect scope to be used for span capture - `sentry_sdk/traces.py:438-439`
In _end(), lines 398-400 compute a scope variable from the passed parameter, self._scope, or sentry_sdk.get_current_scope(). However, line 439 ignores this computed scope and directly calls sentry_sdk.get_current_scope()._capture_span(self). This means if the span was associated with a different scope (e.g., passed via the scope parameter from __exit__), the span will be captured on the wrong scope, potentially causing incorrect event association or data loss.
Low
redis.is_cluster attribute not set when name is empty - `sentry_sdk/integrations/redis/utils.py:152-160`
In _set_client_data, the redis.is_cluster attribute is only set when name is truthy (inside the if name: block on line 152). Comparing with the original code shown in the diff, span.set_tag("redis.is_cluster", is_cluster) was called unconditionally before the refactoring. This means if name is empty or falsy, the redis.is_cluster attribute will not be set on the span, which is inconsistent with _set_pipeline_data (lines 115-120) where is_cluster is set unconditionally.
Also found at:
sentry_sdk/integrations/stdlib.py:323
NoOpStreamedSpan created without scope loses context manager functionality - `sentry_sdk/scope.py:1273`
At line 1273, NoOpStreamedSpan() is created without passing scope=self, unlike line 1255 which correctly passes scope=self. When a NoOpStreamedSpan is used as a context manager without a scope, it doesn't set itself as the current span on the scope (early return in __enter__), and doesn't restore the old span in __exit__. This leads to inconsistent behavior between ignored segment spans and ignored child spans.
Duration: 35m 50s · Tokens: 20.9M in / 177.5k out · Cost: $28.72 (+extraction: $0.02, +merge: $0.00)
Annotations
Check failure on line 167 in sentry_sdk/integrations/graphene.py
github-actions / warden: find-bugs
StreamedSpan is only imported for type checking but used at runtime
`StreamedSpan` is imported under `TYPE_CHECKING` (line 30) but is used in an `isinstance()` check at runtime (line 167). When span streaming mode is enabled, executing a GraphQL query will raise `NameError: name 'StreamedSpan' is not defined` when the code reaches the `finally` block.
Check failure on line 574 in sentry_sdk/integrations/anthropic.py
github-actions / warden: find-bugs
[UBH-6BM] StreamedSpan is only imported for type checking but used at runtime (additional location)
`StreamedSpan` is imported under `TYPE_CHECKING` (line 30) but is used in an `isinstance()` check at runtime (line 167). When span streaming mode is enabled, executing a GraphQL query will raise `NameError: name 'StreamedSpan' is not defined` when the code reaches the `finally` block.
Check warning on line 70 in sentry_sdk/_span_batcher.py
github-actions / warden: find-bugs
Byte size tracking skipped when count-based flush threshold is reached
When `size + 1 >= self.MAX_BEFORE_FLUSH` (line 68), the span is added to `_span_buffer` at line 66 but the function returns at line 70 without updating `_running_size`. This means the byte size of that span is never tracked. If the flush thread is delayed or another span arrives before the flush completes, the byte-size tracking will undercount the actual buffer size, potentially allowing more data to accumulate than intended before a byte-based flush.
Check warning on line 337 in sentry_sdk/integrations/celery/__init__.py
github-actions / warden: find-bugs
[UM4-KSX] Byte size tracking skipped when count-based flush threshold is reached (additional location)
When `size + 1 >= self.MAX_BEFORE_FLUSH` (line 68), the span is added to `_span_buffer` at line 66 but the function returns at line 70 without updating `_running_size`. This means the byte size of that span is never tracked. If the flush thread is delayed or another span arrives before the flush completes, the byte-size tracking will undercount the actual buffer size, potentially allowing more data to accumulate than intended before a byte-based flush.
Check warning on line 192 in sentry_sdk/integrations/httpx.py
github-actions / warden: find-bugs
Async httpx client has inconsistent baggage handling that can cause duplicate sentry items
The async httpx client uses inline baggage handling (lines 186-192) that differs from the sync client's `add_sentry_baggage_to_headers()` helper. The async implementation appends baggage with `+=` without first stripping existing sentry items, while the sync client properly strips them. This can cause duplicate sentry baggage items when outgoing requests already have sentry-prefixed baggage, potentially causing parsing issues or exceeding header size limits on downstream services.
Check warning on line 146 in sentry_sdk/integrations/redis/_async_common.py
github-actions / warden: find-bugs
Spans not properly closed on exception in async Redis execute_command
The `_sentry_execute_command` function manually calls `__enter__()` and `__exit__()` on `db_span` and `cache_span`, but if `await old_execute_command()` raises an exception, neither span's `__exit__` method is called. This causes span leakage (spans never sent), scope corruption (the old span is never restored via `scope.span = old_span` in `__exit__`), and memory leaks. The fix is to wrap the command execution in a try/finally block that ensures both spans are properly exited.
Check warning on line 135 in sentry_sdk/integrations/redis/_sync_common.py
github-actions / warden: find-bugs
[9AQ-VD8] Spans not properly closed on exception in async Redis execute_command (additional location)
The `_sentry_execute_command` function manually calls `__enter__()` and `__exit__()` on `db_span` and `cache_span`, but if `await old_execute_command()` raises an exception, neither span's `__exit__` method is called. This causes span leakage (spans never sent), scope corruption (the old span is never restored via `scope.span = old_span` in `__exit__`), and memory leaks. The fix is to wrap the command execution in a try/finally block that ensures both spans are properly exited.
Check warning on line 321 in sentry_sdk/integrations/strawberry.py
github-actions / warden: find-bugs
Async resolve method in StreamedSpan path missing set_op() and set_origin() calls
The async `resolve` method for StreamedSpan (lines 314-321) does not call `set_op(OP.GRAPHQL_RESOLVE)` or `set_origin(StrawberryIntegration.origin)`, while the sync `resolve` method (lines 352-362) correctly calls both. This inconsistency means async GraphQL resolver spans in streaming mode will be missing operation type and origin metadata, causing data loss in tracing.
Check warning on line 439 in sentry_sdk/traces.py
github-actions / warden: find-bugs
Computed scope variable is unused in _end(), causing incorrect scope to be used for span capture
In `_end()`, lines 398-400 compute a `scope` variable from the passed parameter, `self._scope`, or `sentry_sdk.get_current_scope()`. However, line 439 ignores this computed scope and directly calls `sentry_sdk.get_current_scope()._capture_span(self)`. This means if the span was associated with a different scope (e.g., passed via the `scope` parameter from `__exit__`), the span will be captured on the wrong scope, potentially causing incorrect event association or data loss.