From 2877a775e076952bab073f46af0338c854519e86 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 2 Feb 2026 14:43:09 +0100 Subject: [PATCH 1/3] feat(core): Add traceLifecycle option and beforeSendSpan utilities Adds support for span streaming configuration: - Add `traceLifecycle` option to ClientOptions ('static' | 'stream') - Add `BeforeSendSpanCallback` type with optional `_streamed` marker - Add `withStreamedSpan()` utility to wrap callbacks for streamed spans - Add `isStreamedBeforeSendSpanCallback()` type guard utility This is part of the span streaming feature that allows spans to be sent incrementally rather than waiting for the full trace to complete. Co-authored-by: Cursor --- packages/core/src/index.ts | 8 ++++- packages/core/src/types-hoist/options.ts | 32 ++++++++++++++++- packages/core/src/utils/beforeSendSpan.ts | 42 +++++++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/utils/beforeSendSpan.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b05934465dfb..6370ae61279b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -67,6 +67,7 @@ export { prepareEvent } from './utils/prepareEvent'; export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; export { hasSpansEnabled } from './utils/hasSpansEnabled'; +export { withStreamedSpan, isStreamedBeforeSendSpanCallback } from './utils/beforeSendSpan'; export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; export { handleCallbackErrors } from './utils/handleCallbackErrors'; export { parameterize, fmt } from './utils/parameterize'; @@ -397,7 +398,12 @@ export type { Extra, Extras } from './types-hoist/extra'; export type { Integration, IntegrationFn } from './types-hoist/integration'; export type { Mechanism } from './types-hoist/mechanism'; export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocation } from './types-hoist/misc'; -export type { ClientOptions, CoreOptions as Options, ServerRuntimeOptions } from './types-hoist/options'; +export type { + BeforeSendSpanCallback, + ClientOptions, + CoreOptions as Options, + ServerRuntimeOptions, +} from './types-hoist/options'; export type { Package } from './types-hoist/package'; export type { PolymorphicEvent, PolymorphicRequest } from './types-hoist/polymorphics'; export type { diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 9f8baca5b428..527173c86147 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -500,6 +500,16 @@ export interface ClientOptions SpanJSON; + beforeSendSpan?: BeforeSendSpanCallback; /** * An event-processing callback for transaction events, guaranteed to be invoked after all other event @@ -615,6 +628,23 @@ export interface ClientOptions Breadcrumb | null; } +/** + * A callback for processing spans before they are sent. + * + * By default, this callback receives and returns {@link SpanJSON} objects. + * When using `traceLifecycle: 'stream'`, wrap your callback with `withStreamedSpan` + * to receive and return {@link StreamedSpanJSON} objects instead. + * + * @see StreamedSpanJSON for the streamed span format used with `traceLifecycle: 'stream'` + */ +export type BeforeSendSpanCallback = ((span: SpanJSON) => SpanJSON) & { + /** + * When true, indicates this callback is designed to handle the {@link StreamedSpanJSON} format + * used with `traceLifecycle: 'stream'`. Set this by wrapping your callback with `withStreamedSpan`. + */ + _streamed?: true; +}; + /** Base configuration options for every SDK. */ export interface CoreOptions extends Omit>, 'integrations' | 'transport' | 'stackParser'> { diff --git a/packages/core/src/utils/beforeSendSpan.ts b/packages/core/src/utils/beforeSendSpan.ts new file mode 100644 index 000000000000..cf7118971231 --- /dev/null +++ b/packages/core/src/utils/beforeSendSpan.ts @@ -0,0 +1,42 @@ +import type { BeforeSendSpanCallback, ClientOptions } from '../types-hoist/options'; +import type { StreamedSpanJSON } from '../types-hoist/span'; +import { addNonEnumerableProperty } from './object'; + +/** + * A wrapper to use the new span format in your `beforeSendSpan` callback. + * + * When using `traceLifecycle: 'stream'`, wrap your callback with this function + * to receive and return {@link StreamedSpanJSON} instead of the standard {@link SpanJSON}. + * + * @example + * + * Sentry.init({ + * traceLifecycle: 'stream', + * beforeSendSpan: withStreamedSpan((span) => { + * // span is of type StreamedSpanJSON + * return span; + * }), + * }); + * + * @param callback - The callback function that receives and returns a {@link StreamedSpanJSON}. + * @returns A callback that is compatible with the `beforeSendSpan` option when using `traceLifecycle: 'stream'`. + */ +export function withStreamedSpan( + callback: (span: StreamedSpanJSON) => StreamedSpanJSON, +): BeforeSendSpanCallback { + addNonEnumerableProperty(callback, '_streamed', true); + // type-casting here because TS can't infer the type correctly + return callback as unknown as BeforeSendSpanCallback; +} + +/** + * Typesafe check to identify if a `beforeSendSpan` callback expects the streamed span JSON format. + * + * @param callback - The `beforeSendSpan` callback to check. + * @returns `true` if the callback was wrapped with {@link withStreamedSpan}. + */ +export function isStreamedBeforeSendSpanCallback( + callback: ClientOptions['beforeSendSpan'], +): callback is BeforeSendSpanCallback & { _streamed: true } { + return !!callback && '_streamed' in callback && !!callback._streamed; +} From fc38e1fc08c10dfec4d5659d0b3a2abe25f1fffb Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 2 Feb 2026 15:13:14 +0100 Subject: [PATCH 2/3] refinements, add tests --- packages/core/src/client.ts | 5 +++- packages/core/src/envelope.ts | 3 ++- packages/core/src/index.ts | 9 ++----- packages/core/src/types-hoist/options.ts | 20 +++++--------- packages/core/src/utils/beforeSendSpan.ts | 9 +++---- .../test/lib/utils/beforeSendSpan.test.ts | 26 +++++++++++++++++++ 6 files changed, 45 insertions(+), 27 deletions(-) create mode 100644 packages/core/test/lib/utils/beforeSendSpan.test.ts diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 1ed447ab802d..e9e3c03538f7 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -34,6 +34,7 @@ import type { SeverityLevel } from './types-hoist/severity'; import type { Span, SpanAttributes, SpanContextData, SpanJSON } from './types-hoist/span'; import type { StartSpanOptions } from './types-hoist/startSpanOptions'; import type { Transport, TransportMakeRequestResponse } from './types-hoist/transport'; +import { isStreamedBeforeSendSpanCallback } from './utils/beforeSendSpan'; import { createClientReportEnvelope } from './utils/clientreport'; import { debug } from './utils/debug-logger'; import { dsnToString, makeDsn } from './utils/dsn'; @@ -1498,7 +1499,9 @@ function processBeforeSend( event: Event, hint: EventHint, ): PromiseLike | Event | null { - const { beforeSend, beforeSendTransaction, beforeSendSpan, ignoreSpans } = options; + const { beforeSend, beforeSendTransaction, ignoreSpans } = options; + const beforeSendSpan = !isStreamedBeforeSendSpanCallback(options.beforeSendSpan) && options.beforeSendSpan; + let processedEvent = event; if (isErrorEvent(processedEvent) && beforeSend) { diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 875056890e0e..c7a46359260f 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -18,6 +18,7 @@ import type { Event } from './types-hoist/event'; import type { SdkInfo } from './types-hoist/sdkinfo'; import type { SdkMetadata } from './types-hoist/sdkmetadata'; import type { Session, SessionAggregates } from './types-hoist/session'; +import { isStreamedBeforeSendSpanCallback } from './utils/beforeSendSpan'; import { dsnToString } from './utils/dsn'; import { createEnvelope, @@ -152,7 +153,7 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? const convertToSpanJSON = beforeSendSpan ? (span: SentrySpan) => { const spanJson = spanToJSON(span); - const processedSpan = beforeSendSpan(spanJson); + const processedSpan = !isStreamedBeforeSendSpanCallback(beforeSendSpan) ? beforeSendSpan(spanJson) : spanJson; if (!processedSpan) { showSpanDropWarning(); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6370ae61279b..9968a4489c08 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -67,7 +67,7 @@ export { prepareEvent } from './utils/prepareEvent'; export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; export { hasSpansEnabled } from './utils/hasSpansEnabled'; -export { withStreamedSpan, isStreamedBeforeSendSpanCallback } from './utils/beforeSendSpan'; +export { withStreamedSpan } from './utils/beforeSendSpan'; export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; export { handleCallbackErrors } from './utils/handleCallbackErrors'; export { parameterize, fmt } from './utils/parameterize'; @@ -398,12 +398,7 @@ export type { Extra, Extras } from './types-hoist/extra'; export type { Integration, IntegrationFn } from './types-hoist/integration'; export type { Mechanism } from './types-hoist/mechanism'; export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocation } from './types-hoist/misc'; -export type { - BeforeSendSpanCallback, - ClientOptions, - CoreOptions as Options, - ServerRuntimeOptions, -} from './types-hoist/options'; +export type { ClientOptions, CoreOptions as Options, ServerRuntimeOptions } from './types-hoist/options'; export type { Package } from './types-hoist/package'; export type { PolymorphicEvent, PolymorphicRequest } from './types-hoist/polymorphics'; export type { diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 527173c86147..02d32bfb9fe0 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -6,7 +6,7 @@ import type { Log } from './log'; import type { Metric } from './metric'; import type { TracesSamplerSamplingContext } from './samplingcontext'; import type { SdkMetadata } from './sdkmetadata'; -import type { SpanJSON } from './span'; +import type { SpanJSON, StreamedSpanJSON } from './span'; import type { StackLineParser, StackParser } from './stacktrace'; import type { TracePropagationTargets } from './tracing'; import type { BaseTransportOptions, Transport } from './transport'; @@ -501,10 +501,8 @@ export interface ClientOptions SpanJSON) | BeforeSendStramedSpanCallback; /** * An event-processing callback for transaction events, guaranteed to be invoked after all other event @@ -629,15 +627,11 @@ export interface ClientOptions SpanJSON) & { +export type BeforeSendStramedSpanCallback = ((span: StreamedSpanJSON) => StreamedSpanJSON) & { /** * When true, indicates this callback is designed to handle the {@link StreamedSpanJSON} format * used with `traceLifecycle: 'stream'`. Set this by wrapping your callback with `withStreamedSpan`. diff --git a/packages/core/src/utils/beforeSendSpan.ts b/packages/core/src/utils/beforeSendSpan.ts index cf7118971231..68c4576d179d 100644 --- a/packages/core/src/utils/beforeSendSpan.ts +++ b/packages/core/src/utils/beforeSendSpan.ts @@ -1,4 +1,4 @@ -import type { BeforeSendSpanCallback, ClientOptions } from '../types-hoist/options'; +import type { BeforeSendStramedSpanCallback, ClientOptions } from '../types-hoist/options'; import type { StreamedSpanJSON } from '../types-hoist/span'; import { addNonEnumerableProperty } from './object'; @@ -23,10 +23,9 @@ import { addNonEnumerableProperty } from './object'; */ export function withStreamedSpan( callback: (span: StreamedSpanJSON) => StreamedSpanJSON, -): BeforeSendSpanCallback { +): BeforeSendStramedSpanCallback { addNonEnumerableProperty(callback, '_streamed', true); - // type-casting here because TS can't infer the type correctly - return callback as unknown as BeforeSendSpanCallback; + return callback; } /** @@ -37,6 +36,6 @@ export function withStreamedSpan( */ export function isStreamedBeforeSendSpanCallback( callback: ClientOptions['beforeSendSpan'], -): callback is BeforeSendSpanCallback & { _streamed: true } { +): callback is BeforeSendStramedSpanCallback { return !!callback && '_streamed' in callback && !!callback._streamed; } diff --git a/packages/core/test/lib/utils/beforeSendSpan.test.ts b/packages/core/test/lib/utils/beforeSendSpan.test.ts new file mode 100644 index 000000000000..5e5bdc566889 --- /dev/null +++ b/packages/core/test/lib/utils/beforeSendSpan.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it, vi } from 'vitest'; +import { withStreamedSpan } from '../../../src'; +import { isStreamedBeforeSendSpanCallback } from '../../../src/utils/beforeSendSpan'; + +describe('beforeSendSpan for span streaming', () => { + describe('withStreamedSpan', () => { + it('should be able to modify the span', () => { + const beforeSendSpan = vi.fn(); + const wrapped = withStreamedSpan(beforeSendSpan); + expect(wrapped._streamed).toBe(true); + }); + }); + + describe('isStreamedBeforeSendSpanCallback', () => { + it('returns true if the callback is wrapped with withStreamedSpan', () => { + const beforeSendSpan = vi.fn(); + const wrapped = withStreamedSpan(beforeSendSpan); + expect(isStreamedBeforeSendSpanCallback(wrapped)).toBe(true); + }); + + it('returns false if the callback is not wrapped with withStreamedSpan', () => { + const beforeSendSpan = vi.fn(); + expect(isStreamedBeforeSendSpanCallback(beforeSendSpan)).toBe(false); + }); + }); +}); From 284f13df37cbc1a6cecd51ebd63f080d7d8e6520 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 2 Feb 2026 15:28:02 +0100 Subject: [PATCH 3/3] bump some size limits --- .size-limit.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 761905a49ef3..eedacb4819bd 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -82,7 +82,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'), gzip: true, - limit: '85.55 KB', + limit: '86 KB', }, { name: '@sentry/browser (incl. Tracing, Replay, Feedback)', @@ -103,7 +103,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'sendFeedback'), gzip: true, - limit: '31 KB', + limit: '32 KB', }, { name: '@sentry/browser (incl. FeedbackAsync)', @@ -148,7 +148,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '44.5 KB', + limit: '45 KB', }, // Vue SDK (ESM) { @@ -163,7 +163,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '44.1 KB', + limit: '45 KB', }, // Svelte SDK (ESM) { @@ -171,20 +171,20 @@ module.exports = [ path: 'packages/svelte/build/esm/index.js', import: createImport('init'), gzip: true, - limit: '25.5 KB', + limit: '26 KB', }, // Browser CDN bundles { name: 'CDN Bundle', path: createCDNPath('bundle.min.js'), gzip: true, - limit: '28 KB', + limit: '28.5 KB', }, { name: 'CDN Bundle (incl. Tracing)', path: createCDNPath('bundle.tracing.min.js'), gzip: true, - limit: '43 KB', + limit: '43.5 KB', }, { name: 'CDN Bundle (incl. Logs, Metrics)',