skills/sentry-span-streaming-dart/SKILL.md
Migrate Dart/Flutter SDK to Sentry span streaming (span-first trace lifecycle). Use when asked to "enable span streaming", "migrate to span streaming", "use traceLifecycle stream", "use Sentry.startSpan", or switch from transaction-based to streamed span delivery in a Dart or Flutter project.
npx skillsauth add getsentry/sentry-for-ai sentry-span-streaming-dartInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
All Skills > Feature Setup > Span Streaming (Dart)
Migrate from the default transaction-based trace lifecycle (SentryTraceLifecycle.static) to span streaming (SentryTraceLifecycle.stream), where spans are sent individually as they complete instead of being batched into a transaction at the end.
This skill covers the Dart and Flutter SDKs (sentry, sentry_flutter, and integration packages such as sentry_dio, sentry_sqflite, sentry_drift). For JavaScript, see Span Streaming (JavaScript). For Python, see Span Streaming (Python).
traceLifecycle, SentryTraceLifecycle.stream, Sentry.startSpan, or SentrySpanV2# Which Sentry packages does the project use?
grep -n "sentry" pubspec.yaml 2>/dev/null
# Resolved versions — span streaming requires >= 9.19.0
grep -A8 '^ sentry:' pubspec.lock 2>/dev/null | grep -m1 version
grep -A8 '^ sentry_flutter:' pubspec.lock 2>/dev/null | grep -m1 version
Span streaming requires sentry / sentry_flutter >=9.19.0.
# Find init calls
grep -rn "SentryFlutter\.init\|Sentry\.init" lib/ bin/ test/ --include="*.dart" 2>/dev/null | head -10
# Find old transaction API usage (these become no-ops in streaming mode)
grep -rn "startTransaction\|\.startChild(" lib/ bin/ test/ --include="*.dart" 2>/dev/null | head -20
# Find callbacks affected by the migration
grep -rn "beforeSendTransaction\|beforeSendSpan\|ignoreSpans" lib/ bin/ test/ --include="*.dart" 2>/dev/null
# Find untyped span data/tag usage on transactions
grep -rn "\.setData(\|\.setTag(" lib/ bin/ test/ --include="*.dart" 2>/dev/null | head -20
| Finding | Migration Work |
|---|---|
| Sentry.startTransaction(...) calls | Replace with Sentry.startSpan / Sentry.startSpanSync / Sentry.startInactiveSpan |
| span.startChild(...) calls | Replace with nested Sentry.startSpan calls — parenting is automatic via zones |
| setData / setTag on spans | Replace with typed span.setAttribute(...) / SentryAttribute |
| beforeSendTransaction callback | Never called in streaming mode — migrate logic to beforeSendSpan or ignoreSpans |
| Existing beforeSendSpan / ignoreSpans usage | Review against streaming semantics — beforeSendSpan is mutation-only, ignoreSpans matches names only (see 2.5/2.6) |
| Only auto-instrumentation (no manual spans) | Just enable the option — integrations switch automatically |
Prerequisites: sentry / sentry_flutter >=9.19.0 with tracing enabled (tracesSampleRate or tracesSampler configured).
Set traceLifecycle in the init options. This is the only required config change.
// Flutter
await SentryFlutter.init((options) {
options.dsn = '__DSN__';
options.tracesSampleRate = 1.0;
options.traceLifecycle = SentryTraceLifecycle.stream;
}, appRunner: () => runApp(const MyApp()));
// Plain Dart
await Sentry.init((options) {
options.dsn = '__DSN__';
options.tracesSampleRate = 1.0;
options.traceLifecycle = SentryTraceLifecycle.stream;
});
You can only use one tracing system at a time:
stream mode, the transaction APIs (Sentry.startTransaction, ISentrySpan.startChild) do nothing and log a warning.static mode (the default), the new span APIs (Sentry.startSpan) do nothing.Sentry.startSpan runs an async callback and ends the span when the returned future completes. Nested calls auto-parent through zones — there is no startChild equivalent to migrate to; just nest.
// Before (static mode)
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);
transaction.status = const SpanStatus.ok();
} catch (exception) {
transaction.status = const SpanStatus.internalError(); // -> span.status (see below)
rethrow;
} finally {
await transaction.finish();
}
// After (streaming mode)
await Sentry.startSpan('checkout', (span) async {
final cart = await Sentry.startSpan('load cart', (_) => loadCart());
span.setAttribute('cart.item_count', SentryAttribute.int(cart.items.length));
});
Error handling is automatic: if the callback throws (or the future errors), the span status is set to SentrySpanStatusV2.error before the span ends, and the error is rethrown. Otherwise the status defaults to ok.
You can also set the status manually via the status setter:
span.status = SentrySpanStatusV2.error;
There is no untyped SpanStatus string equivalent. Explicit statuses from the old API (e.g. transaction.status = const SpanStatus.internalError()) migrate to span.status = SentrySpanStatusV2.error — though error is set automatically on throw and ok automatically otherwise, so manual assignment is only needed to override the default.
For synchronous work, use Sentry.startSpanSync:
final config = Sentry.startSpanSync('parse-config', (_) {
return Config.parse(raw);
});
Both variants can be freely nested — parent-child relationships resolve correctly across sync/async boundaries. If a span is not sampled, the callback still runs and receives a no-op span, so all span operations are safe.
Use Sentry.startInactiveSpan when the work cannot be wrapped in a single callback — widget lifecycles, stream subscriptions, platform channel round-trips. You must call end() manually, and other spans do not automatically become its children.
final paymentSpan = Sentry.startInactiveSpan('payment',
attributes: {'payment.provider': SentryAttribute.string('stripe')});
// ...later, from a different entry point
void onDeepLink(Uri uri) {
paymentSpan.end();
}
Parenting. parentSpan defaults to an internal unset sentinel (const UnsetSentrySpanV2()), which means "inherit the currently active span." This is explicitly distinct from passing parentSpan: null, which forces a root span with no parent. Pass an explicit SentrySpanV2 to parent under a specific span. (The same default applies to startSpan / startSpanSync.)
Retroactive timing. startSpan and startSpanSync accept an optional startTimestamp, and span.end(endTimestamp: ...) accepts an explicit end time. Use these 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:
final paymentSpan = Sentry.startInactiveSpan('payment');
// ...native reports it started at `nativeStart` and ended at `nativeEnd`
paymentSpan.end(endTimestamp: nativeEnd);
// startTimestamp is available on the callback variants:
Sentry.startSpanSync('replay-import', (_) => importRows(),
startTimestamp: measuredStart);
setData / setTag to Typed AttributesStreamed spans use typed attributes instead of untyped data and tags:
// Before
transaction.setData('retry_count', 3);
transaction.setTag('payment.provider', 'stripe');
// After
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),
});
span.removeAttribute('old_key');
| Factory | Dart Type |
|---|---|
| SentryAttribute.string(v) | String |
| SentryAttribute.int(v) | int |
| SentryAttribute.bool(v) | bool |
| SentryAttribute.double(v) | double |
beforeSendTransactionbeforeSendTransaction has no effect in streaming mode — transactions are never created, so the callback is never invoked. Migrate its logic:
| Use Case | Streaming Replacement |
|---|---|
| Drop spans by name | ignoreSpans with IgnoreSpanRule |
| Scrub sensitive data / PII | beforeSendSpan |
| Modify span data before send | beforeSendSpan |
beforeSendSpan receives each SentrySpanV2 before it is sent. Unlike other beforeSend callbacks, it cannot drop spans — it is mutation-only (FutureOr<void> return). Use ignoreSpans to drop.
await Sentry.init((options) {
options.traceLifecycle = SentryTraceLifecycle.stream;
options.beforeSendSpan = (span) {
span.removeAttribute('http.request.body');
};
});
Remove the beforeSendTransaction option after migrating its logic.
ignoreSpans (Optional)ignoreSpans drops spans by name before sampling. Matching is name-only (attribute matching is not yet supported in the Dart SDK).
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.
No code changes 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.
tracesSampleRate and tracesSampler work unchanged. Only root spans are sampled; child spans inherit the root's decision. When a root span is not sampled, the callback still executes with a no-op span.
dart analyze 2>&1 | head -30
# or for Flutter projects
flutter analyze 2>&1 | head -30
Expect no new analyzer errors from the migration.
Instruct the user to verify with options.debug = true or network inspection:
application/vnd.sentry.items.span.v2+json instead of transaction envelopes. Spans are buffered briefly and flushed in batches, so expect a short delay before envelopes appear.startTransaction is not supported when traceLifecycle is 'stream' means old transaction API calls remain — find and migrate them.| Symptom | Cause | Fix |
|---|---|---|
| Log: startTransaction is not supported when traceLifecycle is 'stream' | Old transaction API still called in streaming mode | Replace with Sentry.startSpan / startSpanSync |
| Sentry.startSpan produces no spans | traceLifecycle still static (the default), or tracing disabled | Set options.traceLifecycle = SentryTraceLifecycle.stream and a tracesSampleRate |
| beforeSendTransaction never called | Expected in streaming mode | Migrate logic to beforeSendSpan or ignoreSpans, then remove it |
| Returning a value from beforeSendSpan to drop a span does nothing | beforeSendSpan is mutation-only (FutureOr<void> return) | Use ignoreSpans to drop spans |
| Compile error: startSpan callback type mismatch | Callback must return Future<T> for startSpan | Use startSpanSync for synchronous work |
import 'package:flutter/widgets.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> main() async {
await SentryFlutter.init((options) {
options.dsn = '__DSN__';
options.tracesSampleRate = 1.0;
options.traceLifecycle = SentryTraceLifecycle.stream;
}, appRunner: () => runApp(const MyApp()));
await Sentry.startSpan('load-config', (span) async {
span.setAttribute('source', SentryAttribute.string('remote'));
await loadConfig();
});
}
import 'package:sentry/sentry.dart';
Future<void> main() async {
await Sentry.init((options) {
options.dsn = '__DSN__';
options.tracesSampleRate = 1.0;
options.traceLifecycle = SentryTraceLifecycle.stream;
});
await Sentry.startSpan('main-task', (span) async {
span.setAttribute('job.id', SentryAttribute.string('42'));
await doWork();
});
}
// Status (auto-set to error on throw, ok otherwise)
span.status = SentrySpanStatusV2.error; // error | ok
// Retroactive timing
Sentry.startSpanSync('task', (_) => work(), startTimestamp: start); // also on startSpan
span.end(endTimestamp: end); // on any span
// Parenting (startSpan / startSpanSync / startInactiveSpan)
// default: inherit active span | parentSpan: someSpan -> explicit parent | parentSpan: null -> root
>=9.19.0 (sentry / sentry_flutter)options.traceLifecycle = SentryTraceLifecycle.stream set in every init callSentry.startTransaction calls replaced with Sentry.startSpan / startSpanSync / startInactiveSpanstartChild calls replaced with nested Sentry.startSpan callssetData / setTag on spans replaced with typed setAttribute / SentryAttributebeforeSendTransaction logic migrated to beforeSendSpan or ignoreSpans, option removedstartInactiveSpan spans have a guaranteed end() call pathdart analyze / flutter analyze passesdevelopment
Migrate JavaScript SDK to Sentry span streaming (span-first trace lifecycle). Use when asked to "enable span streaming", "migrate to span streaming", "use traceLifecycle stream", "add spanStreamingIntegration", or switch from transaction-based to streamed span delivery in a JavaScript project.
development
Migrate Python SDK to Sentry span streaming (span-first trace lifecycle). Use when asked to "enable span streaming", "migrate to span streaming", "use trace_lifecycle stream", or switch from transaction-based to streamed span delivery in a Python project.
development
Keep Sentry SDKs up to date. Use when asked to upgrade the Sentry SDK across major versions, migrate SDK versions, or fix deprecated APIs.
testing
Full Sentry Snapshots setup for Apple/Cocoa projects. Use when asked to "setup SnapshotPreviews", "setup Apple snapshot testing", "upload Apple snapshots to Sentry", "setup Apple snapshot GitHub Actions", or "setup Apple selective snapshot testing".