Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions docs/platforms/dart/common/tracing/new-spans/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
---
title: New Spans
description: "Learn how to use stream mode to send spans to Sentry as they finish, removing the 1,000-span limit and making trace data visible sooner."
sidebar_order: 10
new: true
---

By default, the Sentry SDK collects all spans in memory and sends them to Sentry as a single transaction once the root span ends. This is called transaction mode.
Stream mode changes this by sending spans to Sentry in batches as they finish, instead of waiting for the whole transaction to complete.

<Expandable title="Why use stream mode?">

- **No 1,000-span limit.** In transaction mode, transactions are capped at 1,000 spans. Stream mode has no upper limit since spans are sent in batches as they finish.
- **Lower memory usage.** Spans are flushed as they complete instead of being held in memory until the root span ends. This is especially useful for long-lived screens and background isolates.
- **Faster visibility.** Span data arrives in Sentry as your app runs, instead of only after the entire operation completes.
- **Fewer spans lost to crashes.** If your app terminates unexpectedly, spans that were already flushed are still delivered. In transaction mode, a crash before the transaction ends means all of its span data is lost.

</Expandable>

You can find the following span types mentioned throughout this page:

- **Root span**: The topmost span in a trace. It has no parent span, and sampling decisions are made here.
- **Child span**: Any span nested under a parent span within the same trace.

This graph shows how these span types relate to each other within a trace:

```
Trace
└── Root span
├── Child span
│ └── Child span
└── Child span
```

<Alert level="warning" title="Migrating from transaction mode?">

Stream mode replaces the transaction-based APIs with new span APIs, so migrating to stream mode and adopting the new span APIs are the same step. If you have existing custom instrumentation, see the <PlatformLink to="/tracing/new-spans/migration-guide/">Migration Guide</PlatformLink> for a full list of changes.

</Alert>

## Prerequisites

You need:

- <PlatformLink to="/tracing/#configure">Tracing configured</PlatformLink> in
your app
- Sentry SDK `>=9.19.0`

## Enable Stream Mode

Opt in by setting `traceLifecycle` to `SentryTraceLifecycle.stream` when initializing the SDK. This is the only required config change:

<PlatformContent includePath="performance/span-streaming-enable" />

To revert to transaction mode, remove the option or set `traceLifecycle` to `SentryTraceLifecycle.static` (the default).

You can only use one tracing system at a time:

- In `stream` mode, the transaction APIs (`Sentry.startTransaction`, `ISentrySpan.startChild`) do nothing and log a warning.
- In `static` mode, the new span APIs (`Sentry.startSpan`) do nothing.
- Auto-instrumentations switch to the correct API automatically based on this setting.

## Manual Instrumentation (Optional)

The SDK instruments common operations for you, but you can wrap your own code in spans to measure anything that matters to your app.

### Start a Span

`Sentry.startSpan` runs a callback and ends the span when the returned future completes. Spans created inside an active span are automatically associated with the parent through zones, so there's no separate "child span" call to make — just nest:

```dart
await Sentry.startSpan('checkout', (span) async {
await Sentry.startSpan('load cart', (_) => loadCart());

await Sentry.startSpan('submit payment', (_) => submitPayment());
});
```

Error handling is automatic: if the callback throws (or its future errors), the span status is set to `error` before the span ends and the error is rethrown. Otherwise the status defaults to `ok`.

For synchronous work, use `Sentry.startSpanSync`. Both variants can be freely nested, and parent-child relationships resolve correctly across sync and async boundaries:

```dart
final config = Sentry.startSpanSync('parse-config', (_) {
return Config.parse(raw);
});
```

If a span isn't sampled, the callback still runs and receives a no-op span, so all span operations remain safe to call.

#### Spans That Outlive a Callback

Use `Sentry.startInactiveSpan` when the work can't be wrapped in a single callback — widget lifecycles, stream subscriptions, or platform channel round-trips. You must call `end()` manually, and other spans do **not** automatically become its children:

```dart
final paymentSpan = Sentry.startInactiveSpan(
'payment',
attributes: {'payment.provider': SentryAttribute.string('stripe')},
);

// ...later, from a different entry point
void onPaymentComplete() {
paymentSpan.end();
}
```

#### Control Parenting

By default, a span inherits the currently active span as its parent. To change this, pass `parentSpan`:

- `parentSpan: null` forces a root span with no parent.
- `parentSpan: someSpan` parents the new span under a specific `SentrySpanV2`.

This applies to `startSpan`, `startSpanSync`, and `startInactiveSpan`.

#### Set Span Timing Retroactively

