Skip to content
Open
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
14 changes: 7 additions & 7 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -317,7 +317,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '53 KB',
limit: '55 KB',
},
// Node SDK (ESM)
{
Expand All @@ -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');
Expand All @@ -356,7 +356,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '114 KB',
limit: '116 KB',
},
];

Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export {
statsigIntegration,
unleashIntegration,
growthbookIntegration,
spanStreamingIntegration,
metrics,
} from '@sentry/node';

Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/aws-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export {
unleashIntegration,
growthbookIntegration,
metrics,
spanStreamingIntegration,
} from '@sentry/node';

export {
Expand Down
1 change: 1 addition & 0 deletions packages/bun/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export {
statsigIntegration,
unleashIntegration,
metrics,
spanStreamingIntegration,
} from '@sentry/node';

export {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
42 changes: 42 additions & 0 deletions packages/core/src/integrations/spanStreaming.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link

Choose a reason for hiding this comment

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

Duplicate spanStreamingIntegration logic between core and browser

Low Severity

The new spanStreamingIntegration in @sentry/core substantially duplicates the existing spanStreamingIntegration in packages/browser/src/integrations/spanstreaming.ts. The setup logic — checking hasSpanStreamingEnabled, validating beforeSendSpan compatibility, creating a SpanBuffer, and subscribing to afterSpanEnd — is nearly identical. The browser version adds a beforeSetup hook and an afterSegmentSpanEnd listener, but the shared core could be extracted into a common helper to reduce maintenance burden and the risk of divergent bug fixes.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

135 changes: 135 additions & 0 deletions packages/core/test/integrations/spanStreaming.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
1 change: 1 addition & 0 deletions packages/google-cloud-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export {
statsigIntegration,
unleashIntegration,
metrics,
spanStreamingIntegration,
} from '@sentry/node';

export {
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/node-core/src/common-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export {
consoleIntegration,
wrapMcpServerWithSentry,
featureFlagsIntegration,
spanStreamingIntegration,
metrics,
envToBool,
} from '@sentry/core';
Expand Down
4 changes: 3 additions & 1 deletion packages/node-core/src/light/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
linkedErrorsIntegration,
propagationContextFromHeaders,
requestDataIntegration,
spanStreamingIntegration,
stackParserFromStackParserOptions,
} from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';
Expand All @@ -38,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(),
Expand All @@ -60,6 +61,7 @@ export function getDefaultIntegrations(): Integration[] {
childProcessIntegration(),
processSessionIntegration(),
modulesIntegration(),
...(options?.traceLifecycle === 'stream' ? [spanStreamingIntegration()] : []),
];
}

Expand Down
4 changes: 3 additions & 1 deletion packages/node-core/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
linkedErrorsIntegration,
propagationContextFromHeaders,
requestDataIntegration,
spanStreamingIntegration,
stackParserFromStackParserOptions,
} from '@sentry/core';
import {
Expand Down Expand Up @@ -46,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`
Expand All @@ -71,6 +72,7 @@ export function getDefaultIntegrations(): Integration[] {
childProcessIntegration(),
processSessionIntegration(),
modulesIntegration(),
...(options?.traceLifecycle === 'stream' ? [spanStreamingIntegration()] : []),
];
}

Expand Down
14 changes: 14 additions & 0 deletions packages/node-core/test/light/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export {
consoleIntegration,
wrapMcpServerWithSentry,
featureFlagsIntegration,
spanStreamingIntegration,
createLangChainCallbackHandler,
instrumentLangGraph,
instrumentStateGraphCompile,
Expand Down
3 changes: 2 additions & 1 deletion packages/node/src/sdk/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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()] : []),
];
}

Comment on lines 33 to 39
Copy link

Choose a reason for hiding this comment

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

Bug: The getDefaultIntegrationsWithoutPerformance function doesn't accept or forward options, preventing serverless SDKs from enabling span streaming via traceLifecycle: 'stream'.
Severity: MEDIUM

Suggested Fix

Modify getDefaultIntegrationsWithoutPerformance to accept an options parameter and pass it to getNodeCoreDefaultIntegrations(). Update callers of getDefaultIntegrationsWithoutPerformance, such as in packages/aws-serverless/src/init.ts, to pass their received _options parameter.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/node/src/sdk/index.ts#L33-L39

Potential issue: The `getDefaultIntegrationsWithoutPerformance` function in
`@sentry/node` is called without any arguments. This function, in turn, calls
`getNodeCoreDefaultIntegrations()` without forwarding any options. As a result, when
serverless SDKs like `@sentry/aws-serverless` and `@sentry/google-cloud-serverless` use
this function, the `traceLifecycle: 'stream'` option set during `Sentry.init()` is
ignored. This prevents the `spanStreamingIntegration` from being added, effectively
disabling the span streaming feature for these environments despite it being a
documented public option.

Did we get this right? 👍 / 👎 to inform future reviews.

Expand Down
Loading
Loading