Help us improve
Share bugs, ideas, or general feedback.
From sentry
Migrates Python SDK from transaction-based to span-first trace lifecycle for Sentry span streaming. Use when asked to enable span streaming or switch to streamed span delivery.
npx claudepluginhub getsentry/sentry-for-ai --plugin sentryHow this skill is triggered — by the user, by Claude, or both
Slash command
/sentry:sentry-span-streaming-pythonThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
> [All Skills](../../SKILL_TREE.md) > [Feature Setup](../sentry-feature-setup/SKILL.md) > Span Streaming (Python)
Migrate JavaScript Sentry SDKs from transaction-based to span streaming (span-first trace lifecycle) for lower latency per-span delivery.
Configures Sentry SDK v8+ for performance monitoring, distributed tracing, sampling, and profiling in Node.js/TypeScript and Python apps.
OpenTelemetry tracing discipline: correct spans, propagation, and semantic conventions produce useful traces. Invoke whenever task involves any interaction with distributed tracing — span creation, context propagation, instrumentation, sampling configuration, or OpenTelemetry SDK setup.
Share bugs, ideas, or general feedback.
All Skills > Feature Setup > Span Streaming (Python)
Migrate from the default transaction-based trace lifecycle (static) to span streaming (stream), where spans are sent individually as they complete instead of being batched into a transaction at the end.
This skill covers the Python SDK. For JavaScript, see Span Streaming (JavaScript).
trace_lifecycle, sentry_sdk.traces, or the new start_span API# Find sentry_sdk.init calls
grep -rn "sentry_sdk\.init\|sentry_sdk\.init(" --include="*.py" -l 2>/dev/null | head -20
# Find existing start_span / start_transaction / start_child usage
grep -rn "start_span\|start_transaction\|start_child" --include="*.py" -l 2>/dev/null | head -20
# Find trace decorator usage
grep -rn "@trace\|from sentry_sdk import trace\|from sentry_sdk.tracing import trace" --include="*.py" -l 2>/dev/null | head -20
# Find continue_trace usage
grep -rn "continue_trace" --include="*.py" -l 2>/dev/null | head -20
# Find before_send_transaction usage
grep -rn "before_send_transaction" --include="*.py" -l 2>/dev/null | head -20
# Find direct Span / Transaction class imports
grep -rn "from sentry_sdk.tracing import\|from sentry_sdk import.*Span\|from sentry_sdk import.*Transaction" --include="*.py" -l 2>/dev/null | head -20
# Find set_data / set_tag / set_context on spans
grep -rn "set_data\|set_tag\|set_context" --include="*.py" -l 2>/dev/null | head -20
# Find scope.span / scope.transaction / containing_transaction access
grep -rn "scope\.span\|scope\.transaction\|containing_transaction" --include="*.py" -l 2>/dev/null | head -20
# Find get_trace_context usage
grep -rn "get_trace_context" --include="*.py" -l 2>/dev/null | head -20
After detecting the environment, assess how many files need changes. If the codebase has many files to migrate (e.g. dozens of files with start_span, start_transaction, set_data, etc.), launch subagents to handle independent migration tasks in parallel — for example, one subagent per migration category (span creation, span data, trace propagation) or per module/package. Each subagent should receive the relevant migration rules from this skill and operate on a distinct set of files.
Prerequisites: sentry-sdk >=2.62.0 with tracing enabled (traces_sample_rate or traces_sampler configured).
Add trace_lifecycle to _experiments in ALL occurences of sentry_sdk.init():
import sentry_sdk
# Before
sentry_sdk.init(
dsn="...",
traces_sample_rate=1.0,
)
# After
sentry_sdk.init(
dsn="...",
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream",
},
)
Span streaming requires using the new sentry_sdk.traces.start_span API. The legacy sentry_sdk.start_span and sentry_sdk.start_transaction APIs will not stream spans.
start_spanReplace sentry_sdk.start_span() with sentry_sdk.traces.start_span():
import sentry_sdk
# Before
with sentry_sdk.start_span(name="flow.checkout") as span:
...
# After
with sentry_sdk.traces.start_span(name="flow.checkout") as span:
...
Or change the import:
# Before
from sentry_sdk import start_span
# After
from sentry_sdk.traces import start_span
The new API accepts:
name (required)attributes — key-value pairs (see Migrate Span Data below)parent_span — explicit parent span; defaults to the currently active spanactive — defaults to True; if False, the span won't become other spans' parent automaticallyThe description argument no longer exists — use name instead. The op argument is no longer supported — use the sentry.op attribute instead:
# Before
with sentry_sdk.start_span(op="http.client", description="GET /api/users") as span:
...
# After
with sentry_sdk.traces.start_span(
name="GET /api/users",
attributes={"sentry.op": "http.client"},
) as span:
...
start_transactionReplace sentry_sdk.start_transaction() with sentry_sdk.traces.start_span():
import sentry_sdk
# Before
with sentry_sdk.start_transaction(name="flow.checkout") as transaction:
...
# After
with sentry_sdk.traces.start_span(name="flow.checkout", parent_span=None) as span:
...
Setting parent_span=None forces the span to become a root span, which is the equivalent of starting a transaction in the legacy API.
start_childspan.start_child() no longer exists. Start a new span while the parent is active — it becomes a child automatically:
import sentry_sdk
# Before
with sentry_sdk.start_span(name="outer") as parent:
with parent.start_child(op="db", description="SELECT") as child:
...
# After
with sentry_sdk.traces.start_span(name="outer") as parent:
with sentry_sdk.traces.start_span(
name="SELECT",
attributes={"sentry.op": "db"},
):
...
To control parentage explicitly (e.g. make a span a sibling rather than a child), use parent_span:
with sentry_sdk.traces.start_span(name="outer") as span:
with sentry_sdk.traces.start_span(name="child 1"):
with sentry_sdk.traces.start_span(name="child 2", parent_span=span):
# "child 2" is a sibling of "child 1", not its child
...
@trace DecoratorReplace sentry_sdk.trace with sentry_sdk.traces.trace:
# Before
from sentry_sdk import trace
@trace
def checkout():
...
# After — just change the import
from sentry_sdk.traces import trace
@trace
def checkout():
...
The new decorator also accepts optional name (defaults to the function name), attributes, and active arguments:
from sentry_sdk.traces import trace
@trace(name="checkout", attributes={"flow.pipeline": "legacy"})
def checkout():
...
get_current_spanReplace sentry_sdk.get_current_span() with sentry_sdk.traces.get_current_span():
# Before
from sentry_sdk import get_current_span
span = get_current_span()
# After
from sentry_sdk.traces import get_current_span
span = get_current_span()
scope.span and scope.transactionIf the code accesses the current span or transaction through the scope object, migrate to the new APIs:
import sentry_sdk
# Before
scope = sentry_sdk.get_current_scope()
current_span = scope.span
# After
current_span = sentry_sdk.traces.get_current_span()
import sentry_sdk
# Before
scope = sentry_sdk.get_current_scope()
transaction = scope.transaction
# After
root_span = sentry_sdk.traces.get_current_span()._segment
_segment returns the root span of the current trace (the equivalent of what used to be the transaction). It is a private API — prefer restructuring the code to avoid needing the root span where possible.
span.containing_transactionspan.containing_transaction no longer exists. Use span._segment to get the root span:
# Before
transaction = span.containing_transaction
# After
root_span = span._segment
Span and Transaction ClassesIf the code imports Span or Transaction directly (e.g. for type annotations), replace both with StreamedSpan:
# Before
from sentry_sdk.tracing import Span, Transaction
def process(span: Span) -> None:
...
# After
from sentry_sdk.traces import StreamedSpan
def process(span: StreamedSpan) -> None:
...
get_trace_contextspan.get_trace_context() no longer exists on streaming spans. Migrate based on what you actually need from the trace context:
If the code only reads specific fields (like trace_id, span_id, or parent_span_id), access them directly as span properties:
# Before
ctx = span.get_trace_context()
trace_id = ctx["trace_id"]
span_id = ctx["span_id"]
# After
trace_id = span.trace_id
span_id = span.span_id
If the code genuinely needs the full trace context dict (e.g. to pass it to an external system or serialize it), use the private method span._get_trace_context():
# Before
ctx = span.get_trace_context()
# After
ctx = span._get_trace_context()
Prefer the direct property access where possible — _get_trace_context() is a private API and may change.
In span streaming mode, spans have no contexts, data, or tags. Everything is a span attribute. Attribute keys are strings; values must be int, bool, str, float, or an array of these types. None is not supported.
Important: Unlike the old set_data / set_tag APIs, set_attribute only supports primitive types. Non-primitive values must be either stringified or broken down into multiple primitive-typed attributes:
# Before — set_data accepted any type
span.set_data("request", {"method": "POST", "path": "/api/checkout"})
span.set_data("response_headers", response.headers)
# After — flatten dicts into separate attributes
span.set_attributes({
"request.method": "POST",
"request.path": "/api/checkout",
})
# After — stringify objects that can't be flattened
span.set_attribute("response_headers", str(response.headers))
set_data# Before
span.set_data("flow.step", "submit_payment")
# After
span.set_attribute("flow.step", "submit_payment")
set_tag# Before
span.set_tag("http.status_code", 201)
# After
span.set_attribute("http.response.status_code", 201)
When migrating multiple consecutive set_data / set_tag calls, combine them into a single set_attributes() call:
# Before
span.set_data("flow.step", "submit_payment")
span.set_data("flow.version", "0.35")
span.set_tag("http.status_code", 201)
# After
span.set_attributes({
"flow.step": "submit_payment",
"flow.version": "0.35",
"http.response.status_code": 201,
})
set_contextDictionaries are not supported as attribute values. Flatten them into separate attributes:
# Before
span.set_context("flow", {"id": "123456789", "pipeline": "legacy"})
# After
span.set_attributes({"flow.id": "123456789", "flow.pipeline": "legacy"})
Tags set on the scope with sentry_sdk.set_tag() are not applied to streaming spans. Use sentry_sdk.set_attribute() to apply data to spans:
import sentry_sdk
sentry_sdk.set_tag("region", "Europe") # applied to errors and other tag-supporting telemetry
sentry_sdk.set_attribute("region", "Europe") # applied to spans, logs, metrics
with sentry_sdk.traces.start_span(name="flow.checkout") as span:
span.set_attribute("flow.version", "0.35")
span.set_attributes({"flow.conversion": 1.0, "flow.use_new_pipeline": True})
span.remove_attribute("flow.conversion")
Status can only be ok (default) or error:
from sentry_sdk.traces import start_span
with start_span(name="process") as span:
try:
...
except Exception:
span.status = "error"
sentry_sdk.traces.continue_trace() replaces the legacy sentry_sdk.continue_trace(). It is no longer a context manager — it sets the propagation context, and the next span picks it up automatically:
import sentry_sdk
headers = {
"sentry-trace": "4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1",
"baggage": "sentry-trace_id=...",
}
# Before
with sentry_sdk.continue_trace(headers) as transaction:
...
# After
sentry_sdk.traces.continue_trace(headers)
with sentry_sdk.traces.start_span(name="handle request"):
...
To start a completely new trace, use sentry_sdk.traces.new_trace():
import sentry_sdk
with sentry_sdk.traces.start_span(name="span in trace 1"):
...
sentry_sdk.traces.new_trace()
with sentry_sdk.traces.start_span(name="span in trace 2"):
# This span is the root of a new, separate trace
...
If you use traces_sample_rate, no changes are needed — it works the same way.
If you use a custom traces_sampler, the sampling context has a different structure in streaming mode:
def traces_sampler(sampling_context):
# sampling_context["span_context"] contains:
# name, trace_id, parent_span_id, parent_sampled, attributes
if sampling_context["span_context"]["name"] in IGNORED_SPAN_NAMES:
return 0.0
return 1.0
sentry_sdk.init(
traces_sampler=traces_sampler,
_experiments={"trace_lifecycle": "stream"},
)
The sampling decision is made when start_span() is called. Child spans inherit the parent's sampling decision unless filtered by ignore_spans.
Custom sampling context is no longer an argument to start_span. Set it on the scope before starting the span:
import sentry_sdk
def traces_sampler(sampling_context):
if sampling_context["asgi_scope"].method not in ("GET", "POST"):
return 0.0
return 1.0
# Before
with sentry_sdk.start_span(
name="handle request",
custom_sampling_context={"asgi_scope": asgi_scope},
):
...
# After
sentry_sdk.Scope.set_custom_sampling_context({"asgi_scope": asgi_scope})
with sentry_sdk.traces.start_span(name="handle request"):
...
Custom sampling context must be set after continue_trace (which resets propagation context) and before start_span (which is when sampling happens).
ignore_spans (Optional)ignore_spans filters spans at creation time. Rules can be strings, compiled regexes, or dictionaries with name and/or attributes conditions:
import re
import sentry_sdk
sentry_sdk.init(
_experiments={
"trace_lifecycle": "stream",
"ignore_spans": [
# String match against span name
"/health",
# Regex match against span name
re.compile(r"/flow/.*"),
# Match by attributes (all must match)
{
"attributes": {
"service.id": "15def9a",
"flow.pipeline": "legacy",
}
},
# Match by name and attributes
{
"name": re.compile(r"/flow/.*"),
"attributes": {
"service.id": re.compile(r".*\.facade"),
},
},
],
},
)
Only the span name and attributes set at creation time are available for matching — attributes added later in the span's lifetime are not considered.
If an ignored span is a top-level span, its entire subtree is also ignored. If a non-top-level span is ignored, its children are not automatically ignored unless they match a rule themselves.
before_send_transactionbefore_send_transaction has no effect in streaming mode. Spans are sent individually as they complete, not batched into transactions.
Important: In the legacy transaction-based model, before_send_transaction ran after the entire transaction finished, so it had access to all data set during the span's lifetime (e.g. HTTP status codes, response sizes, final results). In streaming mode, the replacements (ignore_spans and before_send_span) can only work with attributes set at span start time. Attributes added later during the span's lifetime (like http.response.status_code, response body size, or other late-set data) are not available.
If your before_send_transaction logic depends on attributes not set at span start, that filtering logic cannot be replicated in streaming mode and must be removed. Consider moving such filtering to a server-side mechanism (e.g. Sentry inbound data filters or Relay rules) instead.
| Use Case | Streaming Replacement |
|---|---|
| Drop spans by name/route | Use ignore_spans |
| Drop/filter by late-set attributes (e.g. HTTP status code) | Cannot be replicated — remove the logic or use server-side filtering |
| Modify span data before send | Use before_send_span |
| Filter by transaction name | Use ignore_spans with string/regex pattern |
Remove the before_send_transaction option from sentry_sdk.init() after migrating its logic.
before_send_span (Optional)before_send_span lets you modify spans before they leave the SDK, for example to sanitize sensitive values. It receives span and hint arguments and must return a span:
import sentry_sdk
def postprocess_span(span, hint):
attributes_to_sanitize = [
"http.request.header.custom-auth",
"http.request.header.custom-user-id",
]
for attribute in attributes_to_sanitize:
if span["attributes"].get(attribute):
span["attributes"][attribute] = "[Sanitized]"
return span
sentry_sdk.init(
_experiments={
"trace_lifecycle": "stream",
"before_send_span": postprocess_span,
},
)
before_send_span cannot be used to drop spans — use ignore_spans for that. If the callback returns anything other than a span dictionary, the return value is ignored.
Instruct the user to verify:
| Symptom | Cause | Fix |
|---|---|---|
| Spans not streaming | Using legacy sentry_sdk.start_span | Switch to sentry_sdk.traces.start_span |
AttributeError on start_child | start_child removed in streaming mode | Use sentry_sdk.traces.start_span while parent is active |
None attribute value rejected | None not supported as attribute value | Remove the attribute or use a sentinel string |
set_data/set_tag has no effect on span | These methods don't apply to streaming spans | Use span.set_attribute() |
| Scope tags missing from spans | set_tag not applied to streaming spans | Use sentry_sdk.set_attribute() |
Custom sampling context not available in traces_sampler | Set after start_span or before continue_trace | Set on scope after continue_trace but before start_span |
scope.span returns wrong type or None | Scope-based span access not reliable in streaming mode | Use sentry_sdk.traces.get_current_span() |
AttributeError on containing_transaction | Attribute removed in streaming mode | Use span._segment |
AttributeError on get_trace_context | Method removed in streaming mode | Use span.trace_id / span.span_id directly, or span._get_trace_context() |
before_send_transaction not called | Expected in streaming mode | Migrate logic to before_send_span or ignore_spans |
before_send_transaction logic relied on late-set attributes (e.g. status code) | These attributes aren't available at span creation time | Remove the logic or use server-side filtering (Sentry inbound filters / Relay rules) |
import sentry_sdk
sentry_sdk.init(
dsn="__DSN__",
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream",
},
)
from sentry_sdk.traces import start_span
with start_span(name="my operation", attributes={"sentry.op": "task"}) as span:
span.set_attribute("result.count", 42)
>=2.62.0_experiments={"trace_lifecycle": "stream"} to sentry_sdk.init()sentry_sdk.start_span() migrated to sentry_sdk.traces.start_span()sentry_sdk.start_transaction() migrated to sentry_sdk.traces.start_span()span.start_child() migrated to sentry_sdk.traces.start_span()sentry_sdk.get_current_span() migrated to sentry_sdk.traces.get_current_span()scope.span replaced with sentry_sdk.traces.get_current_span()scope.transaction replaced with sentry_sdk.traces.get_current_span()._segmentspan.containing_transaction replaced with span._segment@sentry_sdk.trace migrated to @sentry_sdk.traces.traceSpan / Transaction class imports replaced with StreamedSpanspan.get_trace_context() replaced with direct properties (span.trace_id, etc.) or span._get_trace_context()description replaced with nameop replaced with sentry.op attributeset_data() / set_tag() / set_context() replaced with set_attribute()set_tag() supplemented with set_attribute() where neededcontinue_trace migrated to non-context-manager sentry_sdk.traces.continue_trace()custom_sampling_context migrated to Scope.set_custom_sampling_context()traces_sampler) Updated to handle new sampling_context shapebefore_send_transaction logic migrated to before_send_span or ignore_spansbefore_send_transaction logic that depends on late-set attributes (e.g. HTTP status code) removed or moved to server-side filteringbefore_send_transaction removed from config