From 2eeaff8c1194b9a61fc4c0444386c2b64344af21 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 24 Jul 2026 17:44:38 +0200 Subject: [PATCH] feat(core)!: Enable span streaming by default Default clients to the streamed trace lifecycle and automatically install the required streaming integration in browser and server runtimes. Explicit static lifecycle configuration remains supported. BREAKING CHANGE: Spans use the streamed trace lifecycle unless configured otherwise. Fixes #22344 Co-Authored-By: Cursor --- packages/browser/src/sdk.ts | 15 ++++++--- packages/browser/test/sdk.test.ts | 37 ++++++++++++++++++++++ packages/cloudflare/test/sdk.test.ts | 7 ++-- packages/core/src/client.ts | 2 +- packages/core/src/server-runtime-client.ts | 2 ++ packages/core/src/types/options.ts | 2 +- packages/core/test/lib/client.test.ts | 16 +++++++++- packages/deno/test/sdk.test.ts | 8 ++--- packages/node/test/sdk/init.test.ts | 18 ++++------- packages/vercel-edge/test/sdk.test.ts | 10 +++--- 10 files changed, 86 insertions(+), 31 deletions(-) diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index f9ee4938ace1..0309aed98248 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -19,6 +19,7 @@ import { globalHandlersIntegration } from './integrations/globalhandlers'; import { httpContextIntegration } from './integrations/httpcontext'; import { linkedErrorsIntegration } from './integrations/linkederrors'; import { spotlightBrowserIntegration } from './integrations/spotlight'; +import { spanStreamingIntegration } from './integrations/spanstreaming'; import { defaultStackParser } from './stack-parsers'; import { makeFetchTransport } from './transports/fetch'; import { normalizeStringifyValue } from './normalizeStringifyValue'; @@ -110,14 +111,20 @@ export function init(options: BrowserOptions = {}): Client | undefined { } /*! rollup-include-development-only-end */ + const integrations = getIntegrationsToSetup({ + integrations: options.integrations, + defaultIntegrations, + }); + + if (options.traceLifecycle !== 'static' && !integrations.some(integration => integration.name === 'SpanStreaming')) { + integrations.push(spanStreamingIntegration()); + } + const clientOptions: BrowserClientOptions = { ...options, enabled: shouldDisableBecauseIsBrowserExtenstion ? false : options.enabled, stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup({ - integrations: options.integrations, - defaultIntegrations, - }), + integrations, transport: options.transport || makeFetchTransport, }; diff --git a/packages/browser/test/sdk.test.ts b/packages/browser/test/sdk.test.ts index 3d84da69e565..744025bc1b1d 100644 --- a/packages/browser/test/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -61,6 +61,32 @@ describe('init', () => { expect(optionsPassed?.integrations.length).toBeGreaterThan(0); }); + it('installs spanStreamingIntegration by default', () => { + // @ts-expect-error this is fine for testing + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {}); + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: undefined }); + + init(options); + + const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; + expect(optionsPassed?.integrations.some(integration => integration.name === 'SpanStreaming')).toBe(true); + }); + + it('does not install spanStreamingIntegration when traceLifecycle is static', () => { + // @ts-expect-error this is fine for testing + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {}); + const options = getDefaultBrowserOptions({ + dsn: PUBLIC_DSN, + defaultIntegrations: undefined, + traceLifecycle: 'static', + }); + + init(options); + + const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; + expect(optionsPassed?.integrations.some(integration => integration.name === 'SpanStreaming')).toBe(false); + }); + test("doesn't install default integrations if told not to", () => { const DEFAULT_INTEGRATIONS: Integration[] = [ new MockIntegration('MockIntegration 0.3'), @@ -73,6 +99,17 @@ describe('init', () => { expect(DEFAULT_INTEGRATIONS[1]!.setupOnce as Mock).toHaveBeenCalledTimes(0); }); + it('installs spanStreamingIntegration with defaultIntegrations disabled', () => { + // @ts-expect-error this is fine for testing + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {}); + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: false }); + + init(options); + + const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; + expect(optionsPassed?.integrations.some(integration => integration.name === 'SpanStreaming')).toBe(true); + }); + it('installs merged default integrations, with overrides provided through options', () => { const DEFAULT_INTEGRATIONS = [ new MockIntegration('MockIntegration 1.1'), diff --git a/packages/cloudflare/test/sdk.test.ts b/packages/cloudflare/test/sdk.test.ts index 09efdd96f3d6..643c9f4462fb 100644 --- a/packages/cloudflare/test/sdk.test.ts +++ b/packages/cloudflare/test/sdk.test.ts @@ -22,10 +22,9 @@ describe('init', () => { expect(client).toBeInstanceOf(CloudflareClient); }); - test('installs SpanStreaming integration when traceLifecycle is "stream"', () => { + test('installs SpanStreaming integration by default', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - traceLifecycle: 'stream', }); const client = getClient(); @@ -36,8 +35,8 @@ describe('init', () => { ); }); - test("does not install SpanStreaming integration when traceLifecycle is not 'stream'", () => { - init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + test("does not install SpanStreaming integration when traceLifecycle is 'static'", () => { + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', traceLifecycle: 'static' }); const client = getClient(); expect(client?.getOptions()).toEqual( diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 8171a53c02c4..6a11959e9983 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -234,7 +234,7 @@ export abstract class Client { * @param options Options for the client. */ protected constructor(options: O) { - this._options = { attachStacktrace: true, ...options }; + this._options = { attachStacktrace: true, traceLifecycle: 'stream', ...options }; this._integrations = {}; this._numProcessing = 0; this._outcomes = {}; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 9b1c2374e50a..ec10c516ef9b 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -40,6 +40,8 @@ export class ServerRuntimeClient< public constructor(options: O) { addUserAgentToTransportHeaders(options); + options.traceLifecycle ??= 'stream'; + // When span streaming is enabled (`traceLifecycle: 'stream'`), the `spanStreamingIntegration` // is required to flush spans. We add it here so the individual server SDKs don't have to. // A user-provided `spanStreamingIntegration` always takes precedence over the one we add. diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index a161d8b36e10..9e4283d02133 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -554,7 +554,7 @@ export interface ClientOptions { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, test: true }); const client = new TestClient(options); - expect(client.getOptions()).toEqual({ attachStacktrace: true, ...options }); + expect(client.getOptions()).toEqual({ attachStacktrace: true, traceLifecycle: 'stream', ...options }); + }); + + test('defaults traceLifecycle to stream', () => { + const options = getDefaultTestClientOptions(); + delete options.traceLifecycle; + const client = new TestClient(options); + + expect(client.getOptions().traceLifecycle).toBe('stream'); + }); + + test('preserves an explicit static traceLifecycle', () => { + const client = new TestClient(getDefaultTestClientOptions({ traceLifecycle: 'static' })); + + expect(client.getOptions().traceLifecycle).toBe('static'); }); }); diff --git a/packages/deno/test/sdk.test.ts b/packages/deno/test/sdk.test.ts index eac8f4d7997e..5d0d8973a075 100644 --- a/packages/deno/test/sdk.test.ts +++ b/packages/deno/test/sdk.test.ts @@ -8,8 +8,8 @@ Deno.test('init() should return client', () => { assertNotEquals(init({}), undefined); }); -Deno.test('adds spanStreamingIntegration when traceLifecycle is "stream"', () => { - const client = init({ traceLifecycle: 'stream' }); +Deno.test('adds spanStreamingIntegration by default', () => { + const client = init({}); const integrations = client.getOptions().integrations; assertArrayIncludes( integrations.map(i => i.name), @@ -17,8 +17,8 @@ Deno.test('adds spanStreamingIntegration when traceLifecycle is "stream"', () => ); }); -Deno.test('doesn\'t add spanStreamingIntegration when traceLifecycle is not "stream"', () => { - const client = init({}); +Deno.test('doesn\'t add spanStreamingIntegration when traceLifecycle is "static"', () => { + const client = init({ traceLifecycle: 'static' }); const integrations = client.getOptions().integrations; assert(!integrations.some(i => i.name === 'SpanStreaming')); }); diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index cca2608a2639..2f1d85b03561 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -61,16 +61,12 @@ describe('init()', () => { }); describe('integrations', () => { - it("doesn't install default integrations if told not to", () => { + it('only installs the required spanStreaming integration if default integrations are disabled', () => { init({ dsn: PUBLIC_DSN, defaultIntegrations: false }); const client = getClient(); - expect(client?.getOptions()).toEqual( - expect.objectContaining({ - integrations: [], - }), - ); + expect(client?.getOptions().integrations.map(integration => integration.name)).toEqual(['SpanStreaming']); expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); }); @@ -171,8 +167,8 @@ describe('init()', () => { ); }); - it('installs spanStreaming integration when traceLifecycle is "stream"', () => { - init({ dsn: PUBLIC_DSN, traceLifecycle: 'stream' }); + it('installs spanStreaming integration by default', () => { + init({ dsn: PUBLIC_DSN }); const client = getClient(); expect(client?.getOptions()).toEqual( @@ -182,8 +178,8 @@ describe('init()', () => { ); }); - it("doesn't install spanStreaming integration when traceLifecycle is not 'stream'", () => { - init({ dsn: PUBLIC_DSN }); + it("doesn't install spanStreaming integration when traceLifecycle is 'static'", () => { + init({ dsn: PUBLIC_DSN, traceLifecycle: 'static' }); const client = getClient(); expect(client?.getOptions()).toEqual( @@ -194,7 +190,7 @@ describe('init()', () => { }); it('installs spanStreaming integration even with custom defaultIntegrations', () => { - init({ dsn: PUBLIC_DSN, traceLifecycle: 'stream', defaultIntegrations: [] }); + init({ dsn: PUBLIC_DSN, defaultIntegrations: [] }); const client = getClient(); expect(client?.getOptions()).toEqual( diff --git a/packages/vercel-edge/test/sdk.test.ts b/packages/vercel-edge/test/sdk.test.ts index d5a67edff718..0da12197a5f4 100644 --- a/packages/vercel-edge/test/sdk.test.ts +++ b/packages/vercel-edge/test/sdk.test.ts @@ -3,22 +3,22 @@ import { init, spanStreamingIntegration } from '../src'; import type { Integration } from '@sentry/core'; describe('init', () => { - it('adds spanStreamingIntegration when traceLifecycle is "stream"', () => { - const client = init({ traceLifecycle: 'stream' }); + it('adds spanStreamingIntegration by default', () => { + const client = init({}); const integrations = client?.getOptions().integrations; expect(integrations?.map(i => i.name)).toContain('SpanStreaming'); }); - it('doesn\'t add spanStreamingIntegration when traceLifecycle is not "stream"', () => { - const client = init({}); + it('doesn\'t add spanStreamingIntegration when traceLifecycle is "static"', () => { + const client = init({ traceLifecycle: 'static' }); const integrations = client?.getOptions().integrations; expect(integrations?.map(i => i.name)).not.toContain('SpanStreaming'); }); it('adds spanStreaming integration even with custom defaultIntegrations', () => { - const client = init({ traceLifecycle: 'stream', defaultIntegrations: [] }); + const client = init({ defaultIntegrations: [] }); const integrations = client?.getOptions().integrations; expect(integrations?.map(i => i.name)).toContain('SpanStreaming');