---
title: "Migrate to Stream Mode"
description: "Learn how to migrate your custom instrumentation from transaction mode to stream mode."
url: https://docs.sentry.io/platforms/dart/tracing/streamed-spans/migration-guide/
---

# Migrate to Stream Mode | Sentry for Dart

Stream mode replaces the transaction-based APIs with new span APIs. If you use custom instrumentation (creating transactions manually, setting span data, or filtering spans) you'll need to update that code before switching to stream mode. This guide walks through the changes.

For an introduction to stream mode itself, see [Streamed Spans](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md).

## [Enable Stream Mode](https://docs.sentry.io/platforms/dart/tracing/streamed-spans/migration-guide.md#enable-stream-mode)

Set `traceLifecycle` to `SentryTraceLifecycle.stream` when initializing the SDK:

```dart
 await Sentry.init((options) {
   options.dsn = 'https://<key>@o<orgId>.ingest.sentry.io/<projectId>';
   options.tracesSampleRate = 1.0;
+  options.traceLifecycle = SentryTraceLifecycle.stream;
 });
```

## [Span Creation](https://docs.sentry.io/platforms/dart/tracing/streamed-spans/migration-guide.md#span-creation)

Replace `Sentry.startTransaction` and `span.startChild` with `Sentry.startSpan`. `startSpan` runs a callback and ends the span when the returned future completes. Whether the resulting span is a service span, a child span, or a sibling depends on the `parentSpan` argument and what's currently active.

```dart
// Starting what used to be a transaction: use parentSpan: null with startSpan to force a service span
- final transaction = Sentry.startTransaction('checkout', 'task');
- try {
-   await runCheckout();
- } finally {
-   await transaction.finish();
- }
+ await Sentry.startSpan('checkout', (span) async {
+   span.setAttribute('sentry.op', SentryAttribute.string('task'));
+   await runCheckout();
+ },
+ parentSpan: null,
+);

// Starting a child span: just start a span while the parent is active
- final transaction = Sentry.startTransaction('checkout', 'task');
- try {
-   final chargeSpan = transaction.startChild('charge-card');
-   try {
-     await paymentService.charge();
-   } finally {
-     await chargeSpan.finish();
-   }
- } finally {
-   await transaction.finish();
- }
+ await Sentry.startSpan('checkout', (checkoutSpan) async {
+   checkoutSpan.setAttribute('sentry.op', SentryAttribute.string('task'));
+   await Sentry.startSpan('charge-card', (chargeSpan) async {
+     await paymentService.charge();
+   });
+ });

// Starting a child span: if the span's parent should be a span that's currently not active, you can provide it explicitly
- final parent = Sentry.startTransaction('background-sync', 'task');
- try {
-   await someOtherAsyncBoundary();
-   final child = parent.startChild('fetch-page');
-   try {
-     await api.fetchPage();
-   } finally {
-     await child.finish();
-   }
- } finally {
-   await parent.finish();
- }
+ final parent = Sentry.startInactiveSpan(
+   'background-sync',
+   parentSpan: null,
+ );
+
+ try {
+   await someOtherAsyncBoundary();
+   await Sentry.startSpan(
+     'fetch-page',
+     (child) async {
+       await api.fetchPage();
+     },
+     parentSpan: parent,
+   );
+ } finally {
+   parent.end();
+ }
```

`op` is not a dedicated argument of `startSpan` — set it as the `sentry.op` attribute instead.

For synchronous work, use `Sentry.startSpanSync` instead. When the work can't be wrapped in a single callback (widget lifecycles, stream subscriptions, platform channels), use `Sentry.startInactiveSpan` and call `end()` manually. See [Start a Span](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#start-a-span) for details.

## [Span Attributes](https://docs.sentry.io/platforms/dart/tracing/streamed-spans/migration-guide.md#span-attributes)

In stream mode, spans have no contexts, data, or tags — everything is a typed attribute. Replace `setData` and `setTag` with `setAttribute` or `setAttributes`:

