---
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/guides/flutter/tracing/streamed-spans/migration-guide/
---

# Migrate to Stream Mode for Flutter

Stream mode is the default in v10 and replaces the transaction-based APIs with new span APIs. If you use custom instrumentation (creating transactions manually, setting span data, or filtering spans) update that code when upgrading to v10, or explicitly set `options.traceLifecycle = SentryTraceLifecycle.static` to retain transaction mode while migrating. This guide walks through the changes.

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

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

Stream mode is the default in v10. If your initialization explicitly selects `SentryTraceLifecycle.static`, remove that assignment or change it to `SentryTraceLifecycle.stream`. SDK versions before v10 require an explicit opt-in:

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

## [Span Creation](https://docs.sentry.io/platforms/dart/guides/flutter/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/guides/flutter/tracing/streamed-spans.md#start-a-span) for details.

## [Span Attributes](https://docs.sentry.io/platforms/dart/guides/flutter/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/guides/flutter/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/guides/flutter/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/guides/flutter/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, hint) {
+   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/guides/flutter/tracing/streamed-spans.md#extended-configuration-optional) for details.

## [Sampling](https://docs.sentry.io/platforms/dart/guides/flutter/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.
