diff --git a/docs/platforms/dart/common/tracing/streamed-spans/index.mdx b/docs/platforms/dart/common/tracing/streamed-spans/index.mdx new file mode 100644 index 0000000000000..84e8236a12a68 --- /dev/null +++ b/docs/platforms/dart/common/tracing/streamed-spans/index.mdx @@ -0,0 +1,436 @@ +--- +title: Streamed 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. + + + +- **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. + + + +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. +- **Service span**: A top-level span with no parent, created with `parentSpan: null`. It's the stream mode equivalent of a transaction. The first service span in a trace is also its root span. +- **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 [service A] + ├── Child span + │ └── Child span + └── Service span [service B] + ├── Child span + └── Child span +``` + + + +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 Migration Guide for a full list of changes. + + + +## Prerequisites + +You need: + +- Tracing configured in + your app +- Sentry SDK `>=9.23.0` + +## Enable Stream Mode + + + + + +Opt in by setting `traceLifecycle` to `SentryTraceLifecycle.stream` when initializing the SDK. This is the only required config change: + + + + + + + + + + +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. + + + Auto-instrumentations switch to the correct API automatically based on this + setting. This includes Flutter's frames tracking, app start, TTID/TTFD, + navigation, user interaction, HTTP, database, and GraphQL instrumentations. + + + + +When stream mode is enabled, the SDK maintains an internal buffer that groups spans by trace ID. + +Spans are flushed: + +- On a regular interval (every 5 seconds by default). +- When a trace's buffer reaches 1,000 spans. +- When the SDK shuts down. +- When the internal buffer size exceeds 1 MiB. + +Each flush sends only the spans accumulated since the last flush, grouped into envelopes by trace ID. + + + +## 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. + + + + +```dart +await Sentry.startSpan('checkout', (span) async { + // Your code here +}); +``` + + + + + + +Child spans created inside an active span are automatically associated with the parent through zones: + + + + +```dart +await Sentry.startSpan('checkout', (span) async { + await Sentry.startSpan('load cart', (_) => loadCart()); + + await Sentry.startSpan('submit payment', (_) => submitPayment()); +}); +``` + + + + + + +By default, a span inherits the currently active span as its parent. To change this, pass `parentSpan`: + +- `parentSpan: null` forces a service span with no parent. +- `parentSpan: someSpan` parents the new span under a specific span. + + + + +```dart + +// force a new service span +await Sentry.startSpan( + 'checkout-flow', + (span) async { + await runCheckout(); + }, + parentSpan: null, +); + +// start a child span with a specific parent +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(); +} +``` + + + + + +#### Create Spans for Synchronous Work + + + + + +`Sentry.startSpan` takes an asynchronous callback (returning `Future`). For synchronous work, use `Sentry.startSpanSync`, which takes a synchronous callback (returning `T`). 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. + +#### Create 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 have to call `end()` manually, and other spans do **not** automatically become its children — to nest a span under it, pass it explicitly via `parentSpan` when starting the child. + + + + +```dart +final paymentSpan = Sentry.startInactiveSpan( + 'payment', + attributes: {'payment.provider': SentryAttribute.string('stripe')}, +); + +// ...later, from a different entry point +void onPaymentComplete() { + paymentSpan.end(); +} +``` + + + + + +#### Retroactively Set Span Timing + + + + + +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 + + + + + +Attach structured metadata to spans using `setAttribute` or `setAttributes`, and remove them with `removeAttribute`. + + + +Sentry automatically sets several standard attributes on spans. To avoid accidentally overwriting these, refer to our Sentry Attribute Conventions. + + + + + + +```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` | +| `SentryAttribute.stringArray(v)` | `List` | +| `SentryAttribute.intArray(v)` | `List` | +| `SentryAttribute.boolArray(v)` | `List` | +| `SentryAttribute.doubleArray(v)` | `List` | + + + +### Set Span Status + +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`. + +Status can only be `SentrySpanStatusV2.ok` or `SentrySpanStatusV2.error`: + +```dart +await Sentry.startSpan('sync', (span) async { + if (!await isReachable()) { + span.status = SentrySpanStatusV2.error; + return; + } + await sync(); +}); +``` + +## 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`: + + + + +```dart +options.beforeSendSpan = (span) { + span.removeAttribute('http.request.body'); +}; +``` + + + + + + + +`beforeSendSpan` runs after the span ends, so it has access to all attributes set during its lifetime. To drop spans instead of modifying them, use [`ignoreSpans`](#drop-spans). + + + +### Drop Spans + + + + + +To prevent specific spans from being sent, use `ignoreSpans`. Rules are evaluated at span start and match against the span name. Attribute-based matching is not yet supported. + + + + +```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`, no changes are needed — it works the same way in stream mode. + + + + + +If you use a custom `tracesSampler`, the shape of the sampling context is different in stream mode. Instead of a transaction context, read the span's `name` and `attributes` from `samplingContext.spanContext`: + + + + +```dart +options.tracesSampler = (samplingContext) { + final spanContext = samplingContext.spanContext; + if (spanContext.name == 'health-check') { + return 0.0; + } + return 0.2; +}; +``` + + + + + +Only service spans are sampled and child spans inherit the service span's decision. When a service span isn't sampled, its callback still executes with a no-op span, so all span operations remain safe to call. + +## 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, check if the span envelopes are sent with the content type `application/vnd.sentry.items.span.v2+json` instead of transaction envelopes. +- **Check your logs**: If the SDK logs warnings about unsupported span operations, you may still be using the legacy Span API somewhere in your code. See the Migration Guide to update it. diff --git a/docs/platforms/dart/common/tracing/streamed-spans/migration-guide.mdx b/docs/platforms/dart/common/tracing/streamed-spans/migration-guide.mdx new file mode 100644 index 0000000000000..90de84b1f7c02 --- /dev/null +++ b/docs/platforms/dart/common/tracing/streamed-spans/migration-guide.mdx @@ -0,0 +1,159 @@ +--- +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 Streamed Spans. + +## Enable Stream Mode + +Set `traceLifecycle` to `SentryTraceLifecycle.stream` when initializing the SDK: + + + +## 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 diff +// 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 for details. + +## 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 diff +- 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 Span Attributes for the full list. + +Tags set on the scope with (`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 + +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. `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` option after migrating its logic. See Extended Configuration for details. + +## 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 diff +- 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. diff --git a/platform-includes/performance/span-streaming-enable-diff/dart.flutter.mdx b/platform-includes/performance/span-streaming-enable-diff/dart.flutter.mdx new file mode 100644 index 0000000000000..526202196eb00 --- /dev/null +++ b/platform-includes/performance/span-streaming-enable-diff/dart.flutter.mdx @@ -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()), + ); +``` diff --git a/platform-includes/performance/span-streaming-enable-diff/dart.mdx b/platform-includes/performance/span-streaming-enable-diff/dart.mdx new file mode 100644 index 0000000000000..8c929de6d2ac6 --- /dev/null +++ b/platform-includes/performance/span-streaming-enable-diff/dart.mdx @@ -0,0 +1,7 @@ +```dart diff + await Sentry.init((options) { + options.dsn = '___PUBLIC_DSN___'; + options.tracesSampleRate = 1.0; ++ options.traceLifecycle = SentryTraceLifecycle.stream; + }); +``` diff --git a/platform-includes/performance/span-streaming-enable/dart.flutter.mdx b/platform-includes/performance/span-streaming-enable/dart.flutter.mdx new file mode 100644 index 0000000000000..578a2a58c5237 --- /dev/null +++ b/platform-includes/performance/span-streaming-enable/dart.flutter.mdx @@ -0,0 +1,16 @@ +```dart +import 'package:flutter/widgets.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +Future 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()), + ); +} +``` diff --git a/platform-includes/performance/span-streaming-enable/dart.mdx b/platform-includes/performance/span-streaming-enable/dart.mdx new file mode 100644 index 0000000000000..cfd22a73f4996 --- /dev/null +++ b/platform-includes/performance/span-streaming-enable/dart.mdx @@ -0,0 +1,12 @@ +```dart +import 'package:sentry/sentry.dart'; + +Future main() async { + await Sentry.init((options) { + options.dsn = '___PUBLIC_DSN___'; + options.tracesSampleRate = 1.0; + // Enables stream mode + options.traceLifecycle = SentryTraceLifecycle.stream; + }); +} +```