```dart
- span.setData('retry_count', 3);
- span.setTag('payment.provider', 'stripe');
+ span.setAttribute('retry_count', SentryAttribute.int(3));
+ span.setAttributes({
+  'payment.provider': SentryAttribute.string('stripe'),
+  'cache.hit': SentryAttribute.bool(true),
+  'response_time_ms': SentryAttribute.double(12.5),
+});
```

Attribute values must be created with a typed `SentryAttribute` factory (`string`, `int`, `bool`, or `double`). See [Add Attributes](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#add-attributes) for the full list.

Tags set on the scope using `Sentry.configureScope((scope) => scope.setTag(...))` aren't applied to spans in stream mode. Use `Sentry.setAttributes(...)` to set attributes that apply to your spans instead.

## [Span Status](https://docs.sentry.io/platforms/dart/tracing/streamed-spans/migration-guide.md#span-status)

In stream mode, status is set automatically — `error` if the callback throws, `ok` otherwise. Explicit statuses from the old API migrate to the typed `SentrySpanStatusV2`, which can only be `ok` or `error`:

```dart
- transaction.status = const SpanStatus.internalError();
+ span.status = SentrySpanStatusV2.error;
```

Manual assignment is only needed to override the automatic default.

## [Filtering and Dropping Spans](https://docs.sentry.io/platforms/dart/tracing/streamed-spans/migration-guide.md#filtering-and-dropping-spans)

`beforeSendTransaction` has **no effect** in stream mode — transactions are never created, so the callback is never invoked. Migrate its logic to `beforeSendSpan` to modify spans and `ignoreSpans` to drop them:

```dart
- options.beforeSendTransaction = (transaction, hint) {
-   // scrub sensitive data, drop transactions by name, etc.
-   return transaction;
- };
+ options.beforeSendSpan = (span) {
+   span.removeAttribute('http.request.body');
+ };
+ options.ignoreSpans = [
+   IgnoreSpanRule.nameEquals('health-check'),
+ ];
```

`ignoreTransactions` also has **no effect** in stream mode. Replace it with `ignoreSpans`:

```dart
- options.ignoreTransactions = [
-   'health-check',
-   r'^internal\.',
- ];
+ options.ignoreSpans = [
+   IgnoreSpanRule.nameContains('health-check'),
+   IgnoreSpanRule.nameStartsWith('internal.'),
+ ];
```

`ignoreTransactions` interprets each entry as a case-insensitive regular expression and matches substrings by default. `ignoreSpans` uses explicit, case-sensitive rules. Choose `nameContains`, `nameEquals`, `nameStartsWith`, or `nameEndsWith` based on the existing pattern. Pass a `RegExp` to `nameContains` or `nameStartsWith` when needed.

Note that `beforeSendSpan` is mutation-only and cannot drop spans — use `ignoreSpans` for that. `beforeSendSpan` runs when each span ends, so it sees the full set of attributes, including any added during the span's lifetime. `ignoreSpans`, by contrast, is evaluated when a span is created and matches on the span name only. Remove the `beforeSendTransaction` and `ignoreTransactions` options after migrating their logic. See [Extended Configuration](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#extended-configuration-optional) for details.

## [Sampling](https://docs.sentry.io/platforms/dart/tracing/streamed-spans/migration-guide.md#sampling)

`tracesSampleRate` works unchanged. A custom `tracesSampler` still works, but the sampling context changes: read the span's `name` and `attributes` from `samplingContext.spanContext` instead of the transaction context.

```dart
- options.tracesSampler = (samplingContext) {
-   final name = samplingContext.transactionContext.name;
-   // ...
- };
+ options.tracesSampler = (samplingContext) {
+   final name = samplingContext.spanContext.name;
+   // ...
+ };
```

Only **root spans** are sampled; child spans inherit the root span's decision. When a root span isn't sampled, its callback still executes with a no-op span, so all span operations remain safe to call.
