Skip to content
Merged
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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I decided to migrate this test to span streaming since it specifically tests the Sentry.init-less loader setup. In this case, the default will be span streaming. The only way to opt-out of it would be to call Sentry.init in onload. Doing this in this test would defeat the purpose of the noOnload test scenario 😅

Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipTracingTest,
waitForTransactionRequestOnUrl,
} from '../../../../utils/helpers';
import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';

sentryTest('should create a pageload transaction', async ({ getLocalTestUrl, page }) => {
sentryTest('should create a pageload span', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const spanPromise = waitForStreamedSpan(page, s => getSpanOp(s) === 'pageload');
const url = await getLocalTestUrl({ testDir: __dirname });
const req = await waitForTransactionRequestOnUrl(page, url);
await page.goto(url);
const pageloadSpan = await spanPromise;

const eventData = envelopeRequestParser(req);
const timeOrigin = await page.evaluate<number>('window._testBaseTimestamp');

const { start_timestamp: startTimestamp } = eventData;
expect(pageloadSpan.start_timestamp).toBeCloseTo(timeOrigin, 1);

expect(startTimestamp).toBeCloseTo(timeOrigin, 1);

expect(eventData.contexts?.trace?.op).toBe('pageload');
expect(eventData.spans?.length).toBeGreaterThan(0);
expect(eventData.transaction_info?.source).toEqual('url');
expect(pageloadSpan.attributes?.['sentry.segment.name.source'].value).toEqual('url');
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"SENTRY_ENVIRONMENT": "qa",
"SENTRY_TRACES_SAMPLE_RATE": "1.0",
"SENTRY_TUNNEL": "http://localhost:3031/",
"SENTRY_TRACE_LIFECYCLE": "static",

@Lms24 Lms24 Jul 27, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The SDK for Astro 3-5 on cloudflare can only be configured via env variables (known issue, see docs). This makes opt-out less intuitive (but still possible). Astro 6 again pulls in the correct sentry.server.config.js file which lets users opt out via Sentry.init as usual.

},
"assets": {
"binding": "ASSETS",
Expand Down

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

AWS Lambda layer is another case where the opt-out must happen via env variables, as long as users use the auto init. I think this is a fair tradeoff.

Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export class LocalLambdaStack extends Stack {
Variables: {
SENTRY_TRACES_SAMPLE_RATE: 1.0,
SENTRY_DEBUG: true,
SENTRY_TRACE_LIFECYCLE: 'static',
NODE_OPTIONS: `--import=@sentry/aws-serverless/awslambda-auto`,
// We only set SENTRY_DSN if not running TunnelNoDsn, because there
// we want to test that the extension tunnel forwards requests when SENTRY_DSN is missing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export class LocalLambdaStack extends Stack {
SENTRY_DSN: dsn,
SENTRY_TRACES_SAMPLE_RATE: 1.0,
SENTRY_DEBUG: true,
SENTRY_TRACE_LIFECYCLE: 'static',
NODE_OPTIONS: `--import=@sentry/aws-serverless/awslambda-auto`,
},
},
Expand Down
10 changes: 0 additions & 10 deletions packages/browser/src/integrations/spanstreaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@ export const spanStreamingIntegration = defineIntegration(() => {
return {
name: 'SpanStreaming' as const,

beforeSetup(client) {
Comment thread
cursor[bot] marked this conversation as resolved.
// If users only set spanStreamingIntegration, without traceLifecycle, we set it to "stream" for them.
// This avoids the classic double-opt-in problem we'd otherwise have in the browser SDK.
const clientOptions = client.getOptions();
if (!clientOptions.traceLifecycle) {
DEBUG_BUILD && debug.log('[SpanStreaming] setting `traceLifecycle` to "stream"');
clientOptions.traceLifecycle = 'stream';
}
},

setup(client) {
const initialMessage = 'SpanStreaming integration requires';
const fallbackMsg = 'Falling back to static trace lifecycle.';
Expand Down
17 changes: 13 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';
Comment thread
cursor[bot] marked this conversation as resolved.
import { defaultStackParser } from './stack-parsers';
import { makeFetchTransport } from './transports/fetch';
import { normalizeStringifyValue } from './normalizeStringifyValue';
Expand Down Expand Up @@ -110,14 +111,22 @@ export function init(options: BrowserOptions = {}): Client | undefined {
}
/*! rollup-include-development-only-end */

const integrations = getIntegrationsToSetup({
integrations: options.integrations,
defaultIntegrations,
});

options.traceLifecycle ??= 'stream';

if (options.traceLifecycle === 'stream' && !integrations.some(integration => integration.name === 'SpanStreaming')) {
integrations.push(spanStreamingIntegration());
}
Comment thread
cursor[bot] marked this conversation as resolved.

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
15 changes: 0 additions & 15 deletions packages/browser/test/integrations/spanstreaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,9 @@ describe('spanStreamingIntegration', () => {
const integration = spanStreamingIntegration();
expect(integration.name).toBe('SpanStreaming');
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(integration.beforeSetup).toBeDefined();
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(integration.setup).toBeDefined();
});

it('sets traceLifecycle to "stream" if not set', () => {
const client = new BrowserClient({
...getDefaultBrowserClientOptions(),
dsn: 'https://username@domain/123',
integrations: [spanStreamingIntegration()],
});

SentryCore.setCurrentClient(client);
client.init();

expect(client.getOptions().traceLifecycle).toBe('stream');
});

it.each(['static', 'somethingElse'])(
'logs a warning if traceLifecycle is not set to "stream" but to %s',
traceLifecycle => {
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
1 change: 1 addition & 0 deletions packages/bundler-plugins/src/core/sentry/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function createSentryInstance(
dsn: 'https://4c2bae7d9fbc413e8f7385f55c515d51@o1.ingest.sentry.io/6690737',

tracesSampleRate: 1,
traceLifecycle: 'static',

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

decided to keep bundler plugins' trace lifecycle static as well. We can opt into streaming in a separate PR. This is an interesting case because traces in the plugin have higher chance of actually getting streamed due to the longer execution time.

sampleRate: 1,

release: LIB_VERSION,
Expand Down
5 changes: 5 additions & 0 deletions packages/cloudflare/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknow
tracesSampleRate: isFinite(tracesSampleRate) ? tracesSampleRate : undefined,
debug: userOptions.debug ?? envToBool(getEnvVar(env, 'SENTRY_DEBUG')),
tunnel: userOptions.tunnel ?? getEnvVar(env, 'SENTRY_TUNNEL'),
traceLifecycle: userOptions.traceLifecycle ?? getTraceLifecycleFromEnv(getEnvVar(env, 'SENTRY_TRACE_LIFECYCLE')),
/*! rollup-include-development-only */
spotlight,
/*! rollup-include-development-only-end */
Expand Down Expand Up @@ -102,3 +103,7 @@ function getSpotlightFromEnv(
? (envUrl ?? true) // true: use env URL if present, otherwise true
: (envBool ?? envUrl); // undefined: use env var (bool or URL)
}

function getTraceLifecycleFromEnv(envVar: string | undefined): 'static' | 'stream' | undefined {
return envVar === 'stream' || envVar === 'static' ? envVar : undefined;
}
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 };
Comment thread
cursor[bot] marked this conversation as resolved.
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 @@ -543,7 +543,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
16 changes: 16 additions & 0 deletions packages/node/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ function getClientOptions(
const spotlight = getSpotlightConfig(options.spotlight);

const tracesSampleRate = getTracesSampleRate(options.tracesSampleRate);
const traceLifecycle = getTraceLifecycle(options.traceLifecycle);

const mergedOptions = {
...options,
Expand All @@ -311,6 +312,7 @@ function getClientOptions(
release,
tracesSampleRate,
spotlight,
traceLifecycle,
debug: envToBool(options.debug ?? process.env.SENTRY_DEBUG),
};

Expand Down Expand Up @@ -355,6 +357,20 @@ function getTracesSampleRate(tracesSampleRate: NodeOptions['tracesSampleRate']):
return isFinite(parsed) ? parsed : undefined;
}

function getTraceLifecycle(traceLifecycle: NodeOptions['traceLifecycle']): 'stream' | 'static' {
if (traceLifecycle !== undefined) {
return traceLifecycle;
}

const lifecycleFromEnv = process.env.SENTRY_TRACE_LIFECYCLE;

if (lifecycleFromEnv === 'stream' || lifecycleFromEnv === 'static') {
return lifecycleFromEnv;
}

return 'stream';
}

/**
* Update scope and propagation context based on environmental variables.
*
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 @@ -59,16 +59,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 @@ -169,8 +165,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 @@ -180,8 +176,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 @@ -192,7 +188,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
Loading
Loading