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)', 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 b05934465dfb..9968a4489c08 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 } from './utils/beforeSendSpan'; export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; export { handleCallbackErrors } from './utils/handleCallbackErrors'; export { parameterize, fmt } from './utils/parameterize'; diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 9f8baca5b428..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'; @@ -500,6 +500,14 @@ export interface ClientOptions SpanJSON; + beforeSendSpan?: ((span: SpanJSON) => SpanJSON) | BeforeSendStramedSpanCallback; /** * An event-processing callback for transaction events, guaranteed to be invoked after all other event @@ -615,6 +626,19 @@ export interface ClientOptions Breadcrumb | null; } +/** + * A callback for processing streamed spans before they are sent. + * + * @see {@link StreamedSpanJSON} for the streamed span format used with `traceLifecycle: 'stream'` + */ +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`. + */ + _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..68c4576d179d --- /dev/null +++ b/packages/core/src/utils/beforeSendSpan.ts @@ -0,0 +1,41 @@ +import type { BeforeSendStramedSpanCallback, 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, +): BeforeSendStramedSpanCallback { + addNonEnumerableProperty(callback, '_streamed', true); + return callback; +} + +/** + * 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 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); + }); + }); +});