When the real start or end of the work happened before you could create or end the span — for example, a duration measured by a platform channel — pass `startTimestamp` or an explicit end time:

```dart
// startTimestamp is available on the callback variants
Sentry.startSpanSync('replay-import', (_) => importRows(),
startTimestamp: measuredStart);

final paymentSpan = Sentry.startInactiveSpan('payment');
// ...native reports the work ended at `nativeEnd`
paymentSpan.end(endTimestamp: nativeEnd);
```

### Add Span Attributes

Streamed spans use typed attributes instead of untyped data and tags. Set them with `setAttribute` or `setAttributes`, and remove them with `removeAttribute`:

```dart
await Sentry.startSpan('process-order', (span) async {
span.setAttribute('order.id', SentryAttribute.string('abc-123'));

span.setAttributes({
'order.item_count': SentryAttribute.int(5),
'order.priority': SentryAttribute.bool(true),
'order.total': SentryAttribute.double(42.50),
});

await processOrder();
});
```

Each attribute value is created with a typed `SentryAttribute` factory:

| Factory | Dart Type |
| --------------------------- | --------- |
| `SentryAttribute.string(v)` | `String` |
| `SentryAttribute.int(v)` | `int` |
| `SentryAttribute.bool(v)` | `bool` |
| `SentryAttribute.double(v)` | `double` |

<Alert level="info">

Sentry automatically sets several standard attributes on spans. To avoid accidentally overwriting these, refer to our <a href="https://getsentry.github.io/sentry-conventions/attributes/">Sentry Attribute Conventions</a>.

</Alert>

### Set Span Status

The status is set automatically — `error` if the callback throws, `ok` otherwise — so you only need to set it manually to override the default:

```dart
await Sentry.startSpan('sync', (span) async {
if (!await isReachable()) {
span.status = SentrySpanStatusV2.error;
return;
}
await sync();
});
```

Status can only be `SentrySpanStatusV2.ok` or `SentrySpanStatusV2.error`.

## Extended Configuration (Optional)

You can shape what ends up in Sentry by filtering span data or dropping spans entirely.

### Filter Spans

To modify or redact span data before it's sent, use `beforeSendSpan`. It receives each `SentrySpanV2` before it's sent. Unlike other `beforeSend` callbacks, it **cannot drop spans** — it's mutation-only. Use [`ignoreSpans`](#drop-spans) to drop spans instead.

```dart
options.beforeSendSpan = (span) {
span.removeAttribute('http.request.body');
};
```

### Drop Spans

