diff --git a/MIGRATION.md b/MIGRATION.md index e939dd14dc04..72d8d5c39030 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -194,15 +194,161 @@ The same applies to the no-code entry points, e.g. `node --import=@sentry/node/i Affected SDKs: All SDKs. -Each span is sent to Sentry the moment it finishes instead of being buffered until the root span completes. This means spans are no longer bound by the 1000-span per transaction limit and their individual payload-size limits have been increased. +Spans are now sent to Sentry in small batches instead of being buffered until the root span completes. +This means spans are no longer bound by the 1000-span per transaction limit and their individual payload-size limits have been increased. -The new model comes with some changes to Sentry hooks such as `beforeSendSpan` or options like `ignoreSpans` and requires manual migration. `beforeSendTransaction` and `ignoreTransactions` will **no-op**. Users who cannot migrate yet can opt into the previous transaction-based static model. +The new model comes with some changes to Sentry hooks such as `beforeSendSpan` or options like `ignoreSpans` and requires manual migration. +The `beforeSendTransaction` and `ignoreTransactions` options will **no-op**. +If you cannot migrate to span streaming yet, you can opt into the previous transaction-based static model. -> **TODO(v11):** The migration path for span streaming is still being defined. Document: -> -> - the concrete before/after for `beforeSendSpan` and `ignoreSpans`, -> - the exact replacement for `beforeSendTransaction` / `ignoreTransactions`, -> - how to opt back into the transaction-based model (option name + example). +#### `beforeSendSpan` receives the streamed span format + +Your `beforeSendSpan` callback now receives a `StreamedSpanJSON` object and is invoked as each span finishes, rather than for all spans of a transaction right before that transaction is sent. As in v10, it is invoked for the root span as well as for child spans. + +The payload fields were renamed: + +| Before (`SpanJSON`) | After (`StreamedSpanJSON`) | +| ------------------- | ------------------------------ | +| `description` | `name` | +| `data` | `attributes` | +| `op` | `attributes['sentry.op']` | +| `timestamp` | `end_timestamp` | +| `status` (`string`) | `status` (`'ok'` or `'error'`) | + +The `status` field, now only contains two statuses: `'ok'` and `'error'`. +Streamed spans always have a status (while status was optional on transaction-based spans). +Previously more fine-grained error statuses are now mapped to `'error'`. +Additional error information may be set via span attributes (e.g. `sentry.status.message`). + +```js +// Before +Sentry.init({ + beforeSendSpan: span => { + if (span.op === 'db.query') { + span.description = scrub(span.description); + span.data['db.statement'] = scrub(span.data['db.statement']); + } + return span; + }, +}); + +// After +Sentry.init({ + beforeSendSpan: span => { + if (span.attributes?.['sentry.op'] === 'db.query') { + span.name = scrub(span.name); + span.attributes['db.statement'] = scrub(span.attributes['db.statement']); + } + return span; + }, +}); +``` + +Returning `null` to drop a span was already disallowed in v9 and remains a no-op. Use `ignoreSpans` to filter spans. + +If you cannot migrate the callback yet, opt out of span streaming and wrap `beforeSendSpan` with `Sentry.withStaticSpan()`: + +```js +Sentry.init({ + traceLifecycle: 'static', + beforeSendSpan: Sentry.withStaticSpan(span => { + span.description = scrub(span.description); + return span; + }), +}); +``` + +A `beforeSendSpan` callback that does not match the configured `traceLifecycle` is **never invoked** — an unwrapped callback is ignored in `'static'` mode, and a `withStaticSpan`-wrapped callback is ignored in `'stream'` mode. Enable debug logging to surface a warning about the mismatch. Previously, an incompatible callback silently downgraded the SDK to the static lifecycle instead. + +The `withStreamedSpan()` helper is now a no-op, since streamed payloads are the default. It is deprecated and will be removed in v12. You can remove the wrapper: + +```js +// Before +beforeSendSpan: Sentry.withStreamedSpan(span => span); + +// After +beforeSendSpan: span => span; +``` + +The internal `isStreamedBeforeSendSpanCallback()` function from `@sentry/core` was removed. + +#### Replacing `beforeSendTransaction` + +`beforeSendTransaction` no-ops because no transaction events are produced. +For **scrubbing and data modification**, move the logic to `beforeSendSpan` and guard on `is_segment` to target what used to be the transaction +For **dropping** a transaction or child spans, use `ignoreSpans` (see below). The `beforeSendSpan` callback cannot drop spans. + +```js +// Before +Sentry.init({ + beforeSendTransaction: event => { + if (event.transaction === 'GET /health') { + return null; + } + event.transaction = scrubIds(event.transaction); + return event; + }, +}); + +// After +Sentry.init({ + ignoreSpans: [ + 'GET /health' + ] + beforeSendSpan: span => { + if (span.is_segment) { + span.name = scrubIds(span.name); + } + return span; + }, +}); +``` + +Note that scope `tags` and `extra` are not carried over to streamed spans, since spans only have attributes. Use `Sentry.setAttribute()` / `Sentry.setAttributes()` instead. + +#### Replacing `ignoreTransactions` with `ignoreSpans` + +`ignoreTransactions` no-ops. Use `ignoreSpans` to match the segment span instead: when a segment span is ignored, all of its child spans are dropped with it, which is equivalent to dropping the whole transaction. + +```js +// Before +Sentry.init({ + ignoreTransactions: ['GET /health'], +}); + +// After +Sentry.init({ + ignoreSpans: ['GET /health'], +}); +``` + +`ignoreSpans` matches on the span `name` (formerly `description`). Because it applies to every span rather than just to root spans, consider narrowing the filter with the object form so that child spans sharing a name are not dropped as collateral: + +```js +Sentry.init({ + ignoreSpans: [{ name: 'GET /health', attributes: { 'sentry.op': 'http.server' } }], +}); +``` + +`ignoreSpans` itself is unchanged in shape, but it now takes effect when a span **starts** rather than when the transaction is sent. Matched spans are never recorded at all, which means a matched non-segment span's children are re-parented to its parent instead of being dropped. + +#### Opting out of span streaming + +To keep the previous transaction-based model, set `traceLifecycle: 'static'`: + +```js +Sentry.init({ + traceLifecycle: 'static', + + // `beforeSendSpan` MUST be wrapped with Sentry.withStaticSpan: + beforeSendSpan: Sentry.withStaticSpan(span => { + span.description = scrub(span.description); + return span; + }), +}); +``` + +In Node, Bun, Vercel Edge and Cloudflare you can also set the `SENTRY_TRACE_LIFECYCLE=static` environment variable instead. The static lifecycle only exists for backwards compatibility and is planned for removal in a future major version, so treat this as a temporary measure. ### Logs are enabled by default 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/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/init.js index 64cd4ccff9d7..5ea3f7b57bf2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/init.js @@ -17,14 +17,14 @@ Sentry.init({ tracesSampleRate: 1, // A plain (non-streamed) `beforeSendSpan` operates on the v1 `SpanJSON`. INP is sent as a v2 span, // so this verifies the static callback still runs and its changes are carried into the v2 span. - beforeSendSpan: span => { + beforeSendSpan: Sentry.withStaticSpan(span => { if (span.op === 'ui.interaction.click') { span.description = 'scrubbed'; span.data['custom.attribute'] = 'from-before-send-span'; } return span; - }, + }), debug: true, }); diff --git a/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-static/scenario-unwrapped.ts b/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-static/scenario-unwrapped.ts new file mode 100644 index 000000000000..e3cad96f2f45 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-static/scenario-unwrapped.ts @@ -0,0 +1,28 @@ +import type { SpanJSON } from '@sentry/core'; +import type { NodeOptions } from '@sentry/node'; +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +// Simulates a callback that was not migrated to `withStaticSpan`. The cast stands in for the +// JavaScript users who don't get a type error here. +const unmigratedBeforeSendSpan = ((span: SpanJSON) => { + span.description = 'thisShouldNotBeApplied'; + return span; +}) as unknown as NodeOptions['beforeSendSpan']; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + transport: loggingTransport, + release: '1.0.0', + traceLifecycle: 'static', + beforeSendSpan: unmigratedBeforeSendSpan, +}); + +Sentry.startSpan({ name: 'test-span', op: 'test' }, () => { + Sentry.startSpan({ name: 'test-child-span', op: 'test-child' }, () => { + // noop + }); +}); + +void Sentry.flush(); diff --git a/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-static/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-static/scenario.ts new file mode 100644 index 000000000000..8dee60e5b99d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-static/scenario.ts @@ -0,0 +1,31 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + transport: loggingTransport, + release: '1.0.0', + traceLifecycle: 'static', + beforeSendSpan: Sentry.withStaticSpan(span => { + if (span.description === 'test-child-span') { + span.description = 'customChildSpanName'; + span.data['sentry.custom_attribute'] = 'customAttributeValue'; + } + + if (span.is_segment) { + span.description = 'customRootSpanName'; + span.data['sentry.custom_root_attribute'] = 'customRootAttributeValue'; + } + + return span; + }), +}); + +Sentry.startSpan({ name: 'test-span', op: 'test' }, () => { + Sentry.startSpan({ name: 'test-child-span', op: 'test-child' }, () => { + // noop + }); +}); + +void Sentry.flush(); diff --git a/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-static/test.ts b/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-static/test.ts new file mode 100644 index 000000000000..1dc4c63b042b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/beforeSendSpan-static/test.ts @@ -0,0 +1,46 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('withStaticSpan applies changes to child spans', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: event => { + expect(event.spans).toHaveLength(1); + + const childSpan = event.spans![0]!; + expect(childSpan.description).toBe('customChildSpanName'); + expect(childSpan.data['sentry.custom_attribute']).toBe('customAttributeValue'); + }, + }) + .start() + .completed(); +}); + +test('withStaticSpan applies changes to the root span', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: event => { + expect(event.transaction).toBe('customRootSpanName'); + expect(event.contexts?.trace?.data?.['sentry.custom_root_attribute']).toBe('customRootAttributeValue'); + }, + }) + .start() + .completed(); +}); + +test('a beforeSendSpan callback without withStaticSpan is not invoked in the static trace lifecycle', async () => { + await createRunner(__dirname, 'scenario-unwrapped.ts') + .expect({ + transaction: event => { + expect(event.transaction).toBe('test-span'); + expect(event.spans).toHaveLength(1); + expect(event.spans![0]!.description).toBe('test-child-span'); + }, + }) + .start() + .completed(); +}); 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 ecf793e49585..23aab90f4edf 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -150,8 +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 - '_streamed', + // Marker used to detect `beforeSendSpan` callbacks expecting the static span format + '_static', // This is used in metadata integration '_sentryModuleMetadata', ], diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 245c0993459d..c011b2a9d4b2 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -172,6 +172,8 @@ export { unleashIntegration, growthbookIntegration, spanStreamingIntegration, + withStaticSpan, + // oxlint-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..3472adfad925 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; +// oxlint-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 faa62b15b6d8..78fd0d96733c 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -159,6 +159,8 @@ export { growthbookIntegration, metrics, spanStreamingIntegration, + withStaticSpan, + // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, } from '@sentry/node'; diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index d1e9d7662db3..86e038cbb850 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -72,6 +72,8 @@ export { spanToTraceHeader, spanToBaggageHeader, updateSpanName, + withStaticSpan, + // oxlint-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 e8ed1a0de537..2c3c69aeaa1f 100644 --- a/packages/browser/src/integrations/spanstreaming.ts +++ b/packages/browser/src/integrations/spanstreaming.ts @@ -4,34 +4,19 @@ import { debug, defineIntegration, hasSpanStreamingEnabled, - isStreamedBeforeSendSpanCallback, SpanBuffer, spanIsSampled, } from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build'; +export const INTEGRATION_NAME = 'SpanStreaming' as const; + export const spanStreamingIntegration = defineIntegration(() => { return { - name: 'SpanStreaming' as const, - + name: INTEGRATION_NAME, setup(client) { - const initialMessage = 'SpanStreaming integration requires'; - const fallbackMsg = 'Falling back to static trace lifecycle.'; - const clientOptions = client.getOptions(); - if (!hasSpanStreamingEnabled(client)) { - clientOptions.traceLifecycle = 'static'; - DEBUG_BUILD && debug.warn(`${initialMessage} \`traceLifecycle\` to be set to "stream"! ${fallbackMsg}`); - return; - } - - 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)) { - clientOptions.traceLifecycle = 'static'; - DEBUG_BUILD && - debug.warn(`${initialMessage} a beforeSendSpan callback using \`withStreamedSpan\`! ${fallbackMsg}`); + DEBUG_BUILD && debug.log(`[${INTEGRATION_NAME}] \`traceLifecycle\` is "static", skipping setup.`); return; } diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index d3bf3c960d6e..84f4940c8b41 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -19,7 +19,10 @@ import { globalHandlersIntegration } from './integrations/globalhandlers'; import { httpContextIntegration } from './integrations/httpcontext'; import { linkedErrorsIntegration } from './integrations/linkederrors'; import { spotlightBrowserIntegration } from './integrations/spotlight'; -import { spanStreamingIntegration } from './integrations/spanstreaming'; +import { + spanStreamingIntegration, + INTEGRATION_NAME as SPAN_STREAMING_INTEGRATION_NAME, +} from './integrations/spanstreaming'; import { defaultStackParser } from './stack-parsers'; import { makeFetchTransport } from './transports/fetch'; import { normalizeStringifyValue } from './normalizeStringifyValue'; @@ -116,9 +119,10 @@ export function init(options: BrowserOptions = {}): Client | undefined { defaultIntegrations, }); - options.traceLifecycle ??= 'stream'; - - if (options.traceLifecycle === 'stream' && !integrations.some(integration => integration.name === 'SpanStreaming')) { + if ( + options.traceLifecycle !== 'static' && + !integrations.some(integration => integration.name === SPAN_STREAMING_INTEGRATION_NAME) + ) { integrations.push(spanStreamingIntegration()); } diff --git a/packages/browser/test/integrations/spanstreaming.test.ts b/packages/browser/test/integrations/spanstreaming.test.ts index db4209a823e8..16c93f6c8f12 100644 --- a/packages/browser/test/integrations/spanstreaming.test.ts +++ b/packages/browser/test/integrations/spanstreaming.test.ts @@ -6,7 +6,7 @@ import { } from '@sentry/core/browser'; import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { BrowserClient, spanStreamingIntegration } from '../../src'; +import { BrowserClient, spanStreamingIntegration, withStaticSpan } from '../../src'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; // Mock SpanBuffer as a class that can be instantiated @@ -40,62 +40,60 @@ describe('spanStreamingIntegration', () => { expect(integration.setup).toBeDefined(); }); - it.each(['static', 'somethingElse'])( - 'logs a warning if traceLifecycle is not set to "stream" but to %s', - traceLifecycle => { - const debugSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - const client = new BrowserClient({ - ...getDefaultBrowserClientOptions(), - dsn: 'https://username@domain/123', - integrations: [spanStreamingIntegration()], - // @ts-expect-error - we want to test the warning for invalid traceLifecycle values - traceLifecycle, - }); - - 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(() => {}); + it('does not set up span streaming if traceLifecycle is "static"', () => { + const debugSpy = vi.spyOn(debug, 'log').mockImplementation(() => {}); const client = new BrowserClient({ ...getDefaultBrowserClientOptions(), dsn: 'https://username@domain/123', integrations: [spanStreamingIntegration()], - traceLifecycle: 'stream', - beforeSendSpan: (span: Span) => span, + traceLifecycle: 'static', + tracesSampleRate: 1, }); SentryCore.setCurrentClient(client); client.init(); - expect(debugSpy).toHaveBeenCalledWith( - 'SpanStreaming integration requires a beforeSendSpan callback using `withStreamedSpan`! Falling back to static trace lifecycle.', - ); + expect(debugSpy).toHaveBeenCalledWith('[SpanStreaming] `traceLifecycle` is "static", skipping setup.'); debugSpy.mockRestore(); - expect(client.getOptions().traceLifecycle).toBe('static'); + expect(MockSpanBuffer).not.toHaveBeenCalled(); + + // Without the hooks registered, ending a span must not enqueue anything + client.emit('afterSpanEnd', new SentryCore.SentrySpan({ name: 'test', sampled: true })); + expect(mockSpanBufferInstance.add).not.toHaveBeenCalled(); + }); + + it.each([ + ['explicitly set to "stream"', 'stream' as const], + ['left unset', undefined], + ])('sets up span streaming if traceLifecycle is %s', (_, traceLifecycle) => { + const client = new BrowserClient({ + ...getDefaultBrowserClientOptions(), + dsn: 'https://username@domain/123', + integrations: [spanStreamingIntegration()], + traceLifecycle, + }); + + SentryCore.setCurrentClient(client); + client.init(); + + expect(MockSpanBuffer).toHaveBeenCalledTimes(1); + expect(client.getOptions().traceLifecycle).toBe('stream'); }); - it('does nothing if traceLifecycle set to "stream"', () => { + it('still sets up span streaming if beforeSendSpan is wrapped with withStaticSpan', () => { const client = new BrowserClient({ ...getDefaultBrowserClientOptions(), dsn: 'https://username@domain/123', integrations: [spanStreamingIntegration()], traceLifecycle: 'stream', + beforeSendSpan: withStaticSpan(span => span), }); SentryCore.setCurrentClient(client); client.init(); + expect(MockSpanBuffer).toHaveBeenCalledTimes(1); expect(client.getOptions().traceLifecycle).toBe('stream'); }); diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index deecd197a3a9..e6639bf842d5 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -176,6 +176,8 @@ export { unleashIntegration, metrics, spanStreamingIntegration, + withStaticSpan, + // oxlint-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 2a0a3050f8e3..78b808427b80 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -105,6 +105,8 @@ export { growthbookIntegration, logger, metrics, + withStaticSpan, + // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, spanStreamingIntegration, } from '@sentry/core'; diff --git a/packages/cloudflare/test/instrumentCloudflareAgent.test.ts b/packages/cloudflare/test/instrumentCloudflareAgent.test.ts index 5f0211fdc739..618377156fd1 100644 --- a/packages/cloudflare/test/instrumentCloudflareAgent.test.ts +++ b/packages/cloudflare/test/instrumentCloudflareAgent.test.ts @@ -10,6 +10,7 @@ import { import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; import { CloudflareClient, type CloudflareClientOptions } from '../src/client'; +import { withStaticSpan } from '../src/index'; import { instrumentCloudflareAgent } from '../src/instrumentations/agents'; import { resetSdk } from './testUtils'; @@ -62,10 +63,10 @@ describe('instrumentCloudflareAgent', () => { transactions.push(event); return event; }, - beforeSendSpan: span => { + beforeSendSpan: withStaticSpan(span => { childSpans.push(span); return span; - }, + }), }; client = new CloudflareClient(options); diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 8de2eb856239..f9a20a930705 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'; @@ -39,7 +39,7 @@ import type { StartSpanOptions } from './types/startSpanOptions'; import type { Transport, TransportMakeRequestResponse } from './types/transport'; import type { ResolvedDataCollection } from './types/datacollection'; import { createClientReportEnvelope } from './utils/clientreport'; -import { debug } from './utils/debug-logger'; +import { consoleSandbox, debug } from './utils/debug-logger'; import { dsnToString, makeDsn } from './utils/dsn'; import { addItemToEnvelope, createAttachmentEnvelopeItem } from './utils/envelope'; import { getPossibleEventMessages } from './utils/eventUtils'; @@ -234,7 +234,13 @@ export abstract class Client { * @param options Options for the client. */ protected constructor(options: O) { - this._options = { attachStacktrace: true, traceLifecycle: 'stream', ...options }; + // Any value other than `'static'` normalizes to the `'stream'` default, so that `traceLifecycle` + // is always one of the two known values for the rest of the SDK. + this._options = { + attachStacktrace: true, + ...options, + traceLifecycle: options.traceLifecycle === 'static' ? 'static' : 'stream', + }; this._integrations = {}; this._numProcessing = 0; this._outcomes = {}; @@ -250,6 +256,24 @@ export abstract class Client { DEBUG_BUILD && debug.warn('No DSN provided, client will not send events.'); } + const { beforeSendSpan, traceLifecycle } = this._options; + // A `beforeSendSpan` callback is only invoked for the span format matching the trace lifecycle, + // so a mismatch means it is silently never called. + if ( + DEBUG_BUILD && + beforeSendSpan && + isStaticBeforeSendSpanCallback(beforeSendSpan) !== (traceLifecycle === 'static') + ) { + consoleSandbox(() => { + // oxlint-disable-next-line no-console + console.warn( + `Ignoring \`beforeSendSpan\`: ${ + traceLifecycle === 'static' ? 'wrap it with' : 'remove' + } \`Sentry.withStaticSpan\` to use it with \`traceLifecycle: "${traceLifecycle}"\`.`, + ); + }); + } + if (this._dsn) { const url = getEnvelopeEndpointWithUrlEncodedAuth( this._dsn, @@ -1677,7 +1701,7 @@ function processBeforeSend( // oxlint-disable-next-line typescript/no-deprecated beforeSendTransaction, } = options; - const beforeSendSpan = !isStreamedBeforeSendSpanCallback(options.beforeSendSpan) && options.beforeSendSpan; + const beforeSendSpan = isStaticBeforeSendSpanCallback(options.beforeSendSpan) && options.beforeSendSpan; let processedEvent = event; diff --git a/packages/core/src/integrations/spanStreaming.ts b/packages/core/src/integrations/spanStreaming.ts index ffd5d0bb7694..d325906b6b1e 100644 --- a/packages/core/src/integrations/spanStreaming.ts +++ b/packages/core/src/integrations/spanStreaming.ts @@ -1,33 +1,21 @@ import type { IntegrationFn } from '../types/integration'; 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'; +export const INTEGRATION_NAME = 'SpanStreaming' as const; + export const spanStreamingIntegration = defineIntegration(() => { return { - name: 'SpanStreaming' as const, + name: INTEGRATION_NAME, setup(client) { - const initialMessage = 'SpanStreaming integration requires'; - const fallbackMsg = 'Falling back to static trace lifecycle.'; - const clientOptions = client.getOptions(); - if (!hasSpanStreamingEnabled(client)) { - clientOptions.traceLifecycle = 'static'; - DEBUG_BUILD && debug.warn(`${initialMessage} \`traceLifecycle\` to be set to "stream"! ${fallbackMsg}`); - return; - } - - const beforeSendSpan = clientOptions.beforeSendSpan; - if (beforeSendSpan && !isStreamedBeforeSendSpanCallback(beforeSendSpan)) { - clientOptions.traceLifecycle = 'static'; - DEBUG_BUILD && - debug.warn(`${initialMessage} a beforeSendSpan callback using \`withStreamedSpan\`! ${fallbackMsg}`); + DEBUG_BUILD && debug.log(`[${INTEGRATION_NAME}] \`traceLifecycle\` is "static", skipping setup.`); return; } diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index ec10c516ef9b..e0426e558ed3 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -2,7 +2,10 @@ import { createCheckInEnvelope } from './checkin'; import { Client } from './client'; import { getIsolationScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; -import { spanStreamingIntegration } from './integrations/spanStreaming'; +import { + spanStreamingIntegration, + INTEGRATION_NAME as SPAN_STREAMING_INTEGRATION_NAME, +} from './integrations/spanStreaming'; import type { Scope } from './scope'; import { DEFAULT_TRANSPORT_BUFFER_SIZE } from './transports/base'; import { addUserAgentToTransportHeaders } from './transports/userAgent'; @@ -40,12 +43,13 @@ export class ServerRuntimeClient< public constructor(options: O) { addUserAgentToTransportHeaders(options); - options.traceLifecycle ??= 'stream'; - // When span streaming is enabled (`traceLifecycle: 'stream'`), the `spanStreamingIntegration` // is required to flush spans. We add it here so the individual server SDKs don't have to. // A user-provided `spanStreamingIntegration` always takes precedence over the one we add. - if (options.traceLifecycle === 'stream' && !options.integrations.some(i => i.name === 'SpanStreaming')) { + if ( + options.traceLifecycle !== 'static' && + !options.integrations.some(i => i.name === SPAN_STREAMING_INTEGRATION_NAME) + ) { options.integrations.push(spanStreamingIntegration()); } diff --git a/packages/core/src/shared-exports.ts b/packages/core/src/shared-exports.ts index 4034ff73d37d..057d70a37e47 100644 --- a/packages/core/src/shared-exports.ts +++ b/packages/core/src/shared-exports.ts @@ -85,8 +85,11 @@ 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, + // oxlint-disable-next-line typescript/no-deprecated + withStreamedSpan, +} 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/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 06004521e417..b9eb44216fa5 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -46,7 +46,7 @@ import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { logSpanEnd } from './logSpans'; import { timedEventsToMeasurements } from './measurement'; import { getSegmentSpanCaptureStrategy, type SegmentSpanCaptureConvertOptions } from './segmentSpanCaptureStrategy'; -import { isStreamedBeforeSendSpanCallback } from './spans/beforeSendSpan'; +import { isStaticBeforeSendSpanCallback } from './spans/beforeSendSpan'; import { captureSpan, captureStandaloneSpanWithStaticCallback } from './spans/captureSpan'; import { createStreamedSpanEnvelope } from './spans/envelope'; import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled'; @@ -557,13 +557,14 @@ function isStandaloneSpan(span: Span): boolean { * TODO(standalone): remove once the static (transaction) trace lifecycle is dropped. */ function sendStandaloneSpan(span: SentrySpan, client: Client): void { - const { beforeSendSpan } = client.getOptions(); + const { beforeSendSpan, traceLifecycle } = client.getOptions(); - // A user who opted out of span streaming writes `beforeSendSpan` in the v1 `SpanJSON` format. That - // callback never runs through `captureSpan` (which only honors streamed callbacks), so scrub the - // span in its native v1 shape and convert it forward to v2, mirroring the gen_ai extraction path. + // A user who opted out of span streaming wraps `beforeSendSpan` with `withStaticSpan` and writes it in + // the v1 `SpanJSON` format. That callback never runs through `captureSpan` (which only honors streamed + // callbacks), so scrub the span in its native v1 shape and convert it forward to v2, mirroring the + // gen_ai extraction path. // TODO(standalone): remove this branch once the static trace lifecycle is dropped. - if (beforeSendSpan && !isStreamedBeforeSendSpanCallback(beforeSendSpan)) { + if (traceLifecycle === 'static' && isStaticBeforeSendSpanCallback(beforeSendSpan)) { const serializedSpan = captureStandaloneSpanWithStaticCallback(span, client, beforeSendSpan); const dsc = getDynamicSamplingContextFromSpan(span); // sendEnvelope should not throw diff --git a/packages/core/src/tracing/spans/beforeSendSpan.ts b/packages/core/src/tracing/spans/beforeSendSpan.ts index 57797e5daead..65b4703fc4cb 100644 --- a/packages/core/src/tracing/spans/beforeSendSpan.ts +++ b/packages/core/src/tracing/spans/beforeSendSpan.ts @@ -1,42 +1,55 @@ -import type { CoreOptions } from '../../types/options'; -import type { BeforeSendStreamedSpanCallback } from '../../types/options'; +import type { BeforeSendStaticSpanCallback, BeforeSendStreamedSpanCallback } from '../../types/options'; import type { 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 static, transaction-based 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 a static span instead of a {@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 - A {@link BeforeSendStaticSpanCallback} that receives and returns a static span. + * @returns A callback that is compatible with the `beforeSendSpan` option when using `traceLifecycle: 'static'`. + */ +export function withStaticSpan(callback: BeforeSendStaticSpanCallback): BeforeSendStreamedSpanCallback { + addNonEnumerableProperty(callback, '_static', true); + // `beforeSendSpan` is typed as a single streamed callback so that an unwrapped callback's parameter + // is contextually typed as `StreamedSpanJSON`. The `_static` marker tells the SDK to hand this + // callback a `SpanJSON` instead, so the declared parameter type is deliberately not what it receives. + return callback as unknown as BeforeSendStreamedSpanCallback; +} + +/** + * A wrapper to explicitly use the streamed span format in your `beforeSendSpan` callback. + * + * @deprecated `beforeSendSpan` callbacks receive {@link StreamedSpanJSON} by default. + * This function returns the callback unchanged and 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 } { - addNonEnumerableProperty(callback, '_streamed', true); - return callback as unknown as StaticBeforeSendSpanCallback & { _streamed: true }; +): BeforeSendStreamedSpanCallback { + return callback; } /** - * Typesafe check to identify if a `beforeSendSpan` callback expects the streamed span JSON format. + * 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 withStreamedSpan}. + * @returns `true` if 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; +export function isStaticBeforeSendSpanCallback(callback: unknown): callback is BeforeSendStaticSpanCallback { + return !!callback && typeof callback === 'function' && '_static' in callback && !!callback._static; } diff --git a/packages/core/src/tracing/spans/captureSpan.ts b/packages/core/src/tracing/spans/captureSpan.ts index 39ad44585c5f..16aee9c8bb65 100644 --- a/packages/core/src/tracing/spans/captureSpan.ts +++ b/packages/core/src/tracing/spans/captureSpan.ts @@ -21,7 +21,7 @@ import { streamedSpanJsonToSerializedSpan, } from '../../utils/spanUtils'; import { getCapturedScopesOnSpan } from '../utils'; -import { isStreamedBeforeSendSpanCallback } from './beforeSendSpan'; +import { isStaticBeforeSendSpanCallback } from './beforeSendSpan'; import { spanJsonToSerializedStreamedSpan } from './spanJsonToStreamedSpan'; import { scopeContextsToSpanAttributes } from './scopeContextAttributes'; import { DEFAULT_ENVIRONMENT } from '../../constants'; @@ -75,9 +75,12 @@ export function captureSpan(span: Span, client: Client): SerializedStreamedSpanW // This also invokes the `processSpan` hook of all integrations client.emit('processSpan', spanJSON); - const { beforeSendSpan } = client.getOptions(); + const { beforeSendSpan, traceLifecycle } = client.getOptions(); const processedSpan = - beforeSendSpan && isStreamedBeforeSendSpanCallback(beforeSendSpan) + // check for traceLifecycle here because in static lifecycle, + // captureSpan is called for INP spans. If an unmigrated beforeSendSpan + // callback is run on these spans, it will throw an error. + traceLifecycle !== 'static' && beforeSendSpan && !isStaticBeforeSendSpanCallback(beforeSendSpan) ? applyBeforeSendSpanCallback(spanJSON, beforeSendSpan) : spanJSON; diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index 1e753620b2a9..aafa01cb500e 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -615,16 +615,19 @@ export interface ClientOptions PromiseLike | ErrorEvent | null; /** - * This function can be defined to modify a child span before it's sent. + * This function can be defined to modify a span before it's sent. * - * When using `traceLifecycle: 'stream'`, wrap your callback with {@link withStreamedSpan} - * to receive and return {@link StreamedSpanJSON} instead. + * The callback receives and returns a {@link StreamedSpanJSON}, matching the default + * `traceLifecycle: 'stream'`. When using `traceLifecycle: 'static'`, wrap your callback with + * {@link withStaticSpan} to receive and return a {@link SpanJSON} instead. + * + * A callback that doesn't match the configured `traceLifecycle` is never invoked. * * @param span The span generated by the SDK. * * @returns The modified span payload that will be sent. */ - beforeSendSpan?: ((span: SpanJSON) => SpanJSON) & { _streamed?: true }; + beforeSendSpan?: BeforeSendStreamedSpanCallback; /** * An event-processing callback for transaction events, guaranteed to be invoked after all other event @@ -669,13 +672,16 @@ 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`. - */ - _streamed?: true; -}; +export type BeforeSendStreamedSpanCallback = (span: StreamedSpanJSON) => StreamedSpanJSON; + +/** + * A callback for processing spans sent as part of transaction events. + * + * Pass this to {@link withStaticSpan} rather than to `beforeSendSpan` directly. + * + * @see {@link SpanJSON} for the static span format used with `traceLifecycle: 'static'` + */ +export type BeforeSendStaticSpanCallback = (span: SpanJSON) => SpanJSON; /** Base configuration options for every SDK. */ export interface CoreOptions extends Omit< diff --git a/packages/core/test/integrations/spanStreaming.test.ts b/packages/core/test/integrations/spanStreaming.test.ts index 8b2badf575ff..3219fb4e14d4 100644 --- a/packages/core/test/integrations/spanStreaming.test.ts +++ b/packages/core/test/integrations/spanStreaming.test.ts @@ -34,57 +34,54 @@ describe('spanStreamingIntegration (core)', () => { expect(integration.setup).toBeDefined(); }); - it.each(['static', 'somethingElse'])( - 'logs a warning if traceLifecycle is not set to "stream" but to %s', - traceLifecycle => { - const debugSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); - const client = new TestClient({ - ...getDefaultTestClientOptions(), - dsn: 'https://username@domain/123', - integrations: [spanStreamingIntegration()], - // @ts-expect-error - we want to test the warning for invalid traceLifecycle values - traceLifecycle, - }); - - 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(() => {}); + it('does not set up span streaming if traceLifecycle is "static"', () => { + const debugSpy = vi.spyOn(debug, 'log').mockImplementation(() => {}); const client = new TestClient({ ...getDefaultTestClientOptions(), dsn: 'https://username@domain/123', integrations: [spanStreamingIntegration()], - traceLifecycle: 'stream', - beforeSendSpan: (span: SentryCore.SpanJSON) => span, + traceLifecycle: 'static', + tracesSampleRate: 1, }); SentryCore.setCurrentClient(client); client.init(); - expect(debugSpy).toHaveBeenCalledWith( - 'SpanStreaming integration requires a beforeSendSpan callback using `withStreamedSpan`! Falling back to static trace lifecycle.', - ); + expect(debugSpy).toHaveBeenCalledWith('[SpanStreaming] `traceLifecycle` is "static", skipping setup.'); debugSpy.mockRestore(); - expect(client.getOptions().traceLifecycle).toBe('static'); + expect(MockSpanBuffer).not.toHaveBeenCalled(); + + // Without the hooks registered, ending a span must not enqueue anything + client.emit('afterSpanEnd', new SentryCore.SentrySpan({ name: 'test', sampled: true })); + expect(mockSpanBufferInstance.add).not.toHaveBeenCalled(); + }); + + it.each([ + ['explicitly set to "stream"', 'stream' as const], + ['left unset', undefined], + ])('sets up span streaming if traceLifecycle is %s', (_, traceLifecycle) => { + const client = new TestClient({ + ...getDefaultTestClientOptions(), + dsn: 'https://username@domain/123', + integrations: [spanStreamingIntegration()], + traceLifecycle, + }); + + SentryCore.setCurrentClient(client); + client.init(); + + expect(MockSpanBuffer).toHaveBeenCalledWith(client); + expect(client.getOptions().traceLifecycle).toBe('stream'); }); - it('sets up buffer when traceLifecycle is "stream"', () => { + it('still sets up span streaming if beforeSendSpan is wrapped with withStaticSpan', () => { const client = new TestClient({ ...getDefaultTestClientOptions(), dsn: 'https://username@domain/123', integrations: [spanStreamingIntegration()], traceLifecycle: 'stream', + beforeSendSpan: SentryCore.withStaticSpan(span => span), }); SentryCore.setCurrentClient(client); diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index d30314073bcf..fc34bb12f045 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'; @@ -94,6 +95,80 @@ describe('Client', () => { expect(consoleWarnSpy).toHaveBeenCalledTimes(0); consoleWarnSpy.mockRestore(); }); + + test('warns that a streamed beforeSendSpan is ignored with traceLifecycle "static"', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + new TestClient( + getDefaultTestClientOptions({ dsn: PUBLIC_DSN, traceLifecycle: 'static', beforeSendSpan: span => span }), + ); + + expect(warnSpy).toHaveBeenCalledWith( + 'Ignoring `beforeSendSpan`: wrap it with `Sentry.withStaticSpan` to use it with `traceLifecycle: "static"`.', + ); + warnSpy.mockRestore(); + }); + + test('warns that a static beforeSendSpan is ignored with traceLifecycle "stream"', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + new TestClient( + getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + traceLifecycle: 'stream', + beforeSendSpan: withStaticSpan(span => span), + }), + ); + + expect(warnSpy).toHaveBeenCalledWith( + 'Ignoring `beforeSendSpan`: remove `Sentry.withStaticSpan` to use it with `traceLifecycle: "stream"`.', + ); + warnSpy.mockRestore(); + }); + + test('reports the normalized traceLifecycle when warning about an unknown value', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + new TestClient( + getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + // @ts-expect-error - we want to test normalization of invalid traceLifecycle values + traceLifecycle: 'somethingElse', + beforeSendSpan: withStaticSpan(span => span), + }), + ); + + expect(warnSpy).toHaveBeenCalledWith( + 'Ignoring `beforeSendSpan`: remove `Sentry.withStaticSpan` to use it with `traceLifecycle: "stream"`.', + ); + warnSpy.mockRestore(); + }); + + test('does not warn for a streamed beforeSendSpan with traceLifecycle "stream"', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + new TestClient( + getDefaultTestClientOptions({ dsn: PUBLIC_DSN, traceLifecycle: 'stream', beforeSendSpan: span => span }), + ); + + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); + + test('does not warn for a static beforeSendSpan with traceLifecycle "static"', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + new TestClient( + getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + traceLifecycle: 'static', + beforeSendSpan: withStaticSpan(span => span), + }), + ); + + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); }); describe('init() / transaction option warnings', () => { @@ -211,6 +286,15 @@ describe('Client', () => { expect(client.getOptions().traceLifecycle).toBe('static'); }); + + test('normalizes an unknown traceLifecycle to stream', () => { + const client = new TestClient( + // @ts-expect-error - we want to test normalization of invalid traceLifecycle values + getDefaultTestClientOptions({ traceLifecycle: 'somethingElse' }), + ); + + expect(client.getOptions().traceLifecycle).toBe('stream'); + }); }); describe('getTransport()', () => { @@ -1180,7 +1264,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); @@ -1399,14 +1483,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); @@ -1514,10 +1600,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); @@ -1596,7 +1684,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/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index 7f509cc23c0e..40f7a1344f9c 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -14,6 +14,7 @@ import { markSpanForOtelSourceInference, spanSourceWasExplicitlySet, } from '../../../src/tracing/utils'; +import { withStaticSpan } from '../../../src/tracing/spans/beforeSendSpan'; import type { Envelope } from '../../../src/types/envelope'; import type { SpanJSON } from '../../../src/types/span'; import { spanToJSON, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; @@ -272,7 +273,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', @@ -295,11 +296,11 @@ describe('SentrySpan', () => { expect(mockSend).toHaveBeenCalled(); }); - test('runs a non-streamed `beforeSendSpan` for standalone spans', () => { + test('runs a static `beforeSendSpan` for standalone spans', () => { // A standalone span is sent as a v2 streamed span, but a user opting out of span streaming still // writes `beforeSendSpan` in the v1 `SpanJSON` format. We scrub the span in its v1 shape before // converting it forward to v2, so the callback runs and its changes are applied. - const beforeSendSpan = vi.fn((span: SpanJSON) => ({ ...span, description: 'scrubbed' })); + const beforeSendSpan = withStaticSpan(vi.fn((span: SpanJSON) => ({ ...span, description: 'scrubbed' }))); const client = new TestClient( getDefaultTestClientOptions({ dsn: 'https://username@domain/123', @@ -334,14 +335,16 @@ describe('SentrySpan', () => { expect(spanItem[1].items[0]!.name).toBe('scrubbed'); }); - test('does not apply scope attributes to standalone spans with a non-streamed `beforeSendSpan`', () => { + test('does not apply scope attributes to standalone spans with a static `beforeSendSpan`', () => { // Scope attributes can hold `{ unit, value }` objects, unexpected for a static callback, so they // are not applied to the standalone (INP) span, just as they are not applied to transactions. const seen: SpanJSON['data'][] = []; - const beforeSendSpan = vi.fn((span: SpanJSON) => { - seen.push({ ...span.data }); - return span; - }); + const beforeSendSpan = withStaticSpan( + vi.fn((span: SpanJSON) => { + seen.push({ ...span.data }); + return span; + }), + ); 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..acab09bfea06 100644 --- a/packages/core/test/lib/tracing/spans/beforeSendSpan.test.ts +++ b/packages/core/test/lib/tracing/spans/beforeSendSpan.test.ts @@ -1,26 +1,40 @@ 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 } from '../../../../src/tracing/spans/beforeSendSpan'; -describe('beforeSendSpan for span streaming', () => { - describe('withStreamedSpan', () => { - it('should be able to modify the span', () => { +describe('beforeSendSpan callback formats', () => { + describe('withStaticSpan', () => { + it('marks the callback as static without making the marker enumerable', () => { const beforeSendSpan = vi.fn(); - const wrapped = withStreamedSpan(beforeSendSpan); - expect(wrapped._streamed).toBe(true); + const wrapped = withStaticSpan(beforeSendSpan); + + expect(wrapped._static).toBe(true); + expect(Object.keys(wrapped)).not.toContain('_static'); }); }); - describe('isStreamedBeforeSendSpanCallback', () => { - it('returns true if the callback is wrapped with withStreamedSpan', () => { + describe('withStreamedSpan', () => { + it('returns the callback unchanged', () => { const beforeSendSpan = vi.fn(); - const wrapped = withStreamedSpan(beforeSendSpan); - expect(isStreamedBeforeSendSpanCallback(wrapped)).toBe(true); + + expect(withStreamedSpan(beforeSendSpan)).toBe(beforeSendSpan); + expect(Object.keys(beforeSendSpan)).not.toContain('_streamed'); }); + }); - it('returns false if the callback is not wrapped with withStreamedSpan', () => { - const beforeSendSpan = vi.fn(); - expect(isStreamedBeforeSendSpanCallback(beforeSendSpan)).toBe(false); + 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); + }); + + it('returns false if the callback is wrapped with withStreamedSpan', () => { + expect(isStaticBeforeSendSpanCallback(withStreamedSpan(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 3f40374908e2..90c4c8c8d1ea 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'; @@ -471,8 +471,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 a default beforeSendSpan callback', () => { + const beforeSendSpan = vi.fn(span => span); const client = new TestClient( getDefaultTestClientOptions({ @@ -480,6 +480,7 @@ describe('captureSpan', () => { tracesSampleRate: 1, release: '1.0.0', environment: 'staging', + traceLifecycle: 'stream', beforeSendSpan, }), ); @@ -492,8 +493,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({ @@ -515,8 +516,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({ @@ -524,6 +524,7 @@ describe('captureSpan', () => { tracesSampleRate: 1, release: '1.0.0', environment: 'staging', + traceLifecycle: 'stream', beforeSendSpan, }), ); diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 65a708042822..6ab8a5a7df80 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -97,6 +97,8 @@ export { wrapMcpServerWithSentry, featureFlagsIntegration, metrics, + withStaticSpan, + // oxlint-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..bc77a68bd35d 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; +// oxlint-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 e6aba2167d7f..c66def738a3c 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -154,6 +154,8 @@ export { unleashIntegration, metrics, spanStreamingIntegration, + withStaticSpan, + // oxlint-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 3afeb1b9dc7d..fb2fe70a528d 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -159,6 +159,8 @@ export { unleashIntegration, metrics, spanStreamingIntegration, + withStaticSpan, + // oxlint-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..42ef9d6a9502 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; +// oxlint-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 997986bbb3a5..def0126bb115 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -176,7 +176,12 @@ export type { CaptureContext, } from '@sentry/core'; -export { metrics, withStreamedSpan } from '@sentry/core'; +export { + metrics, + withStaticSpan, + // oxlint-disable-next-line typescript/no-deprecated + 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..4865aac868d3 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; +// oxlint-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..af8c4da6d94a 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; +// oxlint-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..99c642506693 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -117,6 +117,8 @@ export { spanToTraceHeader, spanToBaggageHeader, updateSpanName, + withStaticSpan, + // oxlint-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..498b640d7e6e 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; +// oxlint-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 a1555930bad4..304e3c69ac29 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -132,6 +132,8 @@ export { createConsolaReporter, createSentryWinstonTransport, spanStreamingIntegration, + withStaticSpan, + // oxlint-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..71e45f1d2a2d 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; +// oxlint-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 6fa2a75fcfdc..1bfb72c8f444 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -131,6 +131,8 @@ export { createConsolaReporter, createSentryWinstonTransport, spanStreamingIntegration, + withStaticSpan, + // oxlint-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..5404d001b205 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; +// oxlint-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 d72e610185a3..9d3f170926f7 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -136,6 +136,8 @@ export { vercelAIIntegration, metrics, spanStreamingIntegration, + withStaticSpan, + // oxlint-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..65fe98529a5c 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -83,6 +83,8 @@ export { withIsolationScope, withMonitor, withScope, + withStaticSpan, + // oxlint-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..d1464a88175e 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; +// oxlint-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 c67b84ed1ff7..56705a2d936c 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -99,6 +99,8 @@ export { featureFlagsIntegration, logger, metrics, + withStaticSpan, + // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, spanStreamingIntegration, } from '@sentry/core';