From a32a10f5fe6b3dbd463c255596891bd445a1d3ef Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 24 Jul 2026 18:32:38 +0200 Subject: [PATCH] feat(core)!: Stream beforeSendSpan payloads by default Make streamed span JSON the default beforeSendSpan contract and provide withStaticSpan for callbacks that still process transaction span JSON. Deprecate withStreamedSpan for removal in version 12. BREAKING CHANGE: beforeSendSpan receives StreamedSpanJSON by default. Fixes #22349 Co-Authored-By: Cursor Co-authored-by: Cursor --- .../beforeSendSpan-streamed/init.js | 6 +-- .../beforeSendSpan-streamed/scenario.ts | 5 +- .../rollup-utils/plugins/bundlePlugins.mjs | 3 +- packages/astro/src/index.server.ts | 2 + packages/astro/src/index.types.ts | 2 + packages/aws-serverless/src/index.ts | 2 + packages/browser/src/exports.ts | 2 + .../browser/src/integrations/spanstreaming.ts | 10 ++-- .../test/integrations/spanstreaming.test.ts | 6 +-- packages/bun/src/index.ts | 2 + packages/cloudflare/src/index.ts | 2 + packages/core/src/client.ts | 4 +- packages/core/src/envelope.ts | 4 +- .../core/src/integrations/spanStreaming.ts | 8 +-- packages/core/src/shared-exports.ts | 8 ++- .../core/src/tracing/spans/beforeSendSpan.ts | 53 +++++++++++++------ packages/core/src/types/options.ts | 21 ++++++-- .../test/integrations/spanStreaming.test.ts | 6 +-- packages/core/test/lib/client.test.ts | 33 +++++++----- packages/core/test/lib/envelope.test.ts | 13 +++-- .../core/test/lib/tracing/sentrySpan.test.ts | 5 +- .../lib/tracing/spans/beforeSendSpan.test.ts | 46 ++++++++++++---- .../lib/tracing/spans/captureSpan.test.ts | 13 +++-- packages/deno/src/index.ts | 2 + packages/effect/src/index.types.ts | 2 + packages/elysia/src/index.ts | 2 + packages/google-cloud-serverless/src/index.ts | 2 + packages/nextjs/src/index.types.ts | 2 + packages/node/src/index.ts | 3 +- packages/nuxt/src/index.types.ts | 2 + packages/react-router/src/index.types.ts | 2 + packages/remix/src/cloudflare/index.ts | 2 + packages/remix/src/index.types.ts | 2 + packages/remix/src/server/index.ts | 2 + packages/solidstart/src/index.types.ts | 2 + packages/solidstart/src/server/index.ts | 2 + packages/sveltekit/src/index.types.ts | 2 + packages/sveltekit/src/server/index.ts | 2 + packages/sveltekit/src/worker/index.ts | 2 + .../tanstackstart-react/src/index.types.ts | 2 + packages/vercel-edge/src/index.ts | 2 + 41 files changed, 206 insertions(+), 87 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/init.js b/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/init.js index f099c1f61c3e..a3fae7e92b48 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/init.js @@ -4,9 +4,9 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, - beforeSendSpan: Sentry.withStreamedSpan(span => { + beforeSendSpan: span => { if (span.attributes['sentry.op'] === 'pageload') { span.name = 'customPageloadSpanName'; span.links = [ @@ -24,5 +24,5 @@ Sentry.init({ span.status = 'something'; } return span; - }), + }, }); diff --git a/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-streamed/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-streamed/scenario.ts index 3c7c0ed81edf..3e885e4170f9 100644 --- a/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-streamed/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-streamed/scenario.ts @@ -4,10 +4,9 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, - traceLifecycle: 'stream', transport: loggingTransport, release: '1.0.0', - beforeSendSpan: Sentry.withStreamedSpan(span => { + beforeSendSpan: span => { if (span.name === 'test-child-span') { span.name = 'customChildSpanName'; if (!span.attributes) { @@ -27,7 +26,7 @@ Sentry.init({ ]; } return span; - }), + }, }); Sentry.startSpan({ name: 'test-span', op: 'test' }, () => { diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index 56712e33a458..b1836e17127f 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -150,7 +150,8 @@ export function makeTerserPlugin() { '_resolveFilename', // Set on e.g. the shim feedbackIntegration to be able to detect it '_isShim', - // Marker set by `withStreamedSpan()` to tag streamed `beforeSendSpan` callbacks + // Markers used to distinguish static and explicitly streamed `beforeSendSpan` callbacks + '_static', '_streamed', // This is used in metadata integration '_sentryModuleMetadata', diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index cb157ec9007e..b12070bdf9ec 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -173,6 +173,8 @@ export { unleashIntegration, growthbookIntegration, spanStreamingIntegration, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, metrics, } from '@sentry/node'; diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index 0c8a5cca7bb6..b3f5037f8751 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -21,6 +21,8 @@ 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 withStaticSpan: typeof clientSdk.withStaticSpan; +// eslint-disable-next-line typescript/no-deprecated export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan; export declare const getDefaultIntegrations: (options: Options) => Integration[]; diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 33de0e664080..62531e5abe1e 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -160,6 +160,8 @@ export { growthbookIntegration, metrics, spanStreamingIntegration, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, experimentalUseDiagnosticsChannelInjection, diagnosticsChannelInjectionIntegrations, diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index d1e9d7662db3..e3a7e5da2850 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -72,6 +72,8 @@ export { spanToTraceHeader, spanToBaggageHeader, updateSpanName, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, metrics, } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations/spanstreaming.ts b/packages/browser/src/integrations/spanstreaming.ts index 6c1a647cb1fb..c27c110ac2f1 100644 --- a/packages/browser/src/integrations/spanstreaming.ts +++ b/packages/browser/src/integrations/spanstreaming.ts @@ -4,7 +4,7 @@ import { debug, defineIntegration, hasSpanStreamingEnabled, - isStreamedBeforeSendSpanCallback, + isStaticBeforeSendSpanCallback, SpanBuffer, spanIsSampled, } from '@sentry/core/browser'; @@ -36,12 +36,12 @@ export const spanStreamingIntegration = defineIntegration(() => { } const beforeSendSpan = clientOptions.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)) { + if (isStaticBeforeSendSpanCallback(beforeSendSpan)) { clientOptions.traceLifecycle = 'static'; DEBUG_BUILD && - debug.warn(`${initialMessage} a beforeSendSpan callback using \`withStreamedSpan\`! ${fallbackMsg}`); + debug.warn( + `SpanStreaming integration is incompatible with a beforeSendSpan callback using \`withStaticSpan\`! ${fallbackMsg}`, + ); return; } diff --git a/packages/browser/test/integrations/spanstreaming.test.ts b/packages/browser/test/integrations/spanstreaming.test.ts index dd5268dab4ec..c4c008c7ddb5 100644 --- a/packages/browser/test/integrations/spanstreaming.test.ts +++ b/packages/browser/test/integrations/spanstreaming.test.ts @@ -79,21 +79,21 @@ describe('spanStreamingIntegration', () => { }, ); - it('falls back to static trace lifecycle if beforeSendSpan is not compatible with span streaming', () => { + it('falls back to static trace lifecycle if beforeSendSpan is marked as static', () => { const debugSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); const client = new BrowserClient({ ...getDefaultBrowserClientOptions(), dsn: 'https://username@domain/123', integrations: [spanStreamingIntegration()], traceLifecycle: 'stream', - beforeSendSpan: (span: Span) => span, + beforeSendSpan: SentryCore.withStaticSpan(span => span), }); SentryCore.setCurrentClient(client); client.init(); expect(debugSpy).toHaveBeenCalledWith( - 'SpanStreaming integration requires a beforeSendSpan callback using `withStreamedSpan`! Falling back to static trace lifecycle.', + 'SpanStreaming integration is incompatible with a beforeSendSpan callback using `withStaticSpan`! Falling back to static trace lifecycle.', ); debugSpy.mockRestore(); diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 4a6c25eed27d..beb8a365839c 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -178,6 +178,8 @@ export { unleashIntegration, metrics, spanStreamingIntegration, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, } from '@sentry/node'; diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index d1f787345503..a07fdb43322b 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -110,6 +110,8 @@ export { growthbookIntegration, logger, metrics, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, spanStreamingIntegration, instrumentStateGraph, diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 6a11959e9983..4d8909032acf 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -11,7 +11,7 @@ import { _INTERNAL_flushMetricsBuffer } from './metrics/internal'; import type { Scope } from './scope'; import { updateSession } from './session'; import { getDynamicSamplingContextFromScope } from './tracing/dynamicSamplingContext'; -import { isStreamedBeforeSendSpanCallback } from './tracing/spans/beforeSendSpan'; +import { isStaticBeforeSendSpanCallback } from './tracing/spans/beforeSendSpan'; import { extractGenAiSpansFromEvent } from './tracing/spans/extractGenAiSpans'; import { DEFAULT_TRANSPORT_BUFFER_SIZE } from './transports/base'; import type { Breadcrumb, BreadcrumbHint, FetchBreadcrumbHint, XhrBreadcrumbHint } from './types/breadcrumb'; @@ -1654,7 +1654,7 @@ function processBeforeSend( hint: EventHint, ): PromiseLike | Event | null { const { beforeSend, beforeSendTransaction, ignoreSpans } = options; - const beforeSendSpan = !isStreamedBeforeSendSpanCallback(options.beforeSendSpan) && options.beforeSendSpan; + const beforeSendSpan = isStaticBeforeSendSpanCallback(options.beforeSendSpan) && options.beforeSendSpan; let processedEvent = event; diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 8a2faa114951..f162835beed4 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -1,7 +1,7 @@ import type { Client } from './client'; import { getDynamicSamplingContextFromSpan } from './tracing/dynamicSamplingContext'; import type { SentrySpan } from './tracing/sentrySpan'; -import { isStreamedBeforeSendSpanCallback } from './tracing/spans/beforeSendSpan'; +import { isStaticBeforeSendSpanCallback } from './tracing/spans/beforeSendSpan'; import type { LegacyCSPReport } from './types/csp'; import type { DsnComponents } from './types/dsn'; import type { @@ -157,7 +157,7 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? const convertToSpanJSON = beforeSendSpan ? (span: SentrySpan) => { const spanJson = spanToJSON(span); - const processedSpan = !isStreamedBeforeSendSpanCallback(beforeSendSpan) ? beforeSendSpan(spanJson) : spanJson; + const processedSpan = isStaticBeforeSendSpanCallback(beforeSendSpan) ? beforeSendSpan(spanJson) : spanJson; if (!processedSpan) { showSpanDropWarning(); diff --git a/packages/core/src/integrations/spanStreaming.ts b/packages/core/src/integrations/spanStreaming.ts index ffd5d0bb7694..36bc2ff55c2d 100644 --- a/packages/core/src/integrations/spanStreaming.ts +++ b/packages/core/src/integrations/spanStreaming.ts @@ -1,7 +1,7 @@ import type { IntegrationFn } from '../types/integration'; import { DEBUG_BUILD } from '../debug-build'; import { defineIntegration } from '../integration'; -import { isStreamedBeforeSendSpanCallback } from '../tracing/spans/beforeSendSpan'; +import { isStaticBeforeSendSpanCallback } from '../tracing/spans/beforeSendSpan'; import { captureSpan } from '../tracing/spans/captureSpan'; import { hasSpanStreamingEnabled } from '../tracing/spans/hasSpanStreamingEnabled'; import { SpanBuffer } from '../tracing/spans/spanBuffer'; @@ -24,10 +24,12 @@ export const spanStreamingIntegration = defineIntegration(() => { } const beforeSendSpan = clientOptions.beforeSendSpan; - if (beforeSendSpan && !isStreamedBeforeSendSpanCallback(beforeSendSpan)) { + if (isStaticBeforeSendSpanCallback(beforeSendSpan)) { clientOptions.traceLifecycle = 'static'; DEBUG_BUILD && - debug.warn(`${initialMessage} a beforeSendSpan callback using \`withStreamedSpan\`! ${fallbackMsg}`); + debug.warn( + `SpanStreaming integration is incompatible with a beforeSendSpan callback using \`withStaticSpan\`! ${fallbackMsg}`, + ); return; } diff --git a/packages/core/src/shared-exports.ts b/packages/core/src/shared-exports.ts index 4ee7ef383bd0..5a10457e3aa4 100644 --- a/packages/core/src/shared-exports.ts +++ b/packages/core/src/shared-exports.ts @@ -86,8 +86,12 @@ export { prepareEvent } from './utils/prepareEvent'; export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; export { hasSpansEnabled } from './utils/hasSpansEnabled'; -export { withStreamedSpan } from './tracing/spans/beforeSendSpan'; -export { isStreamedBeforeSendSpanCallback } from './tracing/spans/beforeSendSpan'; +export { + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated + withStreamedSpan, +} from './tracing/spans/beforeSendSpan'; +export { isStaticBeforeSendSpanCallback, isStreamedBeforeSendSpanCallback } from './tracing/spans/beforeSendSpan'; export { safeSetSpanJSONAttributes } from './tracing/spans/captureSpan'; export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; export { handleCallbackErrors } from './utils/handleCallbackErrors'; diff --git a/packages/core/src/tracing/spans/beforeSendSpan.ts b/packages/core/src/tracing/spans/beforeSendSpan.ts index 57797e5daead..3306a594c94f 100644 --- a/packages/core/src/tracing/spans/beforeSendSpan.ts +++ b/packages/core/src/tracing/spans/beforeSendSpan.ts @@ -1,42 +1,63 @@ -import type { CoreOptions } from '../../types/options'; -import type { BeforeSendStreamedSpanCallback } from '../../types/options'; -import type { StreamedSpanJSON } from '../../types/span'; +import type { BeforeSendStaticSpanCallback, BeforeSendStreamedSpanCallback } from '../../types/options'; +import type { SpanJSON, StreamedSpanJSON } from '../../types/span'; import { addNonEnumerableProperty } from '../../utils/object'; -type StaticBeforeSendSpanCallback = CoreOptions['beforeSendSpan']; - /** - * A wrapper to use the new span format in your `beforeSendSpan` callback. + * A wrapper to use the legacy transaction 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}. + * When using `traceLifecycle: 'static'`, wrap your callback with this function + * to receive and return {@link SpanJSON} instead of {@link StreamedSpanJSON}. * * @example * * Sentry.init({ - * traceLifecycle: 'stream', - * beforeSendSpan: withStreamedSpan((span) => { - * // span is of type StreamedSpanJSON + * traceLifecycle: 'static', + * beforeSendSpan: withStaticSpan((span) => { + * // span is of type SpanJSON * return span; * }), * }); * + * @param callback - The callback function that receives and returns a {@link SpanJSON}. + * @returns A callback that is compatible with the `beforeSendSpan` option when using `traceLifecycle: 'static'`. + */ +export function withStaticSpan(callback: (span: SpanJSON) => SpanJSON): BeforeSendStaticSpanCallback { + addNonEnumerableProperty(callback, '_static', true); + return callback as BeforeSendStaticSpanCallback; +} + +/** + * A wrapper to explicitly use the streamed span format in your `beforeSendSpan` callback. + * + * @deprecated `beforeSendSpan` callbacks receive {@link StreamedSpanJSON} by default. + * This function will be removed in SDK version 12. + * * @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'`. + * @returns The provided callback. */ export function withStreamedSpan( callback: (span: StreamedSpanJSON) => StreamedSpanJSON, -): StaticBeforeSendSpanCallback & { _streamed: true } { +): BeforeSendStreamedSpanCallback & { _streamed: true } { addNonEnumerableProperty(callback, '_streamed', true); - return callback as unknown as StaticBeforeSendSpanCallback & { _streamed: true }; + return callback as BeforeSendStreamedSpanCallback & { _streamed: true }; +} + +/** + * Typesafe check to identify if a `beforeSendSpan` callback expects the static span JSON format. + * + * @param callback - The `beforeSendSpan` callback to check. + * @returns `true` if the callback was wrapped with {@link withStaticSpan}. + */ +export function isStaticBeforeSendSpanCallback(callback: unknown): callback is BeforeSendStaticSpanCallback { + return !!callback && typeof callback === 'function' && '_static' in callback && !!callback._static; } /** * 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}. + * @returns `true` unless the callback was wrapped with {@link withStaticSpan}. */ export function isStreamedBeforeSendSpanCallback(callback: unknown): callback is BeforeSendStreamedSpanCallback { - return !!callback && typeof callback === 'function' && '_streamed' in callback && !!callback._streamed; + return !!callback && typeof callback === 'function' && !isStaticBeforeSendSpanCallback(callback); } diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index 9e4283d02133..b5947f9bfe7b 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -663,14 +663,14 @@ export interface ClientOptions SpanJSON) & { _streamed?: true }; + beforeSendSpan?: BeforeSendStreamedSpanCallback | BeforeSendStaticSpanCallbackMarker; /** * An event-processing callback for transaction events, guaranteed to be invoked after all other event @@ -709,12 +709,23 @@ export interface ClientOptions 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`. + * @deprecated `beforeSendSpan` callbacks use {@link StreamedSpanJSON} by default. */ _streamed?: true; }; +declare const beforeSendStaticSpanCallback: unique symbol; + +type BeforeSendStaticSpanCallbackMarker = { + readonly [beforeSendStaticSpanCallback]: true; +}; + +/** A callback for processing spans sent as part of transaction events. */ +export type BeforeSendStaticSpanCallback = ((span: SpanJSON) => SpanJSON) & + BeforeSendStaticSpanCallbackMarker & { + _static: true; + }; + /** Base configuration options for every SDK. */ export interface CoreOptions extends Omit< Partial>, diff --git a/packages/core/test/integrations/spanStreaming.test.ts b/packages/core/test/integrations/spanStreaming.test.ts index 8b2badf575ff..a2cf52f4deed 100644 --- a/packages/core/test/integrations/spanStreaming.test.ts +++ b/packages/core/test/integrations/spanStreaming.test.ts @@ -58,21 +58,21 @@ describe('spanStreamingIntegration (core)', () => { }, ); - it('falls back to static trace lifecycle if beforeSendSpan is not compatible with span streaming', () => { + it('falls back to static trace lifecycle if beforeSendSpan is marked as static', () => { 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, + beforeSendSpan: SentryCore.withStaticSpan(span => span), }); SentryCore.setCurrentClient(client); client.init(); expect(debugSpy).toHaveBeenCalledWith( - 'SpanStreaming integration requires a beforeSendSpan callback using `withStreamedSpan`! Falling back to static trace lifecycle.', + 'SpanStreaming integration is incompatible with a beforeSendSpan callback using `withStaticSpan`! Falling back to static trace lifecycle.', ); debugSpy.mockRestore(); diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 99112f0e35ef..240502ae5842 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -11,6 +11,7 @@ import { setCurrentClient, SyncPromise, withMonitor, + withStaticSpan, } from '../../src'; import * as integrationModule from '../../src/integration'; import * as logsInternalModule from '../../src/logs/internal'; @@ -1065,7 +1066,7 @@ describe('Client', () => { test('calls `beforeSendSpan` and uses original spans without any changes', () => { expect.assertions(3); - const beforeSendSpan = vi.fn(span => span); + const beforeSendSpan = withStaticSpan(vi.fn(span => span)); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); const client = new TestClient(options); @@ -1284,14 +1285,16 @@ describe('Client', () => { }); test('does not modify existing contexts for root span in `beforeSendSpan`', () => { - const beforeSendSpan = vi.fn((span: SpanJSON) => { - return { - ...span, - data: { - modified: 'true', - }, - }; - }); + const beforeSendSpan = withStaticSpan( + vi.fn((span: SpanJSON) => { + return { + ...span, + data: { + modified: 'true', + }, + }; + }), + ); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); const client = new TestClient(options); @@ -1399,10 +1402,12 @@ describe('Client', () => { test('calls `beforeSendSpan` and uses the modified spans', () => { expect.assertions(4); - const beforeSendSpan = vi.fn(span => { - span.data = { version: 'bravo' }; - return span; - }); + const beforeSendSpan = withStaticSpan( + vi.fn(span => { + span.data = { version: 'bravo' }; + return span; + }), + ); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); const client = new TestClient(options); @@ -1481,7 +1486,7 @@ describe('Client', () => { test('does not discard span and warn when returning null from `beforeSendSpan', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); - const beforeSendSpan = vi.fn(() => null as unknown as SpanJSON); + const beforeSendSpan = withStaticSpan(vi.fn(() => null as unknown as SpanJSON)); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); const client = new TestClient(options); diff --git a/packages/core/test/lib/envelope.test.ts b/packages/core/test/lib/envelope.test.ts index e96132a197ce..17cdf5021233 100644 --- a/packages/core/test/lib/envelope.test.ts +++ b/packages/core/test/lib/envelope.test.ts @@ -9,6 +9,7 @@ import { SentrySpan, setAsyncContextStrategy, setCurrentClient, + withStaticSpan, } from '../../src'; import { _enhanceEventWithSdkInfo, createEventEnvelope, createSpanEnvelope } from '../../src/envelope'; import type { Event } from '../../src/types/event'; @@ -196,7 +197,7 @@ describe('createSpanEnvelope', () => { }); it('calls `beforeSendSpan` and uses original span without any changes', () => { - const beforeSendSpan = vi.fn(span => span); + const beforeSendSpan = withStaticSpan(vi.fn(span => span)); const options = getDefaultTestClientOptions({ dsn: 'https://domain/123', beforeSendSpan }); const client = new TestClient(options); @@ -229,10 +230,12 @@ describe('createSpanEnvelope', () => { }); it('calls `beforeSendSpan` and uses the modified span', () => { - const beforeSendSpan = vi.fn(span => { - span.description = `mutated description: ${span.description}`; - return span; - }); + const beforeSendSpan = withStaticSpan( + vi.fn(span => { + span.description = `mutated description: ${span.description}`; + return span; + }), + ); const options = getDefaultTestClientOptions({ dsn: 'https://domain/123', beforeSendSpan }); const client = new TestClient(options); diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index 5a32da39fed0..8f2db3f26953 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -13,6 +13,7 @@ import { markSpanForOtelSourceInference, spanSourceWasExplicitlySet, } from '../../../src/tracing/utils'; +import { withStaticSpan } from '../../../src/tracing/spans/beforeSendSpan'; import type { SpanJSON } from '../../../src/types/span'; import { spanToJSON, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; import { timestampInSeconds } from '../../../src/utils/time'; @@ -270,7 +271,7 @@ describe('SentrySpan', () => { }); test('sends the span if `beforeSendSpan` does not modify the span', () => { - const beforeSendSpan = vi.fn(span => span); + const beforeSendSpan = withStaticSpan(vi.fn(span => span)); const client = new TestClient( getDefaultTestClientOptions({ dsn: 'https://username@domain/123', @@ -296,7 +297,7 @@ describe('SentrySpan', () => { test('does not drop the span if `beforeSendSpan` returns null', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); - const beforeSendSpan = vi.fn(() => null as unknown as SpanJSON); + const beforeSendSpan = withStaticSpan(vi.fn(() => null as unknown as SpanJSON)); const client = new TestClient( getDefaultTestClientOptions({ dsn: 'https://username@domain/123', diff --git a/packages/core/test/lib/tracing/spans/beforeSendSpan.test.ts b/packages/core/test/lib/tracing/spans/beforeSendSpan.test.ts index 79fd838a1b27..2616415d8bf2 100644 --- a/packages/core/test/lib/tracing/spans/beforeSendSpan.test.ts +++ b/packages/core/test/lib/tracing/spans/beforeSendSpan.test.ts @@ -1,26 +1,52 @@ import { describe, expect, it, vi } from 'vitest'; -import { withStreamedSpan } from '../../../../src'; -import { isStreamedBeforeSendSpanCallback } from '../../../../src/tracing/spans/beforeSendSpan'; +import { withStaticSpan, withStreamedSpan } from '../../../../src'; +import { + isStaticBeforeSendSpanCallback, + isStreamedBeforeSendSpanCallback, +} from '../../../../src/tracing/spans/beforeSendSpan'; + +describe('beforeSendSpan callback formats', () => { + describe('withStaticSpan', () => { + it('marks the callback as static without making the marker enumerable', () => { + const beforeSendSpan = vi.fn(); + const wrapped = withStaticSpan(beforeSendSpan); + + expect(wrapped._static).toBe(true); + expect(Object.keys(wrapped)).not.toContain('_static'); + }); + }); -describe('beforeSendSpan for span streaming', () => { describe('withStreamedSpan', () => { - it('should be able to modify the span', () => { + it('marks the callback as streamed', () => { const beforeSendSpan = vi.fn(); const wrapped = withStreamedSpan(beforeSendSpan); expect(wrapped._streamed).toBe(true); }); }); + describe('isStaticBeforeSendSpanCallback', () => { + it('returns true if the callback is wrapped with withStaticSpan', () => { + const wrapped = withStaticSpan(vi.fn()); + + expect(isStaticBeforeSendSpanCallback(wrapped)).toBe(true); + }); + + it('returns false for an unwrapped callback', () => { + expect(isStaticBeforeSendSpanCallback(vi.fn())).toBe(false); + }); + }); + describe('isStreamedBeforeSendSpanCallback', () => { + it('returns true for an unwrapped callback', () => { + expect(isStreamedBeforeSendSpanCallback(vi.fn())).toBe(true); + }); + it('returns true if the callback is wrapped with withStreamedSpan', () => { - const beforeSendSpan = vi.fn(); - const wrapped = withStreamedSpan(beforeSendSpan); - expect(isStreamedBeforeSendSpanCallback(wrapped)).toBe(true); + expect(isStreamedBeforeSendSpanCallback(withStreamedSpan(vi.fn()))).toBe(true); }); - it('returns false if the callback is not wrapped with withStreamedSpan', () => { - const beforeSendSpan = vi.fn(); - expect(isStreamedBeforeSendSpanCallback(beforeSendSpan)).toBe(false); + it('returns false if the callback is wrapped with withStaticSpan', () => { + expect(isStreamedBeforeSendSpanCallback(withStaticSpan(vi.fn()))).toBe(false); }); }); }); diff --git a/packages/core/test/lib/tracing/spans/captureSpan.test.ts b/packages/core/test/lib/tracing/spans/captureSpan.test.ts index 0560ebd2f702..34c35868d20b 100644 --- a/packages/core/test/lib/tracing/spans/captureSpan.test.ts +++ b/packages/core/test/lib/tracing/spans/captureSpan.test.ts @@ -15,8 +15,8 @@ import { SEMANTIC_ATTRIBUTE_USER_USERNAME, startInactiveSpan, startSpan, + withStaticSpan, withScope, - withStreamedSpan, } from '../../../../src'; import { safeSetSpanJSONAttributes } from '../../../../src/tracing/spans/captureSpan'; import { scopeContextsToSpanAttributes } from '../../../../src/tracing/spans/scopeContextAttributes'; @@ -475,8 +475,8 @@ describe('captureSpan', () => { }); describe('beforeSendSpan', () => { - it('applies beforeSendSpan if it is a span streaming compatible callback', () => { - const beforeSendSpan = withStreamedSpan(vi.fn(span => span)); + it('applies an unwrapped beforeSendSpan callback', () => { + const beforeSendSpan = vi.fn(span => span); const client = new TestClient( getDefaultTestClientOptions({ @@ -496,8 +496,8 @@ describe('captureSpan', () => { expect(beforeSendSpan).toHaveBeenCalledWith(expect.objectContaining({ span_id: span.spanContext().spanId })); }); - it("doesn't apply beforeSendSpan if it is not a span streaming compatible callback", () => { - const beforeSendSpan = vi.fn(span => span); + it("doesn't apply beforeSendSpan if it is marked as static", () => { + const beforeSendSpan = withStaticSpan(vi.fn(span => span)); const client = new TestClient( getDefaultTestClientOptions({ @@ -519,8 +519,7 @@ describe('captureSpan', () => { it('logs a warning if the beforeSendSpan callback returns null', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); - // @ts-expect-error - the types dissallow returning null but this is javascript, so we need to test it - const beforeSendSpan = withStreamedSpan(() => null); + const beforeSendSpan = vi.fn(() => null as unknown as StreamedSpanJSON); const client = new TestClient( getDefaultTestClientOptions({ diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 7bfba75dbb2e..4e0841a67edd 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -97,6 +97,8 @@ export { wrapMcpServerWithSentry, featureFlagsIntegration, metrics, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, logger, consoleLoggingIntegration, diff --git a/packages/effect/src/index.types.ts b/packages/effect/src/index.types.ts index e1544da73485..940d59f49f7f 100644 --- a/packages/effect/src/index.types.ts +++ b/packages/effect/src/index.types.ts @@ -22,6 +22,8 @@ export declare function init(options: Options | clientSdk.BrowserOptions | serve export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; +export declare const withStaticSpan: typeof clientSdk.withStaticSpan; +// eslint-disable-next-line typescript/no-deprecated export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index 715c2484296e..d5939487ea8d 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -155,6 +155,8 @@ export { unleashIntegration, metrics, spanStreamingIntegration, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, bunServerIntegration, makeFetchTransport, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index d07490563212..2a9978672951 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -160,6 +160,8 @@ export { unleashIntegration, metrics, spanStreamingIntegration, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, } from '@sentry/node'; diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index e0d918708bb1..151d338cf3ce 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -27,6 +27,8 @@ export declare const consoleIntegration: typeof serverSdk.consoleIntegration; // Node-only at runtime; the edge build exports an inert shim so named imports resolve in edge-compiled modules. export declare const pinoIntegration: typeof serverSdk.pinoIntegration; export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; +export declare const withStaticSpan: typeof clientSdk.withStaticSpan; +// eslint-disable-next-line typescript/no-deprecated export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan; // Different implementation in server and worker diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index cbbc614c82fe..c677ccb53517 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -178,7 +178,8 @@ export type { CaptureContext, } from '@sentry/core'; -export { metrics, withStreamedSpan } from '@sentry/core'; +// eslint-disable-next-line typescript/no-deprecated +export { metrics, withStaticSpan, withStreamedSpan } from '@sentry/core'; export * as logger from './logs/exports'; export { childProcessIntegration } from './integrations/childProcess'; diff --git a/packages/nuxt/src/index.types.ts b/packages/nuxt/src/index.types.ts index e058e0bfd099..e2e8ba3b66d0 100644 --- a/packages/nuxt/src/index.types.ts +++ b/packages/nuxt/src/index.types.ts @@ -17,6 +17,8 @@ export declare function init(options: Options | SentryNuxtClientOptions | Sentry export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; +export declare const withStaticSpan: typeof clientSdk.withStaticSpan; +// eslint-disable-next-line typescript/no-deprecated export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/react-router/src/index.types.ts b/packages/react-router/src/index.types.ts index e7fb4382d0f2..16f2eca441a3 100644 --- a/packages/react-router/src/index.types.ts +++ b/packages/react-router/src/index.types.ts @@ -17,6 +17,8 @@ export declare function init(options: Options | clientSdk.BrowserOptions | serve export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; +export declare const withStaticSpan: typeof clientSdk.withStaticSpan; +// eslint-disable-next-line typescript/no-deprecated export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan; export declare const defaultStackParser: StackParser; export declare const getDefaultIntegrations: (options: Options) => Integration[]; diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index ef2f89cee2a5..a91869f0d976 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -117,6 +117,8 @@ export { spanToTraceHeader, spanToBaggageHeader, updateSpanName, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, featureFlagsIntegration, } from '@sentry/core'; diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts index 1a54068ae019..867bc011dfc2 100644 --- a/packages/remix/src/index.types.ts +++ b/packages/remix/src/index.types.ts @@ -19,6 +19,8 @@ export declare const browserTracingIntegration: typeof clientSdk.browserTracingI export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; +export declare const withStaticSpan: typeof clientSdk.withStaticSpan; +// eslint-disable-next-line typescript/no-deprecated export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan; export declare const getDefaultIntegrations: (options: Options) => Integration[]; diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index 4fd2331b34fa..e21dffa78807 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -133,6 +133,8 @@ export { createConsolaReporter, createSentryWinstonTransport, spanStreamingIntegration, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, } from '@sentry/node'; diff --git a/packages/solidstart/src/index.types.ts b/packages/solidstart/src/index.types.ts index d984e1384a93..1b458e7cf929 100644 --- a/packages/solidstart/src/index.types.ts +++ b/packages/solidstart/src/index.types.ts @@ -19,6 +19,8 @@ export declare function init(options: Options | clientSdk.BrowserOptions | serve export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; +export declare const withStaticSpan: typeof clientSdk.withStaticSpan; +// eslint-disable-next-line typescript/no-deprecated export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan; export declare const getDefaultIntegrations: (options: Options) => Integration[]; diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index fd5fbb2e0e39..e46cd2744e49 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -132,6 +132,8 @@ export { createConsolaReporter, createSentryWinstonTransport, spanStreamingIntegration, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, } from '@sentry/node'; diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index 1f4bbae0b4a9..0a328b38ab1b 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -48,6 +48,8 @@ export declare function wrapLoadWithSentry any>(orig export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; +export declare const withStaticSpan: typeof clientSdk.withStaticSpan; +// eslint-disable-next-line typescript/no-deprecated export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan; // Different implementation in server and worker diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 5ebd934e9e88..a6defb62c39d 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -137,6 +137,8 @@ export { vercelAIIntegration, metrics, spanStreamingIntegration, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, } from '@sentry/node'; diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index de2146c9e259..e52ebc8aedc4 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -83,6 +83,8 @@ export { withIsolationScope, withMonitor, withScope, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, supabaseIntegration, instrumentSupabaseClient, diff --git a/packages/tanstackstart-react/src/index.types.ts b/packages/tanstackstart-react/src/index.types.ts index d6465a0e14d2..faedd2ab18b5 100644 --- a/packages/tanstackstart-react/src/index.types.ts +++ b/packages/tanstackstart-react/src/index.types.ts @@ -19,6 +19,8 @@ export declare function init(options: Options | clientSdk.BrowserOptions | serve export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration; +export declare const withStaticSpan: typeof clientSdk.withStaticSpan; +// eslint-disable-next-line typescript/no-deprecated export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan; export declare const getDefaultIntegrations: (options: Options) => Integration[]; diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index d8e277f9fea0..7fef86995fee 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -105,6 +105,8 @@ export { featureFlagsIntegration, logger, metrics, + withStaticSpan, + // eslint-disable-next-line typescript/no-deprecated withStreamedSpan, spanStreamingIntegration, } from '@sentry/core';