Migrate to Stream Mode
Learn how to migrate your custom instrumentation from transaction mode to stream mode.
Stream mode requires the streamed Span API. If you use custom instrumentation (creating spans manually, setting span data, or filtering spans) you'll need to update that code before you can switch to stream mode. This guide walks through the changes.
For an introduction to stream mode itself, see Streamed Spans.
Add the trace_lifecycle option when initializing the SDK:
import sentry_sdk
sentry_sdk.init(
dsn="___PUBLIC_DSN___",
traces_sample_rate=1.0,
+ trace_lifecycle="stream",
)
import sentry_sdk
sentry_sdk.init(
dsn="___PUBLIC_DSN___",
traces_sample_rate=1.0,
+ trace_lifecycle="stream",
)
Replace start_span, start_transaction, and start_child with sentry_sdk.traces.start_span(). Whether the resulting span is a service span, a child span, or a sibling depends on the parent_span argument and what's currently active.
import sentry_sdk
# Starting a span
- with sentry_sdk.start_span(op="http.client", description="GET /api/users") as span:
+ with sentry_sdk.traces.start_span(
+ name="GET /api/users",
+ attributes={"sentry.op": "http.client"},
+ ) as span:
...
# Starting what used to be a transaction: pass parent_span=None to force a service span
- with sentry_sdk.start_transaction(name="flow.checkout") as transaction:
+ with sentry_sdk.traces.start_span(name="flow.checkout", parent_span=None) as span:
...
# Starting a child span: just start a span while the parent is active
- with parent.start_child(op="db", description="SELECT") as child:
+ with sentry_sdk.traces.start_span(name="SELECT", attributes={"sentry.op": "db"}):
...
# Starting a child span: if the span's parent should be a span that's currently not active, you can provide it explicitly
- with parent.start_child(op="db", description="SELECT") as child:
+ with sentry_sdk.traces.start_span(name="SELECT", attributes={"sentry.op": "db"}, parent_span=parent):
...
import sentry_sdk
# Starting a span
- with sentry_sdk.start_span(op="http.client", description="GET /api/users") as span:
+ with sentry_sdk.traces.start_span(
+ name="GET /api/users",
+ attributes={"sentry.op": "http.client"},
+ ) as span:
...
# Starting what used to be a transaction: pass parent_span=None to force a service span
- with sentry_sdk.start_transaction(name="flow.checkout") as transaction:
+ with sentry_sdk.traces.start_span(name="flow.checkout", parent_span=None) as span:
...
# Starting a child span: just start a span while the parent is active
- with parent.start_child(op="db", description="SELECT") as child:
+ with sentry_sdk.traces.start_span(name="SELECT", attributes={"sentry.op": "db"}):
...
# Starting a child span: if the span's parent should be a span that's currently not active, you can provide it explicitly
- with parent.start_child(op="db", description="SELECT") as child:
+ with sentry_sdk.traces.start_span(name="SELECT", attributes={"sentry.op": "db"}, parent_span=parent):
...
A few argument changes come along with this:
descriptionno longer exists — usenameinstead.opis no longer a dedicated argument — set it as thesentry.opattribute instead.
If you use the @trace decorator, only the import changes:
- from sentry_sdk import trace
+ from sentry_sdk.traces import trace
@trace
def checkout():
...
- from sentry_sdk import trace
+ from sentry_sdk.traces import trace
@trace
def checkout():
...
If your code imports Span or Transaction directly, for example for type annotations, replace both with StreamedSpan:
- from sentry_sdk.tracing import Span, Transaction
+ from sentry_sdk.traces import StreamedSpan
- def process(span: Span) -> None:
+ def process(span: StreamedSpan) -> None:
...
- from sentry_sdk.tracing import Span, Transaction
+ from sentry_sdk.traces import StreamedSpan
- def process(span: Span) -> None:
+ def process(span: StreamedSpan) -> None:
...
In stream mode, spans have no contexts, data, or tags. Instead, everything is an attribute. Replace set_data(), set_tag(), and set_context() with set_attribute() or set_attributes():
- span.set_data("flow.step", "submit_payment")
- span.set_tag("http.status_code", 201)
+ span.set_attributes({
+ "flow.step": "submit_payment",
+ "http.response.status_code": 201,
+ })
- span.set_data("flow.step", "submit_payment")
- span.set_tag("http.status_code", 201)
+ span.set_attributes({
+ "flow.step": "submit_payment",
+ "http.response.status_code": 201,
+ })
Unlike the old methods, set_attribute only accepts primitive types (str, int, float, bool, or arrays of these). None isn't supported either. Flatten dictionaries into separate attributes, and stringify anything that can't be flattened:
- span.set_data("request", {"method": "POST", "path": "/api/checkout"})
+ span.set_attributes({
+ "request.method": "POST",
+ "request.path": "/api/checkout",
+ })
- span.set_data("request", {"method": "POST", "path": "/api/checkout"})
+ span.set_attributes({
+ "request.method": "POST",
+ "request.path": "/api/checkout",
+ })
Tags set on the scope with sentry_sdk.set_tag() aren't applied to spans in stream mode. 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
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
A few ways of referencing the current span or transaction change in stream mode:
import sentry_sdk
# Getting the current span
- span = sentry_sdk.get_current_span()
+ span = sentry_sdk.traces.get_current_span()
# Getting the current span via the scope
- scope = sentry_sdk.get_current_scope()
- current_span = scope.span
+ current_span = sentry_sdk.traces.get_current_span()
import sentry_sdk
# Getting the current span
- span = sentry_sdk.get_current_span()
+ span = sentry_sdk.traces.get_current_span()
# Getting the current span via the scope
- scope = sentry_sdk.get_current_scope()
- current_span = scope.span
+ current_span = sentry_sdk.traces.get_current_span()
If your code reads specific fields off the trace context, access them as direct properties instead of calling get_trace_context(), which no longer exists on streaming spans:
- ctx = span.get_trace_context()
- trace_id = ctx["trace_id"]
- span_id = ctx["span_id"]
+ trace_id = span.trace_id
+ span_id = span.span_id
- ctx = span.get_trace_context()
- trace_id = ctx["trace_id"]
- span_id = ctx["span_id"]
+ trace_id = span.trace_id
+ span_id = span.span_id
sentry_sdk.traces.continue_trace() replaces the legacy continue_trace(). It's no longer a context manager — it sets the propagation context, and the next span you start picks it up automatically:
import sentry_sdk
headers = {
"sentry-trace": "...",
"baggage": "...",
}
- with sentry_sdk.continue_trace(headers) as transaction:
- ...
+ sentry_sdk.traces.continue_trace(headers)
+ with sentry_sdk.traces.start_span(name="handle request"):
+ ...
import sentry_sdk
headers = {
"sentry-trace": "...",
"baggage": "...",
}
- with sentry_sdk.continue_trace(headers) as transaction:
- ...
+ sentry_sdk.traces.continue_trace(headers)
+ with sentry_sdk.traces.start_span(name="handle request"):
+ ...
Status can only be ok (default) or error in stream mode:
from sentry_sdk.traces import start_span
with start_span(name="process") as span:
try:
...
except Exception:
span.status = "error"
from sentry_sdk.traces import start_span
with start_span(name="process") as span:
try:
...
except Exception:
span.status = "error"
If you use traces_sample_rate, no changes are needed.
If you use a custom traces_sampler, the sampling context has the same structure in stream mode, but not all keys might be populated. The data key under transaction_context contains span attributes known at span start.
import sentry_sdk
def traces_sampler(sampling_context):
if sampling_context["parent_sampled"] is not None:
return float(sampling_context["parent_sampled"])
if sampling_context["transaction_context"]["name"] in IGNORED_SPAN_NAMES:
return 0.0
return 1.0
sentry_sdk.init(
traces_sampler=traces_sampler,
trace_lifecycle="stream",
)
import sentry_sdk
def traces_sampler(sampling_context):
if sampling_context["parent_sampled"] is not None:
return float(sampling_context["parent_sampled"])
if sampling_context["transaction_context"]["name"] in IGNORED_SPAN_NAMES:
return 0.0
return 1.0
sentry_sdk.init(
traces_sampler=traces_sampler,
trace_lifecycle="stream",
)
custom_sampling_context is no longer an argument to start_span. Set it on the scope instead, after continue_trace (which resets the propagation context) and before start_span (which is when sampling happens):
import sentry_sdk
- with sentry_sdk.start_span(
- name="handle request",
- custom_sampling_context={"asgi_scope": asgi_scope},
- ):
- ...
+ sentry_sdk.get_current_scope().set_custom_sampling_context({"asgi_scope": asgi_scope})
+ with sentry_sdk.traces.start_span(name="handle request"):
+ ...
import sentry_sdk
- with sentry_sdk.start_span(
- name="handle request",
- custom_sampling_context={"asgi_scope": asgi_scope},
- ):
- ...
+ sentry_sdk.get_current_scope().set_custom_sampling_context({"asgi_scope": asgi_scope})
+ with sentry_sdk.traces.start_span(name="handle request"):
+ ...
before_send_transaction has no effect in stream mode, since spans are sent individually rather than batched into a transaction. Replace it with ignore_spans (to drop spans) and before_send_span (to modify them):
import re
import sentry_sdk
+ def my_span_processor(span, hint):
+ if span["attributes"].get("sentry.op") == "db.query":
+ span["name"] = "[filtered]"
+ return span
sentry_sdk.init(
dsn="___PUBLIC_DSN___",
traces_sample_rate=1.0,
- before_send_transaction=my_filter,
+ trace_lifecycle="stream",
+ ignore_spans=[
+ "/health",
+ re.compile(r"^GET /api/v1/internal"),
+ {"attributes": {"service.id": "15def9a"}},
+ ],
+ before_send_span=my_span_processor,
)
import re
import sentry_sdk
+ def my_span_processor(span, hint):
+ if span["attributes"].get("sentry.op") == "db.query":
+ span["name"] = "[filtered]"
+ return span
sentry_sdk.init(
dsn="___PUBLIC_DSN___",
traces_sample_rate=1.0,
- before_send_transaction=my_filter,
+ trace_lifecycle="stream",
+ ignore_spans=[
+ "/health",
+ re.compile(r"^GET /api/v1/internal"),
+ {"attributes": {"service.id": "15def9a"}},
+ ],
+ before_send_span=my_span_processor,
)
ignore_spans only has access to the span name and attributes set at creation time — not attributes added later in the span's lifetime, like an HTTP status code set after the request completes. If your before_send_transaction logic was used to drop spans based on late-set data, this can't be replicated in stream mode. Consider server-side filtering with Sentry inbound data filters or Relay rules instead. before_send_span runs after the span ends and has access to all attributes set during the span's lifetime.
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").