Skip to content

[do not merge] feat: Span streaming & new span API #1085

[do not merge] feat: Span streaming & new span API

[do not merge] feat: Span streaming & new span API #1085

Triggered via pull request February 25, 2026 13:03
Status Success
Total duration 21s
Artifacts

changelog-preview.yml

on: pull_request_target
changelog-preview  /  preview
17s
changelog-preview / preview
Fit to window
Zoom out
Zoom in

Annotations

11 errors and 10 warnings
API incompatibility: sentry_sdk.traces.start_span doesn't accept keyword arguments used by callers: sentry_sdk/ai/utils.py#L542
When span streaming is enabled, `get_start_span_function()` returns `sentry_sdk.traces.start_span`, which has signature `start_span(name: str, attributes: Optional[Attributes] = None, parent_span: Optional[StreamedSpan] = None)`. However, all existing callers (anthropic.py, litellm.py, google_genai, langchain.py, mcp.py, openai_agents, pydantic_ai) invoke the returned function with keyword arguments `op=...`, `name=...`, `origin=...` which the new API doesn't accept. This will cause a TypeError at runtime when span streaming is enabled.
StreamedSpan.finish() fails when span not used as context manager: sentry_sdk/integrations/strawberry.py#L192
In streaming mode, spans are created via `sentry_sdk.traces.start_span()` but are not used as context managers nor explicitly started via `.start()`. When `.finish()` is called later, it invokes `__exit__()` which tries to access `self._context_manager_state` that was never set (since `__enter__` was never called). This causes an `AttributeError` that is silently caught by `capture_internal_exceptions()`, preventing the span from being properly ended and sent to Sentry.
[QG3-UL3] StreamedSpan.finish() fails when span not used as context manager (additional location): sentry_sdk/traces.py#L698
In streaming mode, spans are created via `sentry_sdk.traces.start_span()` but are not used as context managers nor explicitly started via `.start()`. When `.finish()` is called later, it invokes `__exit__()` which tries to access `self._context_manager_state` that was never set (since `__enter__` was never called). This causes an `AttributeError` that is silently caught by `capture_internal_exceptions()`, preventing the span from being properly ended and sent to Sentry.
API signature mismatch causes runtime TypeError when streaming is enabled: sentry_sdk/ai/utils.py#L542
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.
[3PL-QKR] API signature mismatch causes runtime TypeError when streaming is enabled (additional location): sentry_sdk/integrations/celery/__init__.py#L330
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.
UnboundLocalError when Redis command raises exception: sentry_sdk/integrations/redis/_sync_common.py#L148
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.
[KYX-6X4] UnboundLocalError when Redis command raises exception (additional location): sentry_sdk/integrations/redis/_async_common.py#L135
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.
StreamedSpan created but never started before finish() is called: sentry_sdk/integrations/strawberry.py#L192
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.
[UUA-HKE] StreamedSpan created but never started before finish() is called (additional location): sentry_sdk/integrations/strawberry.py#L239
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.
[UUA-HKE] StreamedSpan created but never started before finish() is called (additional location): sentry_sdk/integrations/strawberry.py#L261
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.
Type annotation `dict[str, Any]` incompatible with Python 3.6-3.8: sentry_sdk/tracing_utils.py#L478
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).
StreamedSpan not closed on error in Anthropic integration: sentry_sdk/integrations/anthropic.py#L610
When using the new span streaming mode (`_experiments={"trace_lifecycle": "stream"}`), if an exception occurs during Anthropic API calls, the StreamedSpan created in `_sentry_patched_create_common` will not be properly closed. The span is started via `span.__enter__()` but the error cleanup in the `finally` block only handles legacy `Span` objects (via `isinstance(span, Span)`), leaving `StreamedSpan` objects open. This results in spans without end timestamps and potential data loss.
[5LW-VAF] StreamedSpan not closed on error in Anthropic integration (additional location): sentry_sdk/integrations/redis/_async_common.py#L135
When using the new span streaming mode (`_experiments={"trace_lifecycle": "stream"}`), if an exception occurs during Anthropic API calls, the StreamedSpan created in `_sentry_patched_create_common` will not be properly closed. The span is started via `span.__enter__()` but the error cleanup in the `finally` block only handles legacy `Span` objects (via `isinstance(span, Span)`), leaving `StreamedSpan` objects open. This results in spans without end timestamps and potential data loss.
[5LW-VAF] StreamedSpan not closed on error in Anthropic integration (additional location): sentry_sdk/integrations/stdlib.py#L182
When using the new span streaming mode (`_experiments={"trace_lifecycle": "stream"}`), if an exception occurs during Anthropic API calls, the StreamedSpan created in `_sentry_patched_create_common` will not be properly closed. The span is started via `span.__enter__()` but the error cleanup in the `finally` block only handles legacy `Span` objects (via `isinstance(span, Span)`), leaving `StreamedSpan` objects open. This results in spans without end timestamps and potential data loss.
Control flow exceptions (Retry, Ignore, Reject) incorrectly marked as ERROR: sentry_sdk/integrations/celery/__init__.py#L105
When a Celery task raises control flow exceptions (Retry, Ignore, Reject), the `_set_status("aborted")` call sets `SpanStatus.ERROR` for StreamedSpan. However, these are not actual errors - they are expected control flow mechanisms in Celery. A Retry indicates the task will be retried, Ignore means the result is intentionally ignored, and Reject means the task was rejected. Marking these as ERROR may cause misleading error counts in monitoring dashboards.
Missing custom_sampling_context in Celery span streaming mode breaks custom samplers: sentry_sdk/integrations/celery/__init__.py#L330
In the `_wrap_tracer` function's span streaming path (lines 330-337), the `celery_job` custom sampling context containing task name, args, and kwargs is not set, unlike the legacy path which passes it to `start_transaction`. Users with custom traces_sampler functions that make decisions based on celery job information will not receive this context in span streaming mode, potentially causing unexpected sampling behavior.
UnboundLocalError when Redis command raises exception: sentry_sdk/integrations/redis/_sync_common.py#L148
In the `finally` block (lines 143-150), when `old_execute_command` raises an exception, `value` is never assigned. The subsequent call to `_set_cache_data(cache_span, self, cache_properties, value)` on line 148 references the undefined variable `value`, causing an `UnboundLocalError`. While this is caught by `capture_internal_exceptions()`, it's a regression from the old code that only called `_set_cache_data` after successful command execution.
[W8E-SPW] UnboundLocalError when Redis command raises exception (additional location): sentry_sdk/_span_batcher.py#L68
In the `finally` block (lines 143-150), when `old_execute_command` raises an exception, `value` is never assigned. The subsequent call to `_set_cache_data(cache_span, self, cache_properties, value)` on line 148 references the undefined variable `value`, causing an `UnboundLocalError`. While this is caught by `capture_internal_exceptions()`, it's a regression from the old code that only called `_set_cache_data` after successful command execution.
NoOpStreamedSpan returns inconsistent trace_id/span_id values: sentry_sdk/traces.py#L777
The `NoOpStreamedSpan.to_traceparent()` method (line 770-775) returns the real trace_id and span_id from the propagation context, but the `span_id` (line 778-779) and `trace_id` (line 782-783) properties return hardcoded `"000000"` values. This inconsistency means code accessing `.trace_id` or `.span_id` directly gets different data than code parsing `to_traceparent()`. This could cause subtle bugs in trace correlation or header propagation where different parts of the system see different trace identifiers for the same span.
Source code information lost for StreamedSpan due to modification after span.end(): sentry_sdk/integrations/stdlib.py#L183
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#L714
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.