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 @@ -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)',
Expand All @@ -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)',
Expand Down Expand Up @@ -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)
{
Expand All @@ -163,28 +163,28 @@ 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)
{
name: '@sentry/svelte',
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)',
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1498,7 +1499,9 @@ function processBeforeSend(
event: Event,
hint: EventHint,
): PromiseLike<Event | null> | 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) {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
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 @@ -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';
Expand Down
28 changes: 26 additions & 2 deletions packages/core/src/types-hoist/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -500,6 +500,14 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
*/
strictTraceContinuation?: boolean;

/**
* The trace lifecycle, determining whether spans are sent statically when the entire local span tree is complete,
* or streamed in batches, following interval- and action-based triggers.
*
* @default 'static'
*/
traceLifecycle?: 'static' | 'stream';

/**
* The organization ID for your Sentry project.
*
Expand Down Expand Up @@ -579,11 +587,14 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
/**
* This function can be defined to modify a child span before it's sent.
*
* When using `traceLifecycle: 'stream'`, wrap your callback with {@link withStreamedSpan}
* to receive and return {@link StreamedSpanJSON} instead.
*
* @param span The span generated by the SDK.
*
* @returns The modified span payload that will be sent.
*/
beforeSendSpan?: (span: SpanJSON) => SpanJSON;
beforeSendSpan?: ((span: SpanJSON) => SpanJSON) | BeforeSendStramedSpanCallback;

/**
* An event-processing callback for transaction events, guaranteed to be invoked after all other event
Expand Down Expand Up @@ -615,6 +626,19 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => 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;
Copy link
Member

Choose a reason for hiding this comment

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

M: As only true is allowed here, we might need to set this type to true | undefined in case someone enabled exactOptionalPropertyTypes.

Copy link
Member Author

@Lms24 Lms24 Feb 3, 2026

Choose a reason for hiding this comment

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

You're right. However, users shouldn't ever set this but just use the withStreamedSpan helper instead. That being said, it is public API, so we could widen the type enough to make this possible.

Tbh, this withStreamedSpan thing isn't ideal but maybe I could pick your brain a bit for a nicer solution? I was contemplating discrimanted union typing via traceLifecycle but couldn't get it to work without breaking changes for existing users. Maybe you have some ideas how to fix that.

A third option would be to introduce a beforeSendStreamedSpan API for the time span streaming is opt in. Also not ideal because it's again additional API with no great migration path.

Def open for other ideas!

};

/** Base configuration options for every SDK. */
export interface CoreOptions<TO extends BaseTransportOptions = BaseTransportOptions>
extends Omit<Partial<ClientOptions<TO>>, 'integrations' | 'transport' | 'stackParser'> {
Expand Down
41 changes: 41 additions & 0 deletions packages/core/src/utils/beforeSendSpan.ts
Original file line number Diff line number Diff line change
@@ -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;
}
26 changes: 26 additions & 0 deletions packages/core/test/lib/utils/beforeSendSpan.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading