diff --git a/.size-limit.js b/.size-limit.js index d589cfccdc65..6ef49dad2033 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -286,7 +286,7 @@ module.exports = [ path: createCDNPath('bundle.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '100 KB', + limit: '101 KB', disablePlugins: ['@size-limit/esbuild'], }, { diff --git a/MIGRATION.md b/MIGRATION.md index 438b2572f682..4afed9dee58b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -120,15 +120,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/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 cb157ec9007e..f503915569af 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -173,6 +173,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 30f597757b40..991ef239696a 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -160,6 +160,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 4a6c25eed27d..353d16545358 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -178,6 +178,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 a95da03e25ec..46e8effb6c34 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -110,6 +110,8 @@ export { growthbookIntegration, logger, metrics, + withStaticSpan, + // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, spanStreamingIntegration, instrumentStateGraph, diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 6076b065cf62..9224ba76ccfd 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'; @@ -233,7 +233,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 = {}; @@ -249,6 +255,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, @@ -1654,7 +1678,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/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 700e65d2a974..daec7c8a6a88 100644 --- a/packages/core/src/shared-exports.ts +++ b/packages/core/src/shared-exports.ts @@ -86,8 +86,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/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 b65f85b02d46..7bce050af7ff 100644 --- a/packages/core/src/tracing/spans/captureSpan.ts +++ b/packages/core/src/tracing/spans/captureSpan.ts @@ -20,7 +20,7 @@ import { streamedSpanJsonToSerializedSpan, } from '../../utils/spanUtils'; import { getCapturedScopesOnSpan } from '../utils'; -import { isStreamedBeforeSendSpanCallback } from './beforeSendSpan'; +import { isStaticBeforeSendSpanCallback } from './beforeSendSpan'; import { scopeContextsToSpanAttributes } from './scopeContextAttributes'; import { DEFAULT_ENVIRONMENT } from '../../constants'; import { @@ -75,7 +75,7 @@ export function captureSpan(span: Span, client: Client): SerializedStreamedSpanW const { beforeSendSpan } = client.getOptions(); const processedSpan = - beforeSendSpan && isStreamedBeforeSendSpanCallback(beforeSendSpan) + beforeSendSpan && !isStaticBeforeSendSpanCallback(beforeSendSpan) ? applyBeforeSendSpanCallback(spanJSON, beforeSendSpan) : spanJSON; diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index e5f938082509..a3deafe5b2f8 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -628,16 +628,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 @@ -674,13 +677,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 5b0cb752f2c2..3ecca5aee98a 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('getOptions()', () => { @@ -124,6 +199,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()', () => { @@ -1093,7 +1177,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); @@ -1312,14 +1396,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); @@ -1427,10 +1513,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); @@ -1509,7 +1597,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 7522b7061234..853ef21e8acb 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'; @@ -238,128 +239,130 @@ describe('SentrySpan', () => { expect(spanToJSON(span).timestamp).toBe(endTime / 1000); }); - test('uses sampled config for standalone span', () => { - const client = new TestClient( - getDefaultTestClientOptions({ - dsn: 'https://username@domain/123', - enableSend: true, - }), - ); - setCurrentClient(client); - - // @ts-expect-error Accessing private transport API - const mockSend = vi.spyOn(client._transport, 'send'); - - const notSampledSpan = new SentrySpan({ - name: 'not-sampled', - isStandalone: true, - startTimestamp: 1, - endTimestamp: 2, - sampled: false, + describe('standalone spans', () => { + test('uses sampled config for standalone span', () => { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + enableSend: true, + }), + ); + setCurrentClient(client); + + // @ts-expect-error Accessing private transport API + const mockSend = vi.spyOn(client._transport, 'send'); + + const notSampledSpan = new SentrySpan({ + name: 'not-sampled', + isStandalone: true, + startTimestamp: 1, + endTimestamp: 2, + sampled: false, + }); + notSampledSpan.end(); + expect(mockSend).not.toHaveBeenCalled(); + + const sampledSpan = new SentrySpan({ + name: 'is-sampled', + isStandalone: true, + startTimestamp: 1, + endTimestamp: 2, + sampled: true, + }); + sampledSpan.end(); + expect(mockSend).toHaveBeenCalledTimes(1); }); - notSampledSpan.end(); - expect(mockSend).not.toHaveBeenCalled(); - const sampledSpan = new SentrySpan({ - name: 'is-sampled', - isStandalone: true, - startTimestamp: 1, - endTimestamp: 2, - sampled: true, + test('sends the span if `beforeSendSpan` does not modify the span', () => { + const beforeSendSpan = withStaticSpan(vi.fn(span => span)); + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + enableSend: true, + beforeSendSpan, + }), + ); + setCurrentClient(client); + + // @ts-expect-error Accessing private transport API + const mockSend = vi.spyOn(client._transport, 'send'); + const span = new SentrySpan({ + name: 'test', + isStandalone: true, + startTimestamp: 1, + endTimestamp: 2, + sampled: true, + }); + span.end(); + expect(mockSend).toHaveBeenCalled(); }); - sampledSpan.end(); - expect(mockSend).toHaveBeenCalledTimes(1); - }); - test('sends the span if `beforeSendSpan` does not modify the span', () => { - const beforeSendSpan = vi.fn(span => span); - const client = new TestClient( - getDefaultTestClientOptions({ - dsn: 'https://username@domain/123', - enableSend: true, - beforeSendSpan, - }), - ); - setCurrentClient(client); - - // @ts-expect-error Accessing private transport API - const mockSend = vi.spyOn(client._transport, 'send'); - const span = new SentrySpan({ - name: 'test', - isStandalone: true, - startTimestamp: 1, - endTimestamp: 2, - sampled: true, + test('ignores a static `beforeSendSpan` for standalone spans', () => { + // Standalone spans are sent as v2 streamed spans, which only honor an unwrapped (streamed) + // `beforeSendSpan`. A callback wrapped with `withStaticSpan` is ignored, so the span is sent unmodified. + const beforeSendSpan = withStaticSpan(vi.fn(() => null as unknown as SpanJSON)); + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + enableSend: true, + beforeSendSpan, + }), + ); + setCurrentClient(client); + + const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent'); + // @ts-expect-error Accessing private transport API + const mockSend = vi.spyOn(client._transport, 'send'); + const span = new SentrySpan({ + name: 'test', + isStandalone: true, + startTimestamp: 1, + endTimestamp: 2, + sampled: true, + }); + span.end(); + + expect(beforeSendSpan).not.toHaveBeenCalled(); + expect(mockSend).toHaveBeenCalled(); + expect(recordDroppedEventSpy).not.toHaveBeenCalled(); }); - span.end(); - expect(mockSend).toHaveBeenCalled(); - }); - test('ignores a non-streamed `beforeSendSpan` for standalone spans', () => { - // Standalone spans are sent as v2 streamed spans, which only honor a `beforeSendSpan` wrapped - // with `withStreamedSpan`. A plain callback is ignored, so the span is sent unmodified. - const beforeSendSpan = vi.fn(() => null as unknown as SpanJSON); - const client = new TestClient( - getDefaultTestClientOptions({ - dsn: 'https://username@domain/123', - enableSend: true, - beforeSendSpan, - }), - ); - setCurrentClient(client); - - const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent'); - // @ts-expect-error Accessing private transport API - const mockSend = vi.spyOn(client._transport, 'send'); - const span = new SentrySpan({ - name: 'test', - isStandalone: true, - startTimestamp: 1, - endTimestamp: 2, - sampled: true, - }); - span.end(); - - expect(beforeSendSpan).not.toHaveBeenCalled(); - expect(mockSend).toHaveBeenCalled(); - expect(recordDroppedEventSpy).not.toHaveBeenCalled(); - }); - - test('sends a standalone span on its own and excludes it from the parent transaction', async () => { - const client = new TestClient( - getDefaultTestClientOptions({ - dsn: 'https://username@domain/123', - enableSend: true, - tracesSampleRate: 1, - }), - ); - setCurrentClient(client); - - const envelopes: Envelope[] = []; - client.on('beforeEnvelope', envelope => { - envelopes.push(envelope); + test('sends a standalone span on its own and excludes it from the parent transaction', async () => { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + enableSend: true, + tracesSampleRate: 1, + }), + ); + setCurrentClient(client); + + const envelopes: Envelope[] = []; + client.on('beforeEnvelope', envelope => { + envelopes.push(envelope); + }); + + startSpan({ name: 'root' }, () => { + const standaloneChild = startInactiveSpan({ name: 'inp', experimental: { standalone: true } }); + standaloneChild.end(); + }); + + await client.flush(); + + const items = envelopes.flatMap( + envelope => envelope[1] as Array<[{ type?: string; content_type?: string }, any]>, + ); + + // The standalone child is sent on its own as a v2 streamed span... + const streamedItem = items.find(item => item[0]?.content_type === 'application/vnd.sentry.items.span.v2+json'); + expect(streamedItem?.[1]?.items?.[0]?.name).toBe('inp'); + + // ...and is NOT folded into the root transaction (no double-send). + const transactionItem = items.find(item => item[0]?.type === 'transaction'); + expect(transactionItem?.[1]?.spans?.map((span: { description?: string }) => span.description)).not.toContain( + 'inp', + ); }); - - startSpan({ name: 'root' }, () => { - const standaloneChild = startInactiveSpan({ name: 'inp', experimental: { standalone: true } }); - standaloneChild.end(); - }); - - await client.flush(); - - const items = envelopes.flatMap( - envelope => envelope[1] as Array<[{ type?: string; content_type?: string }, any]>, - ); - - // The standalone child is sent on its own as a v2 streamed span... - const streamedItem = items.find(item => item[0]?.content_type === 'application/vnd.sentry.items.span.v2+json'); - expect(streamedItem?.[1]?.items?.[0]?.name).toBe('inp'); - - // ...and is NOT folded into the root transaction (no double-send). - const transactionItem = items.find(item => item[0]?.type === 'transaction'); - expect(transactionItem?.[1]?.spans?.map((span: { description?: string }) => span.description)).not.toContain( - 'inp', - ); }); test('build TransactionEvent for basic root span', () => { 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 0560ebd2f702..88856b93feb4 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 a default 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 52a04b18aeb7..4ff878591009 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 715c2484296e..74257f226a90 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -155,6 +155,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 d07490563212..191c79a6756b 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, + // 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 bf8dd5d7533e..bdc31ef809ab 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -174,7 +174,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 4fd2331b34fa..88350f5e593d 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -133,6 +133,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 fd5fbb2e0e39..dc8640b965e8 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/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/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 5ebd934e9e88..07a8147cefe8 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -137,6 +137,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 d8e277f9fea0..29831c013835 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -105,6 +105,8 @@ export { featureFlagsIntegration, logger, metrics, + withStaticSpan, + // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, spanStreamingIntegration, } from '@sentry/core';