To prevent specific spans from being sent, use `ignoreSpans`. Rules match against the span name (attribute matching isn't supported yet in the Dart SDK):

```dart
options.ignoreSpans = [
IgnoreSpanRule.nameEquals('health-check'),
IgnoreSpanRule.nameStartsWith('internal.'),
IgnoreSpanRule.nameContains('metrics'),
IgnoreSpanRule.nameEndsWith('.bg'),
];
```

| Factory | Matches |
| ---------------------------------------- | --------------------------------- |
| `IgnoreSpanRule.nameEquals(String)` | Exact span name |
| `IgnoreSpanRule.nameStartsWith(Pattern)` | Name prefix (String or RegExp) |
| `IgnoreSpanRule.nameContains(Pattern)` | Name substring (String or RegExp) |
| `IgnoreSpanRule.nameEndsWith(String)` | Name suffix |

When an ignored span has children, the children are re-parented to the nearest recording ancestor rather than dropped.

## Sampling (Optional)

If you use `tracesSampleRate` or a custom `tracesSampler`, no changes are needed — both work the same way in stream mode. Only **root spans** are sampled; child spans inherit the root's decision. When a root span isn't sampled, its callback still executes with a no-op span.

## Flutter Auto-Instrumentation

No code changes are needed. Frames tracking, app start, TTID/TTFD, navigation, user interaction, HTTP, database, and GraphQL instrumentations all switch to the streaming API automatically when `traceLifecycle` is `stream`.

## Verify Your Setup

To make sure you've enabled stream mode successfully:

- **Check the Sentry dashboard**: Spans should appear in the Traces view shortly after each span completes, without waiting for a whole transaction to finish. Spans are buffered briefly and flushed in batches, so expect a short delay before they appear.
- **Check the envelopes**: With `options.debug = true` or network inspection, span envelopes are sent with the content type `application/vnd.sentry.items.span.v2+json` instead of transaction envelopes.
- **Check your logs**: A log line like `startTransaction is not supported when traceLifecycle is 'stream'` means the legacy transaction API is still being called somewhere in your code. See the <PlatformLink to="/tracing/new-spans/migration-guide/">Migration Guide</PlatformLink> to update it.
86 changes: 86 additions & 0 deletions docs/platforms/dart/common/tracing/new-spans/migration-guide.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
title: Migrate to Stream Mode
sidebar_order: 10
description: "Learn how to migrate your custom instrumentation from transaction mode to stream mode."
---

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 <PlatformLink to="/tracing/new-spans/">New Spans</PlatformLink>.

## Enable Stream Mode

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

<PlatformContent includePath="performance/span-streaming-enable-diff" />

In `stream` mode, the transaction APIs (`Sentry.startTransaction`, `ISentrySpan.startChild`) become no-ops and log a warning, so you need to migrate any manual usage.

## Span Creation

Replace `Sentry.startTransaction` and `span.startChild` with `Sentry.startSpan`. `startSpan` runs a callback and ends the span when the returned future completes. Nested calls auto-parent through zones, so there's no `startChild` equivalent — just nest:

```dart diff
- final transaction = Sentry.startTransaction('checkout', 'task');
- try {
- final child = transaction.startChild('db.query', description: 'load cart');
- final cart = await loadCart();
- await child.finish();
- transaction.setData('cart.item_count', cart.items.length);
- } finally {
- await transaction.finish();
- }
+ await Sentry.startSpan('checkout', (span) async {
+ final cart = await Sentry.startSpan('load cart', (_) => loadCart());
+ span.setAttribute('cart.item_count', SentryAttribute.int(cart.items.length));
+ });
```

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 <PlatformLink to="/tracing/new-spans/#start-a-span">Start a Span</PlatformLink> for details.

## Span Attributes

Streamed spans have no untyped data or tags — everything is a typed attribute. Replace `setData` and `setTag` with `setAttribute` or `setAttributes`:

```dart diff
- span.setData('retry_count', 3);
- span.setTag('payment.provider', 'stripe');
+ span.setAttribute('retry_count', SentryAttribute.int(3));
+ span.setAttribute('payment.provider', SentryAttribute.string('stripe'));
```

Attribute values must be created with a typed `SentryAttribute` factory (`string`, `int`, `bool`, or `double`). See <PlatformLink to="/tracing/new-spans/#add-span-attributes">Add Span Attributes</PlatformLink> for the full list.

## 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 diff
- transaction.status = const SpanStatus.internalError();
+ span.status = SentrySpanStatusV2.error;
```

Manual assignment is only needed to override the automatic default.

## 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 diff
- options.beforeSendTransaction = (transaction) {
- // scrub sensitive data, drop transactions by name, etc.
- return transaction;
- };
+ options.beforeSendSpan = (span) {
+ span.removeAttribute('http.request.body');
+ };
+ options.ignoreSpans = [
+ IgnoreSpanRule.nameEquals('health-check'),
+ ];
```

Note that `beforeSendSpan` is mutation-only and cannot drop spans — use `ignoreSpans` for that. Both only have access to the span name and attributes set at creation time, not attributes added later in the span's lifetime. Remove the `beforeSendTransaction` option after migrating its logic. See <PlatformLink to="/tracing/new-spans/#extended-configuration-optional">Extended Configuration</PlatformLink> for details.

## Sampling

`tracesSampleRate` and `tracesSampler` work unchanged. Only **root spans** are sampled; child spans inherit the root'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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
```dart diff
await SentryFlutter.init(
(options) {
options.dsn = '___PUBLIC_DSN___';
options.tracesSampleRate = 1.0;
+ options.traceLifecycle = SentryTraceLifecycle.stream;
},
appRunner: () => runApp(const MyApp()),
);
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```dart diff
await Sentry.init((options) {
options.dsn = '___PUBLIC_DSN___';
options.tracesSampleRate = 1.0;
+ options.traceLifecycle = SentryTraceLifecycle.stream;
});
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
```dart
import 'package:flutter/widgets.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

Future<void> main() async {
await SentryFlutter.init(
(options) {
options.dsn = '___PUBLIC_DSN___';
options.tracesSampleRate = 1.0;
// Enables stream mode
options.traceLifecycle = SentryTraceLifecycle.stream;
},
appRunner: () => runApp(const MyApp()),
);
}
```
12 changes: 12 additions & 0 deletions platform-includes/performance/span-streaming-enable/dart.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
```dart
import 'package:sentry/sentry.dart';

Future<void> main() async {
await Sentry.init((options) {
options.dsn = '___PUBLIC_DSN___';
options.tracesSampleRate = 1.0;
// Enables stream mode
options.traceLifecycle = SentryTraceLifecycle.stream;
});
}
```
Loading