Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions packages/browser/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Browser bundle size increase

Medium Severity

This violates the review rule on large browser bundle size increases. Importing spanStreamingIntegration into sdk.ts pulls SpanBuffer and related streaming code into every bundle that ships init, including non-tracing CDN entry points that previously only exposed a shim.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 2eeaff8. Configure here.

import { defaultStackParser } from './stack-parsers';
import { makeFetchTransport } from './transports/fetch';
import { normalizeStringifyValue } from './normalizeStringifyValue';
Expand Down Expand Up @@ -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());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing integration or E2E tests

Medium Severity

This violates the Testing Conventions rule for feat PRs. The change only adds unit coverage that the integration is present in options; there is no integration or E2E test that asserts streamed spans are actually emitted under the new default.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 2eeaff8. Configure here.


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,
};

Expand Down
37 changes: 37 additions & 0 deletions packages/browser/test/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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'),
Expand Down
7 changes: 3 additions & 4 deletions packages/cloudflare/test/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
* @param options Options for the client.
*/
protected constructor(options: O) {
this._options = { attachStacktrace: true, ...options };
this._options = { attachStacktrace: true, traceLifecycle: 'stream', ...options };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stream default drops browser spans

High Severity

Defaulting traceLifecycle to stream on base Client skips the static transaction path, but BrowserClient never auto-installs spanStreamingIntegration. Only init() adds it, unlike ServerRuntimeClient. Constructing BrowserClient directly therefore enables streaming with no flusher, so finished spans are never sent.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2eeaff8. Configure here.

this._integrations = {};
this._numProcessing = 0;
this._outcomes = {};
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/server-runtime-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
* The trace lifecycle, determining whether spans are sent statically when the entire local span tree is complete,
* or streamed in batches, following interval- and action-based triggers.
*
* @default 'static'
* @default 'stream'
*/
traceLifecycle?: 'static' | 'stream';

Expand Down
16 changes: 15 additions & 1 deletion packages/core/test/lib/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,21 @@ describe('Client', () => {
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');
});
});

Expand Down
8 changes: 4 additions & 4 deletions packages/deno/test/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ 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),
['SpanStreaming'],
);
});

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'));
});
Expand Down
18 changes: 7 additions & 11 deletions packages/node/test/sdk/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down
10 changes: 5 additions & 5 deletions packages/vercel-edge/test/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading