| name | sentry-span-streaming |
| description | Migrate 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. |
| license | Apache-2.0 |
| category | feature-setup |
| parent | sentry-feature-setup |
| disable-model-invocation | true |
| allowed-tools | Bash, Read, Edit, Write, Grep, Glob |
All Skills > Feature Setup > Span Streaming
Sentry Span Streaming Migration
Migrate from the default transaction-based trace lifecycle (static) to span streaming (stream), where spans are sent in multiple batches as they complete instead of being batched into one transaction at the end.
Invoke This Skill When
- User asks to "enable span streaming" or "migrate to span streaming"
- User wants to switch from transaction-based to streamed span delivery
- User mentions
traceLifecycle, spanStreamingIntegration, or withStreamedSpan
- User wants lower latency span delivery or per-span processing
Supported Platforms
| Platform | Status |
|---|
| JavaScript (Browser, Node.js, Bun, Deno, Cloudflare) | Supported |
| Python | Not yet available |
| Ruby | Not yet available |
| Go | Not yet available |
| Other SDKs | Not yet available |
If the user's project does not use a supported SDK, inform them that span streaming is currently only available for JavaScript SDKs and stop here.
Phase 1: Detect
Identify the user's platform, SDK version, and current tracing configuration.
1.1 Detect Platform and SDK
cat package.json 2>/dev/null | grep -E '"@sentry/'
cat requirements.txt setup.py pyproject.toml 2>/dev/null | grep -i sentry
cat Gemfile 2>/dev/null | grep sentry
cat go.mod 2>/dev/null | grep sentry
If an unsupported Sentry SDK is detected, inform the user that span streaming is not yet available for their platform.
1.2 Detect JavaScript Environment
grep -rn "from '@sentry/browser'\|from '@sentry/react'\|from '@sentry/vue'\|from '@sentry/angular'\|from '@sentry/svelte'\|from '@sentry/nextjs'\|from '@sentry/nuxt'\|from '@sentry/sveltekit'\|from '@sentry/remix'\|from '@sentry/solidstart'\|from '@sentry/astro'\|from '@sentry/react-router'" --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" --include="*.mjs" -l 2>/dev/null | head -20
grep -rn "from '@sentry/node'\|from '@sentry/bun'\|from '@sentry/deno'\|from '@sentry/cloudflare'" --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" --include="*.mjs" -l 2>/dev/null | head -20
1.3 Find Existing Sentry Config
grep -rn "Sentry\.init\|init({" --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" --include="*.mjs" -l 2>/dev/null | head -20
grep -rn "beforeSendSpan" --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" --include="*.mjs" -l 2>/dev/null
grep -rn "beforeSendTransaction" --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" --include="*.mjs" -l 2>/dev/null
grep -rn "ignoreSpans" --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" --include="*.mjs" -l 2>/dev/null
1.4 Classify Environment
Based on detection results, classify each Sentry.init call as:
| Environment | Packages | Migration Path |
|---|
| Browser | @sentry/browser, @sentry/react, @sentry/vue, @sentry/angular, @sentry/svelte | Add spanStreamingIntegration() |
| Server | @sentry/node, @sentry/bun, @sentry/deno, @sentry/cloudflare | Add traceLifecycle: 'stream' |
| Framework (both) | @sentry/nextjs, @sentry/nuxt, @sentry/sveltekit, @sentry/remix, @sentry/astro, @sentry/solidstart, @sentry/react-router | Migrate both client and server configs separately |
Phase 2: Migrate (JavaScript)
Prerequisites: Sentry JavaScript SDK >=10.53.1 with tracing enabled (tracesSampleRate or tracesSampler configured).
Apply changes to each Sentry.init call. Work through each file identified in Phase 1.
2.1 Enable Span Streaming
Server-Side SDKs
Add traceLifecycle: 'stream' to Sentry.init():
Sentry.init({
dsn: '...',
tracesSampleRate: 1.0,
});
Sentry.init({
dsn: '...',
tracesSampleRate: 1.0,
traceLifecycle: 'stream',
});
Browser-Side SDKs
Add Sentry.spanStreamingIntegration() to the integrations array. The integration automatically enables traceLifecycle: 'stream' — you do not need to set it manually.
Sentry.init({
dsn: '...',
integrations: [
Sentry.browserTracingIntegration(),
],
tracesSampleRate: 1.0,
});
Sentry.init({
dsn: '...',
integrations: [
Sentry.spanStreamingIntegration(),
Sentry.browserTracingIntegration(),
],
tracesSampleRate: 1.0,
});
The order of spanStreamingIntegration() relative to other integrations does not matter.
Framework SDKs (Client + Server)
Apply the browser migration to client config files and the server migration to server config files. Common patterns:
| Framework | Client Config | Server Config |
|---|
| Next.js | sentry.client.config.ts | sentry.server.config.ts, sentry.edge.config.ts |
| Nuxt | Client-side Sentry.init in module | Server-side Sentry.init in module |
| SvelteKit | src/hooks.client.ts | src/hooks.server.ts |
| Remix | entry.client.tsx | entry.server.tsx |
| Astro | Client-side init | Server-side init |
2.2 Migrate beforeSendSpan
If the user has a beforeSendSpan callback, it must be wrapped with Sentry.withStreamedSpan() to work in streaming mode. Without this wrapper, the SDK falls back to static mode.
The callback shape also changes:
description is now name
data is now attributes
- The span object is
StreamedSpanJSON instead of SpanJSON
Sentry.init({
beforeSendSpan: (span) => {
if (span.description?.includes('/health')) {
span.description = '[filtered]';
}
delete span.data?.['http.request.body'];
return span;
},
});
Sentry.init({
beforeSendSpan: Sentry.withStreamedSpan((span) => {
if (span.name?.includes('/health')) {
span.name = '[filtered]';
}
if (span.attributes) {
delete span.attributes['http.request.body'];
}
return span;
}),
});
Key differences in the callback:
Static (SpanJSON) | Streaming (StreamedSpanJSON) |
|---|
span.description | span.name |
span.data (processed attributes) | span.attributes (raw attributes) |
span.timestamp (end time) | span.end_timestamp |
span.status (optional string) | span.status ('ok' or 'error') |
span.op | span.attributes['sentry.op'] |
Returning null from beforeSendSpan does not drop the span — it is ignored and a warning is logged.
2.3 Remove or Replace beforeSendTransaction
beforeSendTransaction has no effect in streaming mode. Spans are sent individually, not batched into transactions.
Sentry.init({
beforeSendTransaction: (event) => {
if (event.transaction === '/health') {
return null;
}
return event;
},
});
Migration paths depending on what beforeSendTransaction was used for:
| Use Case | Streaming Replacement |
|---|
| Drop spans by name/route | Use ignoreSpans option |
| Modify span data before send | Use beforeSendSpan with withStreamedSpan |
| Filter by transaction name | Use ignoreSpans with string/RegExp pattern |
| Add tags/context to transaction | Use beforeSendSpan with withStreamedSpan |
Remove the beforeSendTransaction option from Sentry.init() after migrating its logic.
2.4 Configure ignoreSpans (Optional)
ignoreSpans works in both static and streaming modes, but the filter is evaluated at different points in the span lifecycle:
- Streaming mode: evaluated when the span starts. Only data available at span start — the span name and the attributes set at creation — is taken into account.
- Static mode: evaluated when the root span ends. Only data available at that point — the span name and attributes — is taken into account.
In both modes, a match prevents the span from being recorded or sent. Because matching can run as early as span start (streaming), only the span name and attributes set when the span begins are guaranteed to be available — do not rely on attributes added later in the span's lifetime.
Sentry.init({
traceLifecycle: 'stream',
ignoreSpans: [
'/health',
'/ready',
/^OPTIONS /,
{
op: 'middleware.handle',
name: /^corsMiddleware/,
},
{
op: 'http.server',
attributes: {
'http.route': /^\/internal\//,
},
},
],
});
Filter object properties:
| Property | Type | Matches Against |
|---|
name | string | RegExp | Span name (description) |
op | string | RegExp | Span operation |
attributes | Record<string, string | RegExp | number | boolean | Array> | Span attributes |
When multiple properties are specified in a filter object, all must match for the span to be ignored.
2.5 Set Up Browser Profiling (Optional)
When using span streaming in the browser, use the v2 profiling options — not the legacy profilesSampleRate. The legacy option is deprecated and does not integrate with the span streaming lifecycle.
Add browserProfilingIntegration() and configure the two v2 options:
Sentry.init({
integrations: [
Sentry.browserTracingIntegration(),
],
tracesSampleRate: 1.0,
profilesSampleRate: 0.5,
});
Sentry.init({
integrations: [
Sentry.spanStreamingIntegration(),
Sentry.browserTracingIntegration(),
Sentry.browserProfilingIntegration(),
],
tracesSampleRate: 1.0,
profileSessionSampleRate: 1.0,
profileLifecycle: 'trace',
});
v2 profiling options:
| Option | Type | Description |
|---|
profileSessionSampleRate | number (0–1) | Percentage of user sessions that have profiling enabled. Default: 0 (disabled). |
profileLifecycle | 'trace' | 'manual' | 'trace': profiler runs automatically while sampled root spans exist. 'manual': start/stop profiler explicitly via Sentry.uiProfiler.startProfiler() / stopProfiler(). Default: 'manual'. |
profileLifecycle: 'trace' requires tracing to be enabled (tracesSampleRate or tracesSampler). The profiler starts when a root span begins and stops when no sampled root spans remain. Profile chunks are sent independently every 60 seconds or when the last root span ends.
Do not mix legacy and v2 options. If profilesSampleRate is set, profileSessionSampleRate has no effect and the SDK logs a warning.
Phase 3: Verify
After applying changes, verify the migration works correctly.
3.1 Build Check
npx tsc --noEmit 2>&1 | head -30
npm run build 2>&1 | tail -20
3.2 Runtime Verification
Instruct the user to verify in their browser devtools or server logs:
- Check network tab: Span envelopes should appear as individual requests with content type
application/vnd.sentry.items.span.v2+json rather than transaction envelopes
- Check Sentry dashboard: Spans should appear in the Traces view shortly after they complete, without waiting for the full transaction to finish
- Check for fallback warnings: If the SDK logs warnings about falling back to static mode, the
beforeSendSpan callback is likely missing the withStreamedSpan wrapper
3.3 Common Issues
| Symptom | Cause | Fix |
|---|
| SDK falls back to static mode | beforeSendSpan not wrapped with withStreamedSpan | Wrap callback: Sentry.withStreamedSpan(callback) |
beforeSendTransaction not called | Expected in streaming mode | Migrate logic to beforeSendSpan or ignoreSpans |
| Spans still arrive as transactions | traceLifecycle not set or integration missing | Server: add traceLifecycle: 'stream'; Browser: add spanStreamingIntegration() |
Type errors on span.description | StreamedSpanJSON uses name not description | Change span.description to span.name in callback |
Type errors on span.data | StreamedSpanJSON uses attributes not data | Change span.data to span.attributes in callback |
profileSessionSampleRate has no effect | Legacy profilesSampleRate is also set | Remove profilesSampleRate and use only profileSessionSampleRate + profileLifecycle |
Quick Reference (JavaScript)
Minimal Server Setup
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: '__DSN__',
tracesSampleRate: 1.0,
traceLifecycle: 'stream',
});
Minimal Browser Setup
import * as Sentry from '@sentry/browser';
Sentry.init({
dsn: '__DSN__',
integrations: [
Sentry.spanStreamingIntegration(),
Sentry.browserTracingIntegration(),
],
tracesSampleRate: 1.0,
});
Browser Setup with Profiling
import * as Sentry from '@sentry/browser';
Sentry.init({
dsn: '__DSN__',
integrations: [
Sentry.spanStreamingIntegration(),
Sentry.browserTracingIntegration(),
Sentry.browserProfilingIntegration(),
],
tracesSampleRate: 1.0,
profileSessionSampleRate: 1.0,
profileLifecycle: 'trace',
});
Full Migration Checklist