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
8 changes: 6 additions & 2 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
"categories": {},
"rules": {
"no-unused-vars": [
"warn",
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],

// === Base rules from eslint-config-sdk/base.js ===
Expand Down
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
4 changes: 2 additions & 2 deletions packages/aws-serverless/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ function shouldDisableLayerExtensionForProxy(): boolean {
*/
// NOTE: in awslambda-auto.ts, we also call the original `getDefaultIntegrations` from `@sentry/node` to load performance integrations.
// If at some point we need to filter a node integration out for good, we need to make sure to also filter it out there.
export function getDefaultIntegrations(_options: Options): Integration[] {
return [...getDefaultIntegrationsWithoutPerformance(), awsIntegration(), awsLambdaIntegration()];
export function getDefaultIntegrations(options: Options): Integration[] {
return [...getDefaultIntegrationsWithoutPerformance(options), awsIntegration(), awsLambdaIntegration()];
}

export interface AwsServerlessOptions extends NodeOptions {
Expand Down
38 changes: 3 additions & 35 deletions packages/browser/src/integrations/spanstreaming.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import type { IntegrationFn } from '@sentry/core';
import {
captureSpan,
debug,
defineIntegration,
hasSpanStreamingEnabled,
isStreamedBeforeSendSpanCallback,
SpanBuffer,
spanIsSampled,
} from '@sentry/core';
import { debug, defineIntegration, setupSpanStreaming } from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';

export const spanStreamingIntegration = defineIntegration(() => {
Expand All @@ -25,36 +17,12 @@ export const spanStreamingIntegration = defineIntegration(() => {
},

setup(client) {
const initialMessage = 'SpanStreaming integration requires';
const fallbackMsg = 'Falling back to static trace lifecycle.';
const buffer = setupSpanStreaming(client);

if (!hasSpanStreamingEnabled(client)) {
DEBUG_BUILD && debug.warn(`${initialMessage} \`traceLifecycle\` to be set to "stream"! ${fallbackMsg}`);
if (!buffer) {
return;
}

const beforeSendSpan = client.getOptions().beforeSendSpan;
// If users misconfigure their SDK by opting into span streaming but
// using an incompatible beforeSendSpan callback, we fall back to the static trace lifecycle.
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 => {
// Negatively sampled spans must not be captured.
// This happens because OTel and we create non-recording spans for negatively sampled spans
// that go through the same life cycle as recording spans.
if (!spanIsSampled(span)) {
return;
}
buffer.add(captureSpan(span, client));
});

// In addition to capturing the span, we also flush the trace when the segment
// span ends to ensure things are sent timely. We never know when the browser
// is closed, users navigate away, etc.
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/test/integrations/spanstreaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('spanStreamingIntegration', () => {
dsn: 'https://username@domain/123',
integrations: [spanStreamingIntegration()],
traceLifecycle: 'stream',
beforeSendSpan: (span: Span) => span,
beforeSendSpan: (span: SentryCore.SpanJSON) => span,
});

SentryCore.setCurrentClient(client);
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 { setupSpanStreaming, spanStreamingIntegration } from './integrations/spanStreaming';

export type { FeatureFlag } from './utils/featureFlags';

Expand Down
53 changes: 53 additions & 0 deletions packages/core/src/integrations/spanStreaming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Client } from '../client';
import type { IntegrationFn } from '../types-hoist/integration';
import type { Span } from '../types-hoist/span';
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';

/**
* Shared setup logic for span streaming integrations.
* Returns a SpanBuffer if setup succeeds, or undefined if span streaming cannot be enabled.
*/
export function setupSpanStreaming(client: Client): SpanBuffer | undefined {
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 undefined;
}

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

const buffer = new SpanBuffer(client);

client.on('afterSpanEnd', (span: Readonly<Span>) => {
if (!spanIsSampled(span)) {
return;
}
buffer.add(captureSpan(span, client));
});

return buffer;
}

export const spanStreamingIntegration = defineIntegration(() => {
return {
name: 'SpanStreaming',

setup(client) {
setupSpanStreaming(client);
},
};
}) satisfies IntegrationFn;
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
4 changes: 2 additions & 2 deletions packages/google-cloud-serverless/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ function getCjsOnlyIntegrations(): Integration[] {
}

/** Get the default integrations for the GCP SDK. */
export function getDefaultIntegrations(_options: Options): Integration[] {
return [...getDefaultIntegrationsWithoutPerformance(), ...getCjsOnlyIntegrations()];
export function getDefaultIntegrations(options?: Options): Integration[] {
return [...getDefaultIntegrationsWithoutPerformance(options), ...getCjsOnlyIntegrations()];
}

/**
Expand Down
Loading
Loading