
sentry-span-streaming-dart
โ 233by getsentry ยท part of getsentry/sentry-for-claude
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.
This is the playbook your agent receives when the skill activates โ you don't need to read it to use the skill, but it's here to audit before installing.
All Skills > Feature Setup > Span Streaming (Dart)
Sentry Span Streaming Migration (Dart/Flutter)
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).
Invoke This Skill When
- User asks to "enable span streaming" or "migrate to span streaming" in a Dart or Flutter project
- User wants to switch from transaction-based to streamed span delivery
- User mentions
traceLifecycle,SentryTraceLifecycle.stream,Sentry.startSpan, orSentrySpanV2 - User wants lower latency span delivery or per-span processing
Phase 1: Detect
1.1 Detect SDK and Version
# 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 versionSpan streaming requires sentry / sentry_flutter >=9.19.0.
1.2 Find Existing Sentry Config and Tracing Usage
# 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 -201.3 Classify the Migration Work
| 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 |
Phase 2: Migrate
Prerequisites: sentry / sentry_flutter >=9.19.0 with tracing enabled (tracesSampleRate or tracesSampler configured).
2.1 Enable Span Streaming
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:
- In
streammode, the transaction APIs (Sentry.startTransaction,ISentrySpan.startChild) do nothing and log a warning. - In
staticmode (the default), the new span APIs (Sentry.startSpan) do nothing. - Auto-instrumentations automatically switch to the correct API based on this setting.
2.2 Replace Transactions with Spans
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.
2.3 Spans That Outlive a Callback
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);2.4 Migrate setData / setTag to Typed Attributes
Streamed 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 |
2.5 Migrate beforeSendTransaction
beforeSendTransaction 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.
2.6 Configure 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.
2.7 Flutter Auto-Instrumentation
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.
2.8 Sampling
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.
Phase 3: Verify
3.1 Static Check
dart analyze 2>&1 | head -30
# or for Flutter projects
flutter analyze 2>&1 | head -30Expect no new analyzer errors from the migration.
3.2 Runtime Verification
Instruct the user to verify with options.debug = true or network inspection:
- Envelope content type: span envelopes are sent with content type
application/vnd.sentry.items.span.v2+jsoninstead of transaction envelopes. Spans are buffered briefly and flushed in batches, so expect a short delay before envelopes appear. - Sentry dashboard: spans appear in the Traces view shortly after each span completes, without waiting for a whole transaction to finish.
- No-op warnings: a log line
startTransaction is not supported when traceLifecycle is 'stream'means old transaction API calls remain โ find and migrate them.
3.3 Common Issues
| 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 |
Quick Reference
Minimal Flutter Setup
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();
});
}Minimal Dart Setup
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();
});
}Span Control Reference
// 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 -> rootFull Migration Checklist
- SDK version is
>=9.19.0(sentry/sentry_flutter) -
options.traceLifecycle = SentryTraceLifecycle.streamset in every init call - All
Sentry.startTransactioncalls replaced withSentry.startSpan/startSpanSync/startInactiveSpan - All
startChildcalls replaced with nestedSentry.startSpancalls -
setData/setTagon spans replaced with typedsetAttribute/SentryAttribute -
beforeSendTransactionlogic migrated tobeforeSendSpanorignoreSpans, option removed -
startInactiveSpanspans have a guaranteedend()call path -
dart analyze/flutter analyzepasses - Spans visible in Sentry Traces view
npx skills add https://github.com/getsentry/sentry-for-claude --skill sentry-span-streaming-dartRun this in your project โ your agent picks the skill up automatically.
No common issues documented yet. If you hit a problem, the repository's GitHub Issues page is the best place to look.
Licensed under MITโ you can use, modify, and redistribute it under that license's terms.
View the full license file on GitHub โ