From 52c06cbe1b6385ad422b19a870f689e93ee31e8f Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 10 Mar 2026 17:17:53 +0100 Subject: [PATCH 1/6] feat(core,node-core,node): Add server-side span streaming implementation --- packages/astro/src/index.types.ts | 1 + packages/core/src/index.ts | 1 + packages/nextjs/src/index.types.ts | 1 + packages/node-core/src/common-exports.ts | 1 + packages/node-core/src/light/sdk.ts | 5 ++ packages/node-core/src/sdk/client.ts | 1 + packages/node-core/src/sdk/index.ts | 5 ++ packages/node/src/index.ts | 1 + packages/nuxt/src/index.types.ts | 1 + packages/opentelemetry/src/spanProcessor.ts | 90 +++++++++---------- packages/react-router/src/index.types.ts | 1 + packages/remix/src/index.types.ts | 1 + packages/solidstart/src/index.types.ts | 1 + packages/sveltekit/src/index.types.ts | 1 + .../tanstackstart-react/src/index.types.ts | 1 + 15 files changed, 65 insertions(+), 47 deletions(-) diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index 2e51ab1b0f1e..8dfffdcbd852 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -20,6 +20,7 @@ export declare function init(options: Options | clientSdk.BrowserOptions | NodeO export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; +export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a2af055232a1..539405f09adc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -184,6 +184,7 @@ export type { export { SpanBuffer } from './tracing/spans/spanBuffer'; export { hasSpanStreamingEnabled } from './tracing/spans/hasSpanStreamingEnabled'; +export { spanStreamingIntegration } from './integrations/spanStreaming'; export type { FeatureFlag } from './utils/featureFlags'; diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 7c92fecd7834..1ac235711ca5 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -23,6 +23,7 @@ export declare function init( export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; +export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; // Different implementation in server and worker export declare const vercelAIIntegration: typeof serverSdk.vercelAIIntegration; diff --git a/packages/node-core/src/common-exports.ts b/packages/node-core/src/common-exports.ts index 3fff4100b352..4877fc159e88 100644 --- a/packages/node-core/src/common-exports.ts +++ b/packages/node-core/src/common-exports.ts @@ -115,6 +115,7 @@ export { consoleIntegration, wrapMcpServerWithSentry, featureFlagsIntegration, + spanStreamingIntegration, metrics, envToBool, } from '@sentry/core'; diff --git a/packages/node-core/src/light/sdk.ts b/packages/node-core/src/light/sdk.ts index ef9229f28de6..1edacca2b07a 100644 --- a/packages/node-core/src/light/sdk.ts +++ b/packages/node-core/src/light/sdk.ts @@ -12,6 +12,7 @@ import { linkedErrorsIntegration, propagationContextFromHeaders, requestDataIntegration, + spanStreamingIntegration, stackParserFromStackParserOptions, } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; @@ -112,6 +113,10 @@ function _init( ); } + if (options.traceLifecycle === 'stream' && !options.integrations.some(({ name }) => name === 'SpanStreaming')) { + options.integrations.push(spanStreamingIntegration()); + } + applySdkMetadata(options, 'node-light', ['node-core']); const client = new LightNodeClient(options); diff --git a/packages/node-core/src/sdk/client.ts b/packages/node-core/src/sdk/client.ts index 80a233aa3954..211610d6491c 100644 --- a/packages/node-core/src/sdk/client.ts +++ b/packages/node-core/src/sdk/client.ts @@ -9,6 +9,7 @@ import { _INTERNAL_flushLogsBuffer, applySdkMetadata, debug, + hasSpanStreamingEnabled, SDK_VERSION, ServerRuntimeClient, } from '@sentry/core'; diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts index de1569135cfd..5b495ad6bf8a 100644 --- a/packages/node-core/src/sdk/index.ts +++ b/packages/node-core/src/sdk/index.ts @@ -14,6 +14,7 @@ import { linkedErrorsIntegration, propagationContextFromHeaders, requestDataIntegration, + spanStreamingIntegration, stackParserFromStackParserOptions, } from '@sentry/core'; import { @@ -126,6 +127,10 @@ function _init( ); } + if (options.traceLifecycle === 'stream' && !options.integrations.some(({ name }) => name === 'SpanStreaming')) { + options.integrations.push(spanStreamingIntegration()); + } + applySdkMetadata(options, 'node-core'); const client = new NodeClient(options); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 8458dee5f6a7..9e1e892ed7ce 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -140,6 +140,7 @@ export { consoleIntegration, wrapMcpServerWithSentry, featureFlagsIntegration, + spanStreamingIntegration, createLangChainCallbackHandler, instrumentLangGraph, instrumentStateGraphCompile, diff --git a/packages/nuxt/src/index.types.ts b/packages/nuxt/src/index.types.ts index 7109e7ad9c78..9a0283cfcee8 100644 --- a/packages/nuxt/src/index.types.ts +++ b/packages/nuxt/src/index.types.ts @@ -16,6 +16,7 @@ export * from './index.server'; export declare function init(options: Options | SentryNuxtClientOptions | SentryNuxtServerOptions): Client | undefined; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; +export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/opentelemetry/src/spanProcessor.ts b/packages/opentelemetry/src/spanProcessor.ts index 3430456caaee..b4b913c5535e 100644 --- a/packages/opentelemetry/src/spanProcessor.ts +++ b/packages/opentelemetry/src/spanProcessor.ts @@ -6,6 +6,7 @@ import { getClient, getDefaultCurrentScope, getDefaultIsolationScope, + hasSpanStreamingEnabled, logSpanEnd, logSpanStart, setCapturedScopesOnSpan, @@ -14,50 +15,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE } from './semanticAttributes import { SentrySpanExporter } from './spanExporter'; import { getScopesFromContext } from './utils/contextData'; import { setIsSetup } from './utils/setupCheck'; - -function onSpanStart(span: Span, parentContext: Context): void { - // This is a reliable way to get the parent span - because this is exactly how the parent is identified in the OTEL SDK - const parentSpan = trace.getSpan(parentContext); - - let scopes = getScopesFromContext(parentContext); - - // We need access to the parent span in order to be able to move up the span tree for breadcrumbs - if (parentSpan && !parentSpan.spanContext().isRemote) { - addChildSpanToSpan(parentSpan, span); - } - - // We need this in the span exporter - if (parentSpan?.spanContext().isRemote) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE, true); - } - - // The root context does not have scopes stored, so we check for this specifically - // As fallback we attach the global scopes - if (parentContext === ROOT_CONTEXT) { - scopes = { - scope: getDefaultCurrentScope(), - isolationScope: getDefaultIsolationScope(), - }; - } - - // We need the scope at time of span creation in order to apply it to the event when the span is finished - if (scopes) { - setCapturedScopesOnSpan(span, scopes.scope, scopes.isolationScope); - } - - logSpanStart(span); - - const client = getClient(); - client?.emit('spanStart', span); -} - -function onSpanEnd(span: Span): void { - logSpanEnd(span); - - const client = getClient(); - client?.emit('spanEnd', span); -} - /** * Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via * the Sentry SDK. @@ -88,13 +45,52 @@ export class SentrySpanProcessor implements SpanProcessorInterface { * @inheritDoc */ public onStart(span: Span, parentContext: Context): void { - onSpanStart(span, parentContext); + // This is a reliable way to get the parent span - because this is exactly how the parent is identified in the OTEL SDK + const parentSpan = trace.getSpan(parentContext); + + let scopes = getScopesFromContext(parentContext); + + // We need access to the parent span in order to be able to move up the span tree for breadcrumbs + if (parentSpan && !parentSpan.spanContext().isRemote) { + addChildSpanToSpan(parentSpan, span); + } + + // We need this in the span exporter + if (parentSpan?.spanContext().isRemote) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE, true); + } + + // The root context does not have scopes stored, so we check for this specifically + // As fallback we attach the global scopes + if (parentContext === ROOT_CONTEXT) { + scopes = { + scope: getDefaultCurrentScope(), + isolationScope: getDefaultIsolationScope(), + }; + } + + // We need the scope at time of span creation in order to apply it to the event when the span is finished + if (scopes) { + setCapturedScopesOnSpan(span, scopes.scope, scopes.isolationScope); + } + + logSpanStart(span); + + const client = getClient(); + client?.emit('spanStart', span); } /** @inheritDoc */ public onEnd(span: Span & ReadableSpan): void { - onSpanEnd(span); + logSpanEnd(span); + + const client = getClient(); + client?.emit('spanEnd', span); - this._exporter.export(span); + if (client && hasSpanStreamingEnabled(client)) { + client.emit('afterSpanEnd', span); + } else { + this._exporter.export(span); + } } } diff --git a/packages/react-router/src/index.types.ts b/packages/react-router/src/index.types.ts index c9c5cb371763..4d0abcb6b0a8 100644 --- a/packages/react-router/src/index.types.ts +++ b/packages/react-router/src/index.types.ts @@ -16,6 +16,7 @@ export declare function init(options: Options | clientSdk.BrowserOptions | serve export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; +export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; export declare const defaultStackParser: StackParser; export declare const getDefaultIntegrations: (options: Options) => Integration[]; diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts index 61d4f7e0b9bb..1cac41c1aacf 100644 --- a/packages/remix/src/index.types.ts +++ b/packages/remix/src/index.types.ts @@ -18,6 +18,7 @@ export declare function init(options: RemixOptions): Client | undefined; export declare const browserTracingIntegration: typeof clientSdk.browserTracingIntegration; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; +export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/solidstart/src/index.types.ts b/packages/solidstart/src/index.types.ts index 4c5ff491c740..58e642ecba22 100644 --- a/packages/solidstart/src/index.types.ts +++ b/packages/solidstart/src/index.types.ts @@ -18,6 +18,7 @@ export declare function init(options: Options | clientSdk.BrowserOptions | serve export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; +export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index d46e88e720ed..78cb27e2e47d 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -47,6 +47,7 @@ export declare function wrapLoadWithSentry any>(orig export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; +export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; // Different implementation in server and worker export declare const vercelAIIntegration: typeof serverSdk.vercelAIIntegration; diff --git a/packages/tanstackstart-react/src/index.types.ts b/packages/tanstackstart-react/src/index.types.ts index 7ab05bfc3831..0545fbf5c87b 100644 --- a/packages/tanstackstart-react/src/index.types.ts +++ b/packages/tanstackstart-react/src/index.types.ts @@ -18,6 +18,7 @@ export declare function init(options: Options | clientSdk.BrowserOptions | serve export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; +export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; From 0a989e3d725fbd309da62ff14510793be2e0199a Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 10 Mar 2026 17:18:39 +0100 Subject: [PATCH 2/6] add new files --- .../core/src/integrations/spanStreaming.ts | 42 ++++++ .../test/integrations/spanStreaming.test.ts | 135 ++++++++++++++++++ .../opentelemetry/test/spanProcessor.test.ts | 57 ++++++++ 3 files changed, 234 insertions(+) create mode 100644 packages/core/src/integrations/spanStreaming.ts create mode 100644 packages/core/test/integrations/spanStreaming.test.ts create mode 100644 packages/opentelemetry/test/spanProcessor.test.ts diff --git a/packages/core/src/integrations/spanStreaming.ts b/packages/core/src/integrations/spanStreaming.ts new file mode 100644 index 000000000000..069382929b2f --- /dev/null +++ b/packages/core/src/integrations/spanStreaming.ts @@ -0,0 +1,42 @@ +import type { IntegrationFn } from '../types-hoist/integration'; +import { DEBUG_BUILD } from '../debug-build'; +import { defineIntegration } from '../integration'; +import { isStreamedBeforeSendSpanCallback } from '../tracing/spans/beforeSendSpan'; +import { captureSpan } from '../tracing/spans/captureSpan'; +import { hasSpanStreamingEnabled } from '../tracing/spans/hasSpanStreamingEnabled'; +import { SpanBuffer } from '../tracing/spans/spanBuffer'; +import { debug } from '../utils/debug-logger'; +import { spanIsSampled } from '../utils/spanUtils'; + +export const spanStreamingIntegration = defineIntegration(() => { + return { + name: 'SpanStreaming', + + setup(client) { + const initialMessage = 'SpanStreaming integration requires'; + const fallbackMsg = 'Falling back to static trace lifecycle.'; + + if (!hasSpanStreamingEnabled(client)) { + DEBUG_BUILD && debug.warn(`${initialMessage} \`traceLifecycle\` to be set to "stream"! ${fallbackMsg}`); + return; + } + + const beforeSendSpan = client.getOptions().beforeSendSpan; + if (beforeSendSpan && !isStreamedBeforeSendSpanCallback(beforeSendSpan)) { + client.getOptions().traceLifecycle = 'static'; + DEBUG_BUILD && + debug.warn(`${initialMessage} a beforeSendSpan callback using \`withStreamedSpan\`! ${fallbackMsg}`); + return; + } + + const buffer = new SpanBuffer(client); + + client.on('afterSpanEnd', span => { + if (!spanIsSampled(span)) { + return; + } + buffer.add(captureSpan(span, client)); + }); + }, + }; +}) satisfies IntegrationFn; diff --git a/packages/core/test/integrations/spanStreaming.test.ts b/packages/core/test/integrations/spanStreaming.test.ts new file mode 100644 index 000000000000..a7b45184145a --- /dev/null +++ b/packages/core/test/integrations/spanStreaming.test.ts @@ -0,0 +1,135 @@ +import * as SentryCore from '../../src'; +import { debug } from '../../src'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { spanStreamingIntegration } from '../../src/integrations/spanStreaming'; +import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; + +const mockSpanBufferInstance = vi.hoisted(() => ({ + flush: vi.fn(), + add: vi.fn(), + drain: vi.fn(), +})); + +const MockSpanBuffer = vi.hoisted(() => { + return vi.fn(() => mockSpanBufferInstance); +}); + +vi.mock('../../src/tracing/spans/spanBuffer', async () => { + const original = await vi.importActual('../../src/tracing/spans/spanBuffer'); + return { + ...original, + SpanBuffer: MockSpanBuffer, + }; +}); + +describe('spanStreamingIntegration (core)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('has the correct name and setup hook', () => { + const integration = spanStreamingIntegration(); + expect(integration.name).toBe('SpanStreaming'); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(integration.setup).toBeDefined(); + }); + + it('logs a warning if traceLifecycle is not set to "stream"', () => { + const debugSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); + const client = new TestClient({ + ...getDefaultTestClientOptions(), + dsn: 'https://username@domain/123', + integrations: [spanStreamingIntegration()], + traceLifecycle: 'static', + }); + + SentryCore.setCurrentClient(client); + client.init(); + + expect(debugSpy).toHaveBeenCalledWith( + 'SpanStreaming integration requires `traceLifecycle` to be set to "stream"! Falling back to static trace lifecycle.', + ); + debugSpy.mockRestore(); + + expect(client.getOptions().traceLifecycle).toBe('static'); + }); + + it('falls back to static trace lifecycle if beforeSendSpan is not compatible with span streaming', () => { + const debugSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); + const client = new TestClient({ + ...getDefaultTestClientOptions(), + dsn: 'https://username@domain/123', + integrations: [spanStreamingIntegration()], + traceLifecycle: 'stream', + beforeSendSpan: (span: SentryCore.SpanJSON) => span, + }); + + SentryCore.setCurrentClient(client); + client.init(); + + expect(debugSpy).toHaveBeenCalledWith( + 'SpanStreaming integration requires a beforeSendSpan callback using `withStreamedSpan`! Falling back to static trace lifecycle.', + ); + debugSpy.mockRestore(); + + expect(client.getOptions().traceLifecycle).toBe('static'); + }); + + it('sets up buffer when traceLifecycle is "stream"', () => { + const client = new TestClient({ + ...getDefaultTestClientOptions(), + dsn: 'https://username@domain/123', + integrations: [spanStreamingIntegration()], + traceLifecycle: 'stream', + }); + + SentryCore.setCurrentClient(client); + client.init(); + + expect(MockSpanBuffer).toHaveBeenCalledWith(client); + expect(client.getOptions().traceLifecycle).toBe('stream'); + }); + + it('enqueues a span into the buffer when the span ends', () => { + const client = new TestClient({ + ...getDefaultTestClientOptions(), + dsn: 'https://username@domain/123', + integrations: [spanStreamingIntegration()], + traceLifecycle: 'stream', + tracesSampleRate: 1, + }); + + SentryCore.setCurrentClient(client); + client.init(); + + const span = new SentryCore.SentrySpan({ name: 'test', sampled: true }); + client.emit('afterSpanEnd', span); + + expect(mockSpanBufferInstance.add).toHaveBeenCalledWith( + expect.objectContaining({ + _segmentSpan: span, + trace_id: span.spanContext().traceId, + span_id: span.spanContext().spanId, + name: 'test', + }), + ); + }); + + it('does not enqueue a span into the buffer when the span is not sampled', () => { + const client = new TestClient({ + ...getDefaultTestClientOptions(), + dsn: 'https://username@domain/123', + integrations: [spanStreamingIntegration()], + traceLifecycle: 'stream', + tracesSampleRate: 1, + }); + + SentryCore.setCurrentClient(client); + client.init(); + + const span = new SentryCore.SentrySpan({ name: 'test', sampled: false }); + client.emit('afterSpanEnd', span); + + expect(mockSpanBufferInstance.add).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/opentelemetry/test/spanProcessor.test.ts b/packages/opentelemetry/test/spanProcessor.test.ts new file mode 100644 index 000000000000..2e3b0b5b999e --- /dev/null +++ b/packages/opentelemetry/test/spanProcessor.test.ts @@ -0,0 +1,57 @@ +import { getClient, startInactiveSpan } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { SentrySpanExporter } from '../src/spanExporter'; +import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; + +const exportSpy = vi.spyOn(SentrySpanExporter.prototype, 'export'); + +describe('SentrySpanProcessor', () => { + beforeEach(() => { + exportSpy.mockClear(); + }); + + describe('with traceLifecycle: static (default)', () => { + beforeEach(() => { + mockSdkInit({ tracesSampleRate: 1 }); + }); + + afterEach(async () => { + await cleanupOtel(); + }); + + it('exports spans via the exporter', () => { + const span = startInactiveSpan({ name: 'test' }); + span.end(); + + expect(exportSpy).toHaveBeenCalled(); + }); + }); + + describe('with traceLifecycle: stream', () => { + beforeEach(() => { + mockSdkInit({ tracesSampleRate: 1, traceLifecycle: 'stream' }); + }); + + afterEach(async () => { + await cleanupOtel(); + }); + + it('does not export spans via the exporter', () => { + const span = startInactiveSpan({ name: 'test' }); + span.end(); + + expect(exportSpy).not.toHaveBeenCalled(); + }); + + it('emits afterSpanEnd', () => { + const afterSpanEndCallback = vi.fn(); + const client = getClient()!; + client.on('afterSpanEnd', afterSpanEndCallback); + + const span = startInactiveSpan({ name: 'test' }); + span.end(); + + expect(afterSpanEndCallback).toHaveBeenCalledWith(span); + }); + }); +}); From e3e9a86a3148223ed5a860f935807ce72cb6d532 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 10 Mar 2026 18:29:10 +0100 Subject: [PATCH 3/6] use `getDefaultIntegrations` to add/not add span streaming integration --- packages/node-core/src/light/sdk.ts | 7 ++----- packages/node-core/src/sdk/index.ts | 7 ++----- packages/node-core/test/light/sdk.test.ts | 14 ++++++++++++++ packages/node/src/sdk/index.ts | 3 ++- packages/node/test/sdk/init.test.ts | 22 ++++++++++++++++++++++ 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/node-core/src/light/sdk.ts b/packages/node-core/src/light/sdk.ts index 1edacca2b07a..1d46d99dd134 100644 --- a/packages/node-core/src/light/sdk.ts +++ b/packages/node-core/src/light/sdk.ts @@ -39,7 +39,7 @@ import { nativeNodeFetchIntegration } from './integrations/nativeNodeFetchIntegr /** * Get default integrations for the Light Node-Core SDK. */ -export function getDefaultIntegrations(): Integration[] { +export function getDefaultIntegrations(options?: Options): Integration[] { return [ // Common eventFiltersIntegration(), @@ -61,6 +61,7 @@ export function getDefaultIntegrations(): Integration[] { childProcessIntegration(), processSessionIntegration(), modulesIntegration(), + ...(options?.traceLifecycle === 'stream' ? [spanStreamingIntegration()] : []), ]; } @@ -113,10 +114,6 @@ function _init( ); } - if (options.traceLifecycle === 'stream' && !options.integrations.some(({ name }) => name === 'SpanStreaming')) { - options.integrations.push(spanStreamingIntegration()); - } - applySdkMetadata(options, 'node-light', ['node-core']); const client = new LightNodeClient(options); diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts index 5b495ad6bf8a..c87d58c5edac 100644 --- a/packages/node-core/src/sdk/index.ts +++ b/packages/node-core/src/sdk/index.ts @@ -47,7 +47,7 @@ import { initializeEsmLoader } from './esmLoader'; /** * Get default integrations for the Node-Core SDK. */ -export function getDefaultIntegrations(): Integration[] { +export function getDefaultIntegrations(options?: Options): Integration[] { return [ // Common // TODO(v11): Replace with `eventFiltersIntegration` once we remove the deprecated `inboundFiltersIntegration` @@ -72,6 +72,7 @@ export function getDefaultIntegrations(): Integration[] { childProcessIntegration(), processSessionIntegration(), modulesIntegration(), + ...(options?.traceLifecycle === 'stream' ? [spanStreamingIntegration()] : []), ]; } @@ -127,10 +128,6 @@ function _init( ); } - if (options.traceLifecycle === 'stream' && !options.integrations.some(({ name }) => name === 'SpanStreaming')) { - options.integrations.push(spanStreamingIntegration()); - } - applySdkMetadata(options, 'node-core'); const client = new NodeClient(options); diff --git a/packages/node-core/test/light/sdk.test.ts b/packages/node-core/test/light/sdk.test.ts index 8b0ef03700d9..522ccd1f55e6 100644 --- a/packages/node-core/test/light/sdk.test.ts +++ b/packages/node-core/test/light/sdk.test.ts @@ -106,6 +106,20 @@ describe('Light Mode | SDK', () => { expect(integrationNames).toContain('NodeFetch'); }); + + it('includes spanStreaming integration when traceLifecycle is "stream"', () => { + const integrations = Sentry.getDefaultIntegrations({ traceLifecycle: 'stream' }); + const integrationNames = integrations.map(i => i.name); + + expect(integrationNames).toContain('SpanStreaming'); + }); + + it("doesn't include spanStreaming integration when traceLifecycle is not 'stream'", () => { + const integrations = Sentry.getDefaultIntegrations(); + const integrationNames = integrations.map(i => i.name); + + expect(integrationNames).not.toContain('SpanStreaming'); + }); }); describe('isInitialized', () => { diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 6942c6500f84..0e74edf602eb 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -1,5 +1,5 @@ import type { Integration, Options } from '@sentry/core'; -import { applySdkMetadata, hasSpansEnabled } from '@sentry/core'; +import { applySdkMetadata, hasSpansEnabled, spanStreamingIntegration } from '@sentry/core'; import type { NodeClient } from '@sentry/node-core'; import { getDefaultIntegrations as getNodeCoreDefaultIntegrations, @@ -33,6 +33,7 @@ export function getDefaultIntegrations(options: Options): Integration[] { // This means that generally request isolation will work (because that is done by httpIntegration) // But `transactionName` will not be set automatically ...(hasSpansEnabled(options) ? getAutoPerformanceIntegrations() : []), + ...(options.traceLifecycle === 'stream' ? [spanStreamingIntegration()] : []), ]; } diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index a6a76a4439bd..101a82e3b6d1 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -143,6 +143,28 @@ describe('init()', () => { }), ); }); + + it('installs spanStreaming integration when traceLifecycle is "stream"', () => { + init({ dsn: PUBLIC_DSN, traceLifecycle: 'stream' }); + const client = getClient(); + + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + integrations: expect.arrayContaining([expect.objectContaining({ name: 'SpanStreaming' })]), + }), + ); + }); + + it("doesn't install spanStreaming integration when traceLifecycle is not 'stream'", () => { + init({ dsn: PUBLIC_DSN }); + + const client = getClient(); + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + integrations: expect.not.arrayContaining([expect.objectContaining({ name: 'SpanStreaming' })]), + }), + ); + }); }); describe('OpenTelemetry', () => { From 17df9599f5db9e8a4ed8573d44a1fe23460cdd01 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 10 Mar 2026 22:57:15 +0100 Subject: [PATCH 4/6] bump size limits --- .size-limit.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 3a4689d59faa..aef521dc83f6 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -117,7 +117,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'metrics'), gzip: true, - limit: '27 KB', + limit: '28 KB', }, { name: '@sentry/browser (incl. Logs)', @@ -220,13 +220,13 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing, Replay, Feedback)', path: createCDNPath('bundle.tracing.replay.feedback.min.js'), gzip: true, - limit: '86 KB', + limit: '87 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)', path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'), gzip: true, - limit: '87 KB', + limit: '88 KB', }, // browser CDN bundles (non-gzipped) { @@ -317,7 +317,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '53 KB', + limit: '55 KB', }, // Node SDK (ESM) { @@ -326,14 +326,14 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '175 KB', + limit: '177 KB', }, { name: '@sentry/node - without tracing', path: 'packages/node/build/esm/index.js', import: createImport('initWithoutDefaultIntegrations', 'getDefaultIntegrationsWithoutPerformance'), gzip: true, - limit: '98 KB', + limit: '100 KB', ignore: [...builtinModules, ...nodePrefixedBuiltinModules], modifyWebpackConfig: function (config) { const webpack = require('webpack'); @@ -356,7 +356,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '114 KB', + limit: '116 KB', }, ]; From e8c9f048379c38bd9b00fdbb752c165483163c1b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 10 Mar 2026 23:05:08 +0100 Subject: [PATCH 5/6] fix missing exports --- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/remix/src/server/index.ts | 1 + packages/solidstart/src/server/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + 7 files changed, 7 insertions(+) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index ac01ff0647a7..cf4c6dc6e9bb 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -170,6 +170,7 @@ export { statsigIntegration, unleashIntegration, growthbookIntegration, + spanStreamingIntegration, metrics, } from '@sentry/node'; diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 1c980e4cae2d..b49b214b65d7 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -157,6 +157,7 @@ export { unleashIntegration, growthbookIntegration, metrics, + spanStreamingIntegration, } from '@sentry/node'; export { diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index c2990e6262a7..6f52a92ab6da 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -177,6 +177,7 @@ export { statsigIntegration, unleashIntegration, metrics, + spanStreamingIntegration, } from '@sentry/node'; export { diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 9478b98f5a58..a881f911e31c 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -157,6 +157,7 @@ export { statsigIntegration, unleashIntegration, metrics, + spanStreamingIntegration, } from '@sentry/node'; export { diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index 1533a1ca7221..eccafe08a637 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -131,6 +131,7 @@ export { consoleLoggingIntegration, createConsolaReporter, createSentryWinstonTransport, + spanStreamingIntegration, } from '@sentry/node'; // Keeping the `*` exports for backwards compatibility and types diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 6e2bc1cb9f61..ffc73956a530 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -130,6 +130,7 @@ export { consoleLoggingIntegration, createConsolaReporter, createSentryWinstonTransport, + spanStreamingIntegration, } from '@sentry/node'; // We can still leave this for the carrier init and type exports diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index d42975ef7876..8db039bb4369 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -135,6 +135,7 @@ export { createSentryWinstonTransport, vercelAIIntegration, metrics, + spanStreamingIntegration, } from '@sentry/node'; // We can still leave this for the carrier init and type exports From 39783482e16801400d8d5ebc8268bb6d8f28c1df Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 10 Mar 2026 23:43:25 +0100 Subject: [PATCH 6/6] fix unused import --- packages/node-core/src/sdk/client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/node-core/src/sdk/client.ts b/packages/node-core/src/sdk/client.ts index 211610d6491c..80a233aa3954 100644 --- a/packages/node-core/src/sdk/client.ts +++ b/packages/node-core/src/sdk/client.ts @@ -9,7 +9,6 @@ import { _INTERNAL_flushLogsBuffer, applySdkMetadata, debug, - hasSpanStreamingEnabled, SDK_VERSION, ServerRuntimeClient, } from '@sentry/core';