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');