From 8486e3266b47b252c9bd6ad19a136cfe5d439da3 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 18 May 2026 18:03:27 +0900 Subject: [PATCH 01/25] feat(opentelemetry): Add SentryTraceProvider --- packages/core/src/tracing/idleSpan.ts | 7 +- .../src/tracing/sentryNonRecordingSpan.ts | 9 +- packages/core/src/tracing/sentrySpan.ts | 15 +- packages/core/src/tracing/trace.ts | 54 ++- packages/core/src/types/span.ts | 8 +- packages/core/src/utils/spanUtils.ts | 19 +- .../core/test/lib/tracing/idleSpan.test.ts | 5 +- packages/opentelemetry/README.md | 33 ++ .../opentelemetry/src/asyncContextStrategy.ts | 99 ++++- packages/opentelemetry/src/custom/client.ts | 5 +- packages/opentelemetry/src/exports.ts | 2 + .../opentelemetry/src/sentryTraceProvider.ts | 379 ++++++++++++++++++ packages/opentelemetry/src/types.ts | 9 +- .../opentelemetry/src/utils/setupCheck.ts | 7 +- 14 files changed, 614 insertions(+), 37 deletions(-) create mode 100644 packages/opentelemetry/src/sentryTraceProvider.ts diff --git a/packages/core/src/tracing/idleSpan.ts b/packages/core/src/tracing/idleSpan.ts index 22ba70b81a65..f6f3bab6ecdd 100644 --- a/packages/core/src/tracing/idleSpan.ts +++ b/packages/core/src/tracing/idleSpan.ts @@ -6,6 +6,7 @@ import type { Span } from '../types/span'; import type { StartSpanOptions } from '../types/startSpanOptions'; import { debug } from '../utils/debug-logger'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; +import { dropUndefinedKeys } from '../utils/object'; import { shouldIgnoreSpan } from '../utils/should-ignore-span'; import { _setSpanForScope } from '../utils/spanOnScope'; import { @@ -124,11 +125,7 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti if (!client || !hasSpansEnabled()) { const span = new SentryNonRecordingSpan(); - const dsc = { - sample_rate: '0', - sampled: 'false', - ...getDynamicSamplingContextFromSpan(span), - } satisfies Partial; + const dsc = dropUndefinedKeys(getDynamicSamplingContextFromSpan(span)) satisfies Partial; freezeDscOnSpan(span, dsc); return span; diff --git a/packages/core/src/tracing/sentryNonRecordingSpan.ts b/packages/core/src/tracing/sentryNonRecordingSpan.ts index 4c4c1064eedb..e009d53a2ce1 100644 --- a/packages/core/src/tracing/sentryNonRecordingSpan.ts +++ b/packages/core/src/tracing/sentryNonRecordingSpan.ts @@ -9,7 +9,7 @@ import type { } from '../types/span'; import type { SpanStatus } from '../types/spanStatus'; import { generateSpanId, generateTraceId } from '../utils/propagationContext'; -import { TRACE_FLAG_NONE } from '../utils/spanUtils'; +import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../utils/spanUtils'; interface SentryNonRecordingSpanArguments extends SentrySpanArguments { dropReason?: EventDropReason; @@ -21,6 +21,7 @@ interface SentryNonRecordingSpanArguments extends SentrySpanArguments { export class SentryNonRecordingSpan implements Span { private _traceId: string; private _spanId: string; + private _sampled: boolean | undefined; /** * Reason why this span was dropped, if applicable ('ignored' or 'sample_rate'). @@ -32,6 +33,7 @@ export class SentryNonRecordingSpan implements Span { public constructor(spanContext: SentryNonRecordingSpanArguments = {}) { this._traceId = spanContext.traceId || generateTraceId(); this._spanId = spanContext.spanId || generateSpanId(); + this._sampled = spanContext.sampled; this.dropReason = spanContext.dropReason; } @@ -40,7 +42,8 @@ export class SentryNonRecordingSpan implements Span { return { spanId: this._spanId, traceId: this._traceId, - traceFlags: TRACE_FLAG_NONE, + traceFlags: this._sampled ? TRACE_FLAG_SAMPLED : TRACE_FLAG_NONE, + sampled: this._sampled, }; } @@ -98,7 +101,7 @@ export class SentryNonRecordingSpan implements Span { * @hidden * @internal */ - public recordException(_exception: unknown, _time?: number | undefined): void { + public recordException(_exception: unknown, _time?: SpanTimeInput | undefined): void { // noop } } diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 84c78e73356d..9271c5c855f8 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -143,8 +143,19 @@ export class SentrySpan implements Span { * @hidden * @internal */ - public recordException(_exception: unknown, _time?: number | undefined): void { - // noop + public recordException(exception: unknown, time?: SpanTimeInput | undefined): void { + const attributes: SpanAttributes = {}; + + if (typeof exception === 'string') { + attributes['exception.message'] = exception; + } else if (exception && typeof exception === 'object') { + const error = exception as { name?: string; message?: string; stack?: string }; + attributes['exception.type'] = error.name; + attributes['exception.message'] = error.message; + attributes['exception.stacktrace'] = error.stack; + } + + this.addEvent('exception', attributes, time); } /** @inheritdoc */ diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 3134c58309b1..94234007f99c 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -22,10 +22,18 @@ import { hasSpansEnabled } from '../utils/hasSpansEnabled'; import { shouldIgnoreSpan } from '../utils/should-ignore-span'; import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled'; import { parseSampleRate } from '../utils/parseSampleRate'; +import { dropUndefinedKeys } from '../utils/object'; import { generateTraceId } from '../utils/propagationContext'; import { safeMathRandom } from '../utils/randomSafeContext'; import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope'; -import { addChildSpanToSpan, getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; +import { + addChildSpanToSpan, + getRootSpan, + spanIsSampled, + spanTimeInputToSeconds, + spanToJSON, + spanToTraceSamplingDecision, +} from '../utils/spanUtils'; import { propagationContextFromHeaders, shouldContinueTrace } from '../utils/tracing'; import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { logSpanStart } from './logSpans'; @@ -344,18 +352,45 @@ function createChildOrRootSpan({ forceTransaction?: boolean; scope: Scope; }): Span { + const isolationScope = getIsolationScope(); + if (!hasSpansEnabled()) { - const span = new SentryNonRecordingSpan(); + let propagationContext: { + traceId: string; + parentSpanId?: string | undefined; + sampled?: boolean | undefined; + dsc?: Partial | undefined; + }; + + if (parentSpan) { + const parentSpanContext = parentSpan.spanContext(); + + propagationContext = { + traceId: parentSpanContext.traceId, + parentSpanId: parentSpanContext.spanId, + sampled: spanToTraceSamplingDecision(parentSpan), + dsc: undefined, + }; + } else { + propagationContext = { + ...isolationScope.getPropagationContext(), + ...scope.getPropagationContext(), + }; + } + + const span = new SentryNonRecordingSpan({ + traceId: propagationContext.traceId, + parentSpanId: propagationContext.parentSpanId, + sampled: propagationContext.sampled, + }); // If this is a root span, we ensure to freeze a DSC // So we can have at least partial data here if (forceTransaction || !parentSpan) { - const dsc = { - sampled: 'false', - sample_rate: '0', + const dsc = dropUndefinedKeys({ + ...(propagationContext.dsc || getDynamicSamplingContextFromSpan(span)), transaction: spanArguments.name, - ...getDynamicSamplingContextFromSpan(span), - } satisfies Partial; + }) satisfies Partial; freezeDscOnSpan(span, dsc); } @@ -373,11 +408,10 @@ function createChildOrRootSpan({ return new SentryNonRecordingSpan({ dropReason: 'ignored', traceId: parentSpan?.spanContext().traceId ?? scope.getPropagationContext().traceId, + sampled: false, }); } - const isolationScope = getIsolationScope(); - let span: Span; if (parentSpan && !forceTransaction) { span = _startChildSpan(parentSpan, scope, spanArguments); @@ -529,7 +563,7 @@ function _startChildSpan(parentSpan: Span, scope: Scope, spanArguments: SentrySp traceId, sampled, }) - : new SentryNonRecordingSpan({ traceId }); + : new SentryNonRecordingSpan({ traceId, sampled }); addChildSpanToSpan(parentSpan, childSpan); diff --git a/packages/core/src/types/span.ts b/packages/core/src/types/span.ts index 26dbbf9d29a4..31b61cb5dcc4 100644 --- a/packages/core/src/types/span.ts +++ b/packages/core/src/types/span.ts @@ -173,6 +173,12 @@ export interface SpanContextData { /** In OpenTelemetry, this can be used to store trace state, which are basically key-value pairs. */ traceState?: TraceState | undefined; + + /** + * Sentry-specific sampling decision for this span context. + * `undefined` means no local sampling decision was made yet. + */ + sampled?: boolean | undefined; } /** @@ -319,5 +325,5 @@ export interface Span { /** * NOT USED IN SENTRY, only added for compliance with OTEL Span interface */ - recordException(exception: unknown, time?: number): void; + recordException(exception: unknown, time?: SpanTimeInput): void; } diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 9f495ef7b30e..be24d888d65e 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -83,7 +83,7 @@ export function spanToTraceContext(span: Span): TraceContext { */ export function spanToTraceHeader(span: Span): string { const { traceId, spanId } = span.spanContext(); - const sampled = spanIsSampled(span); + const sampled = spanToTraceSamplingDecision(span); return generateSentryTraceHeader(traceId, spanId, sampled); } @@ -92,7 +92,7 @@ export function spanToTraceHeader(span: Span): string { */ export function spanToTraceparentHeader(span: Span): string { const { traceId, spanId } = span.spanContext(); - const sampled = spanIsSampled(span); + const sampled = spanToTraceSamplingDecision(span); return generateTraceparentHeader(traceId, spanId, sampled); } @@ -314,6 +314,21 @@ export function spanIsSampled(span: Span): boolean { return traceFlags === TRACE_FLAG_SAMPLED; } +/** + * Returns the sampling decision to propagate for trace headers. + * This intentionally differs from `spanIsSampled`: non-recording spans can + * represent either "sampled false" or "no decision yet" in TwP mode. + */ +export function spanToTraceSamplingDecision(span: Span): boolean | undefined { + const spanContext = span.spanContext(); + + if ('sampled' in spanContext) { + return spanContext.sampled; + } + + return spanIsSampled(span); +} + /** Get the status message to use for a JSON representation of a span. */ export function getStatusMessage(status: SpanStatus | undefined): string | undefined { if (!status || status.code === SPAN_STATUS_UNSET) { diff --git a/packages/core/test/lib/tracing/idleSpan.test.ts b/packages/core/test/lib/tracing/idleSpan.test.ts index 29aaa63c2bb0..ffa92939db26 100644 --- a/packages/core/test/lib/tracing/idleSpan.test.ts +++ b/packages/core/test/lib/tracing/idleSpan.test.ts @@ -62,12 +62,11 @@ describe('startIdleSpan', () => { const idleSpan = startIdleSpan({ name: 'foo' }); expect(idleSpan).toBeDefined(); expect(idleSpan).toBeInstanceOf(SentryNonRecordingSpan); - // DSC is still correctly set on the span + // DSC is still set on the span, but tracing-without-performance should + // preserve deferred sampling instead of freezing an explicit negative decision. expect(getDynamicSamplingContextFromSpan(idleSpan)).toEqual({ environment: 'production', public_key: '123', - sample_rate: '0', - sampled: 'false', trace_id: expect.stringMatching(/[a-f0-9]{32}/), }); diff --git a/packages/opentelemetry/README.md b/packages/opentelemetry/README.md index dd20135c268b..f64d3571fbc1 100644 --- a/packages/opentelemetry/README.md +++ b/packages/opentelemetry/README.md @@ -86,6 +86,39 @@ function setupSentry() { A full setup example can be found in [node-experimental](https://github.com/getsentry/sentry-javascript/blob/develop/packages/node-experimental). +## Experimental Sentry Trace Provider + +`SentryTraceProvider` is an experimental minimal OpenTelemetry tracer provider which creates native Sentry spans directly. +It is useful when code uses the global OpenTelemetry API and you do not need the full OpenTelemetry SDK span processor +and exporter pipeline. + +```js +import { trace } from '@opentelemetry/api'; +import { SentryTraceProvider } from '@sentry/opentelemetry'; + +trace.setGlobalTracerProvider(new SentryTraceProvider()); + +const span = trace.getTracer('example').startSpan('work'); +span.end(); +``` + +In `@sentry/node`, this provider can be enabled with the experimental option: + +```js +Sentry.init({ + dsn: 'xxx', + _experiments: { + useSentryTraceProvider: true, + }, +}); +``` + +This only captures spans from code that uses the global OpenTelemetry tracer provider. Spans created from a separate +custom provider instance still belong to that provider and should be sent to Sentry with `SentrySpanProcessor`. + +When this provider is enabled, additional OpenTelemetry span processors are ignored because Sentry spans are created +directly. OpenTelemetry logs and metrics are not handled by this provider. + ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) diff --git a/packages/opentelemetry/src/asyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts index 7cb8dc0f54eb..95cd41310204 100644 --- a/packages/opentelemetry/src/asyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -1,23 +1,38 @@ import * as api from '@opentelemetry/api'; -import type { Scope, withActiveSpan as defaultWithActiveSpan } from '@sentry/core'; -import { getDefaultCurrentScope, getDefaultIsolationScope, setAsyncContextStrategy } from '@sentry/core'; +import type { Scope, Span, withActiveSpan as defaultWithActiveSpan } from '@sentry/core'; +import { + _INTERNAL_safeMathRandom, + _INTERNAL_setSpanForScope, + baggageHeaderToDynamicSamplingContext, + getDefaultCurrentScope, + getDefaultIsolationScope, + setAsyncContextStrategy, +} from '@sentry/core'; import { SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY, SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, SENTRY_FORK_SET_SCOPE_CONTEXT_KEY, + SENTRY_TRACE_STATE_DSC, + SENTRY_TRACE_STATE_SAMPLE_RAND, } from './constants'; import { continueTrace, startInactiveSpan, startNewTrace, startSpan, startSpanManual, withActiveSpan } from './trace'; import type { CurrentScopes } from './types'; import { getContextFromScope, getScopesFromContext } from './utils/contextData'; +import { getSamplingDecision } from './utils/getSamplingDecision'; import { getActiveSpan } from './utils/getActiveSpan'; import { getTraceData } from './utils/getTraceData'; import { suppressTracing } from './utils/suppressTracing'; +import { isSentryTraceProviderSpan } from './sentryTraceProvider'; /** * Sets the async context strategy to use follow the OTEL context under the hood. * We handle forking a hub inside of our custom OTEL Context Manager (./otelContextManager.ts) */ -export function setOpenTelemetryContextAsyncContextStrategy(): void { +export function setOpenTelemetryContextAsyncContextStrategy( + options: { useOpenTelemetrySpanCreation?: boolean } = {}, +): void { + const { useOpenTelemetrySpanCreation = true } = options; + function getScopes(): CurrentScopes { const ctx = api.context.active(); const scopes = getScopesFromContext(ctx); @@ -83,20 +98,94 @@ export function setOpenTelemetryContextAsyncContextStrategy(): void { } function getCurrentScope(): Scope { - return getScopes().scope; + const scope = getScopes().scope; + if (!useOpenTelemetrySpanCreation) { + syncOpenTelemetrySpanWithScope(scope); + } + return scope; } function getIsolationScope(): Scope { return getScopes().isolationScope; } - setAsyncContextStrategy({ + function withActiveSpanContextOnly(span: Span | null, callback: (scope: Scope) => T): T { + const ctx = span + ? api.trace.setSpan(api.context.active(), span as api.Span) + : api.trace.deleteSpan(api.context.active()); + + return api.context.with(ctx, () => { + const scope = getCurrentScope(); + _INTERNAL_setSpanForScope(scope, span || undefined); + return callback(scope); + }); + } + + function syncOpenTelemetrySpanWithScope(scope: Scope): void { + const activeSpan = api.trace.getSpan(api.context.active()) as Span | undefined; + + if (!activeSpan) { + return; + } + + const scopeSpan = scope.getScopeData().span; + if (scopeSpan === activeSpan) { + return; + } + + const activeSpanContext = activeSpan.spanContext(); + if (activeSpanContext.isRemote) { + if (scopeSpan) { + return; + } + + // A remote OTel span context represents an incoming parent, not a local span + // we can finish and send. Store it as propagation context so the next core + // root span continues the trace and becomes the transaction segment. + const dsc = + baggageHeaderToDynamicSamplingContext(activeSpanContext.traceState?.get(SENTRY_TRACE_STATE_DSC)) ?? {}; + const sampleRandString = activeSpanContext.traceState?.get(SENTRY_TRACE_STATE_SAMPLE_RAND) ?? dsc?.sample_rand; + const sampleRand = typeof sampleRandString === 'string' ? Number(sampleRandString) : undefined; + + scope.setPropagationContext({ + traceId: activeSpanContext.traceId, + parentSpanId: activeSpanContext.spanId, + sampled: getSamplingDecision(activeSpanContext), + dsc, + sampleRand: + typeof sampleRand === 'number' && !Number.isNaN(sampleRand) ? sampleRand : _INTERNAL_safeMathRandom(), + }); + return; + } + + if (scopeSpan && !isSentryTraceProviderSpan(scopeSpan)) { + return; + } + + _INTERNAL_setSpanForScope(scope, activeSpan); + } + + const baseStrategy = { withScope, withSetScope, withSetIsolationScope, withIsolationScope, getCurrentScope, getIsolationScope, + }; + + if (!useOpenTelemetrySpanCreation) { + setAsyncContextStrategy({ + ...baseStrategy, + // Keep OTEL Context and Sentry Scope active-span state in sync, but let + // the core tracing implementation create and send spans. + withActiveSpan: withActiveSpanContextOnly as typeof defaultWithActiveSpan, + }); + return; + } + + setAsyncContextStrategy({ + ...baseStrategy, startSpan, startSpanManual, startInactiveSpan, diff --git a/packages/opentelemetry/src/custom/client.ts b/packages/opentelemetry/src/custom/client.ts index a1f0e4792048..ed97faae4f62 100644 --- a/packages/opentelemetry/src/custom/client.ts +++ b/packages/opentelemetry/src/custom/client.ts @@ -1,9 +1,8 @@ import type { Tracer } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; -import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { Client } from '@sentry/core'; import { SDK_VERSION } from '@sentry/core'; -import type { OpenTelemetryClient as OpenTelemetryClientInterface } from '../types'; +import type { OpenTelemetryClient as OpenTelemetryClientInterface, OpenTelemetryTraceProvider } from '../types'; // Typescript complains if we do not use `...args: any[]` for the mixin, with: // A mixin class must have a constructor with a single rest parameter of type 'any[]'.ts(2545) @@ -23,7 +22,7 @@ export function wrapClientClass< >(ClientClass: ClassConstructor): WrappedClassConstructor { // @ts-expect-error We just assume that this is non-abstract, if you pass in an abstract class this would make it non-abstract class OpenTelemetryClient extends ClientClass implements OpenTelemetryClientInterface { - public traceProvider: BasicTracerProvider | undefined; + public traceProvider: OpenTelemetryTraceProvider | undefined; private _tracer: Tracer | undefined; public constructor(...args: any[]) { diff --git a/packages/opentelemetry/src/exports.ts b/packages/opentelemetry/src/exports.ts index bdda20fd94ce..3baa495fd446 100644 --- a/packages/opentelemetry/src/exports.ts +++ b/packages/opentelemetry/src/exports.ts @@ -45,6 +45,8 @@ export { wrapContextManagerClass } from './contextManager'; export { SentryPropagator, shouldPropagateTraceForUrl } from './propagator'; export { SentrySpanProcessor } from './spanProcessor'; export { SentrySampler, wrapSamplingDecision } from './sampler'; +export { _INTERNAL_getSpanForRecordedException, applyOtelSpanData, SentryTraceProvider } from './sentryTraceProvider'; +export type { OpenTelemetryTraceProvider } from './types'; export { openTelemetrySetupCheck } from './utils/setupCheck'; diff --git a/packages/opentelemetry/src/sentryTraceProvider.ts b/packages/opentelemetry/src/sentryTraceProvider.ts new file mode 100644 index 000000000000..88c9a4fd3cf2 --- /dev/null +++ b/packages/opentelemetry/src/sentryTraceProvider.ts @@ -0,0 +1,379 @@ +/* eslint-disable max-lines */ +import type { + Context, + Span as OpenTelemetrySpan, + SpanOptions, + Tracer, + TracerOptions, + TracerProvider, +} from '@opentelemetry/api'; +import { context, SpanKind, trace } from '@opentelemetry/api'; +import { isTracingSuppressed } from '@opentelemetry/core'; +import { + _INTERNAL_safeMathRandom, + _INTERNAL_setSpanForScope, + addNonEnumerableProperty, + getCurrentScope, + getDynamicSamplingContextFromSpan, + SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SentryNonRecordingSpan, + getSpanStatusFromHttpCode, + spanToJSON, + SPAN_STATUS_ERROR, + SPAN_STATUS_OK, + startInactiveSpan, + startNewTrace, + withScope, +} from '@sentry/core'; +import type { Span, SpanAttributes, SpanLink, SpanStatus, SpanTimeInput } from '@sentry/core'; +import { inferSpanData } from './utils/parseSpanDescription'; +import { getSamplingDecision } from './utils/getSamplingDecision'; +import { setIsSetup } from './utils/setupCheck'; + +type SentrySpanWithOtelKind = Span & { kind?: SpanKind }; +type SentrySpanWithOtelSourceInference = Span & { _sentryOtelInferSource?: boolean }; +type SentryTraceProviderSpan = Span & { _sentryTraceProviderSpan?: true }; + +const recordedExceptionSpans = new WeakMap(); + +export function isSentryTraceProviderSpan(span: Span | undefined): boolean { + return (span as SentryTraceProviderSpan | undefined)?._sentryTraceProviderSpan === true; +} + +export function _INTERNAL_getSpanForRecordedException(exception: unknown): Span | undefined { + if (exception === null || (typeof exception !== 'object' && typeof exception !== 'function')) { + return undefined; + } + + return recordedExceptionSpans.get(exception); +} + +function markSentryTraceProviderSpan(span: Span): Span { + addNonEnumerableProperty(span as SentryTraceProviderSpan, '_sentryTraceProviderSpan', true); + const originalRecordException = span.recordException.bind(span); + addNonEnumerableProperty(span, 'recordException', function (this: Span, exception: unknown, time?: SpanTimeInput) { + if (exception !== null && (typeof exception === 'object' || typeof exception === 'function')) { + // Preserve the closest span that recorded this exception. Frameworks like Nest + // may capture after OTel context has unwound to a parent request span. + if (!recordedExceptionSpans.has(exception)) { + recordedExceptionSpans.set(exception, span); + } + } + + return originalRecordException(exception, time); + }); + return span; +} + +const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE = 'http.response.status_code'; +const LEGACY_HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE = 'http.status_code'; +const RPC_GRPC_STATUS_CODE_ATTRIBUTE = 'rpc.grpc.status_code'; + +const VALID_SPAN_STATUS_MESSAGES = new Set([ + 'ok', + 'deadline_exceeded', + 'unauthenticated', + 'permission_denied', + 'not_found', + 'resource_exhausted', + 'invalid_argument', + 'unimplemented', + 'unavailable', + 'internal_error', + 'unknown_error', + 'cancelled', + 'already_exists', + 'failed_precondition', + 'aborted', + 'out_of_range', + 'data_loss', +]); + +const GRPC_STATUS_CODE_MAP: Record = { + '1': 'cancelled', + '2': 'unknown_error', + '3': 'invalid_argument', + '4': 'deadline_exceeded', + '5': 'not_found', + '6': 'already_exists', + '7': 'permission_denied', + '8': 'resource_exhausted', + '9': 'failed_precondition', + '10': 'aborted', + '11': 'out_of_range', + '12': 'unimplemented', + '13': 'internal_error', + '14': 'unavailable', + '15': 'data_loss', + '16': 'unauthenticated', +}; + +/** + * A minimal OpenTelemetry TracerProvider which creates native Sentry spans. + */ +export class SentryTraceProvider implements TracerProvider { + public readonly resource?: { attributes: SpanAttributes }; + + private readonly _tracers = new Map(); + + public constructor(options: { resource?: { attributes: SpanAttributes } } = {}) { + this.resource = options.resource; + setIsSetup('SentryTraceProvider'); + } + + /** @inheritdoc */ + public getTracer(name: string, version?: string, options?: TracerOptions): Tracer { + const key = JSON.stringify([name, version, options]); + const cachedTracer = this._tracers.get(key); + if (cachedTracer) { + return cachedTracer; + } + + const tracer = new SentryTracer(); + this._tracers.set(key, tracer); + return tracer; + } + + /** Compatibility with SDK tracer providers. */ + public forceFlush(): Promise { + return Promise.resolve(); + } + + /** Compatibility with SDK tracer providers. */ + public shutdown(): Promise { + return Promise.resolve(); + } +} + +class SentryTracer implements Tracer { + /** @inheritdoc */ + public startSpan(name: string, options: SpanOptions = {}, ctx?: Context): OpenTelemetrySpan { + const parentContext = ctx || context.active(); + const parentSpan = options.root ? undefined : trace.getSpan(parentContext); + + if (isTracingSuppressed(parentContext)) { + return this._createNonRecordingSpan(parentSpan); + } + + return context.with(parentContext, () => { + const span = this._startSentrySpan(name, options, parentSpan, ctx !== undefined); + markSentryTraceProviderSpan(span); + applyOtelSpanKind(span, options.kind); + if (options.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === undefined) { + addNonEnumerableProperty(span as SentrySpanWithOtelSourceInference, '_sentryOtelInferSource', true); + } + applyOtelSpanData(span); + return span as OpenTelemetrySpan; + }); + } + + /** @inheritdoc */ + public startActiveSpan unknown>(name: string, fn: F): ReturnType; + public startActiveSpan unknown>( + name: string, + options: SpanOptions, + fn: F, + ): ReturnType; + public startActiveSpan unknown>( + name: string, + options: SpanOptions, + ctx: Context, + fn: F, + ): ReturnType; + public startActiveSpan unknown>( + name: string, + optionsOrFn: SpanOptions | F, + contextOrFn?: Context | F, + fn?: F, + ): ReturnType { + const options = typeof optionsOrFn === 'function' ? {} : optionsOrFn; + const ctx = typeof contextOrFn === 'function' || contextOrFn === undefined ? context.active() : contextOrFn; + const callback = ( + typeof optionsOrFn === 'function' ? optionsOrFn : typeof contextOrFn === 'function' ? contextOrFn : fn + ) as F; + + const span = this.startSpan(name, options, ctx); + const ctxWithSpan = trace.setSpan(ctx, span); + + return context.with(ctxWithSpan, () => { + _INTERNAL_setSpanForScope(getCurrentScope(), span as unknown as Span); + return callback(span) as ReturnType; + }); + } + + private _startSentrySpan( + name: string, + options: SpanOptions, + parentSpan: OpenTelemetrySpan | undefined, + hasExplicitContext: boolean, + ): Span { + const sentryOptions = { + name, + attributes: options.attributes as SpanAttributes | undefined, + links: options.links as SpanLink[] | undefined, + startTime: options.startTime, + }; + + if (options.root) { + return startNewTrace(() => startInactiveSpan({ ...sentryOptions, parentSpan: null })); + } + + if (parentSpan?.spanContext().isRemote) { + return this._startRootSpanWithRemoteParent(sentryOptions, parentSpan); + } + + if (parentSpan) { + return startInactiveSpan({ ...sentryOptions, parentSpan: parentSpan as unknown as Span }); + } + + return startInactiveSpan({ + ...sentryOptions, + parentSpan: hasExplicitContext ? null : undefined, + }); + } + + private _startRootSpanWithRemoteParent( + options: Parameters[0], + parentSpan: OpenTelemetrySpan, + ): Span { + const { spanId, traceId } = parentSpan.spanContext(); + const dsc = getDynamicSamplingContextFromSpan(parentSpan as unknown as Span); + const sampleRand = typeof dsc.sample_rand === 'string' ? Number(dsc.sample_rand) : undefined; + + return withScope(scope => { + scope.setPropagationContext({ + traceId, + parentSpanId: spanId, + sampled: getSamplingDecision(parentSpan.spanContext()), + dsc, + sampleRand: + typeof sampleRand === 'number' && !Number.isNaN(sampleRand) ? sampleRand : _INTERNAL_safeMathRandom(), + }); + _INTERNAL_setSpanForScope(scope, undefined); + + return startInactiveSpan({ ...options, parentSpan: null }); + }); + } + + private _createNonRecordingSpan(parentSpan: OpenTelemetrySpan | undefined): OpenTelemetrySpan { + const span = new SentryNonRecordingSpan({ + traceId: parentSpan?.spanContext().traceId, + }); + markSentryTraceProviderSpan(span); + + return span as OpenTelemetrySpan; + } +} + +/** Apply OTel semantic inference to a Sentry span. */ +export function applyOtelSpanData(span: Span, options: { finalizeStatus?: boolean } = {}): void { + const spanJSON = spanToJSON(span); + const attributes = spanJSON.data; + const kind = (span as SentrySpanWithOtelKind).kind ?? SpanKind.INTERNAL; + const mayInferSource = (span as SentrySpanWithOtelSourceInference)._sentryOtelInferSource === true; + const hasCustomSpanName = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME] !== undefined; + const attributesForInference = + mayInferSource && !hasCustomSpanName && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom' + ? { ...attributes, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: undefined } + : attributes; + const inferred = inferSpanData(spanJSON.description || '', attributesForInference, kind); + + if (kind !== SpanKind.INTERNAL && attributes['otel.kind'] === undefined) { + span.setAttribute('otel.kind', SpanKind[kind]); + } + + if (inferred.op && attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] === undefined) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, inferred.op); + } + + const shouldApplyInferredSource = + inferred.source !== undefined && + inferred.source !== 'custom' && + (spanJSON.parent_span_id === undefined || kind === SpanKind.SERVER); + + if ( + shouldApplyInferredSource && + (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === undefined || (mayInferSource && !hasCustomSpanName)) + ) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, inferred.source); + } + + if (inferred.data) { + Object.entries(inferred.data).forEach(([key, value]) => { + if (value !== undefined && attributes[key] === undefined) { + span.setAttribute(key, value); + } + }); + } + + if ( + mayInferSource && + !hasCustomSpanName && + attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom' && + (inferred.source === undefined || inferred.source === 'custom') + ) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, undefined); + } + + if (options.finalizeStatus) { + applyOtelCompatibilityAttributes(span, attributes); + applyOtelSpanStatus(span, attributes, spanJSON.status); + } + + if ( + inferred.description !== spanJSON.description && + (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom' || (mayInferSource && !hasCustomSpanName)) + ) { + addNonEnumerableProperty(span as Span & { _name?: string }, '_name', inferred.description); + } +} + +function applyOtelSpanKind(span: Span, kind: SpanKind | undefined): void { + addNonEnumerableProperty(span as SentrySpanWithOtelKind, 'kind', kind ?? SpanKind.INTERNAL); +} + +function applyOtelSpanStatus(span: Span, attributes: SpanAttributes, status: string | undefined): void { + if (status === undefined) { + const inferredStatus = inferOtelSpanStatusFromAttributes(attributes); + span.setStatus(inferredStatus || { code: SPAN_STATUS_OK }); + return; + } + + if (!VALID_SPAN_STATUS_MESSAGES.has(status)) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + } +} + +function applyOtelCompatibilityAttributes(span: Span, attributes: SpanAttributes): void { + const legacyHttpStatusCode = attributes[LEGACY_HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE]; + + if (attributes[HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE] === undefined && legacyHttpStatusCode !== undefined) { + span.setAttribute(HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, legacyHttpStatusCode); + attributes[HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE] = legacyHttpStatusCode; + } +} + +function inferOtelSpanStatusFromAttributes(attributes: SpanAttributes): SpanStatus | undefined { + const httpCodeAttribute = + attributes[HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE] ?? attributes[LEGACY_HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE]; + const grpcCodeAttribute = attributes[RPC_GRPC_STATUS_CODE_ATTRIBUTE]; + + const numberHttpCode = + typeof httpCodeAttribute === 'number' + ? httpCodeAttribute + : typeof httpCodeAttribute === 'string' + ? parseInt(httpCodeAttribute, 10) + : undefined; + + if (typeof numberHttpCode === 'number') { + return getSpanStatusFromHttpCode(numberHttpCode); + } + + if (typeof grpcCodeAttribute === 'string') { + return { code: SPAN_STATUS_ERROR, message: GRPC_STATUS_CODE_MAP[grpcCodeAttribute] || 'unknown_error' }; + } + + return undefined; +} diff --git a/packages/opentelemetry/src/types.ts b/packages/opentelemetry/src/types.ts index 807e9b1d857f..4563f7b8c72f 100644 --- a/packages/opentelemetry/src/types.ts +++ b/packages/opentelemetry/src/types.ts @@ -1,10 +1,15 @@ -import type { Span as WriteableSpan, SpanKind, Tracer } from '@opentelemetry/api'; +import type { Span as WriteableSpan, SpanKind, Tracer, TracerProvider } from '@opentelemetry/api'; import type { BasicTracerProvider, ReadableSpan } from '@opentelemetry/sdk-trace-base'; import type { Scope, Span, StartSpanOptions } from '@sentry/core'; +export interface OpenTelemetryTraceProvider extends TracerProvider { + forceFlush(): Promise; + shutdown(): Promise; +} + export interface OpenTelemetryClient { tracer: Tracer; - traceProvider: BasicTracerProvider | undefined; + traceProvider: BasicTracerProvider | OpenTelemetryTraceProvider | undefined; } export interface OpenTelemetrySpanContext extends StartSpanOptions { diff --git a/packages/opentelemetry/src/utils/setupCheck.ts b/packages/opentelemetry/src/utils/setupCheck.ts index 66bc7b445f83..0cc8d9d310f7 100644 --- a/packages/opentelemetry/src/utils/setupCheck.ts +++ b/packages/opentelemetry/src/utils/setupCheck.ts @@ -1,4 +1,9 @@ -type OpenTelemetryElement = 'SentrySpanProcessor' | 'SentryContextManager' | 'SentryPropagator' | 'SentrySampler'; +type OpenTelemetryElement = + | 'SentrySpanProcessor' + | 'SentryContextManager' + | 'SentryPropagator' + | 'SentrySampler' + | 'SentryTraceProvider'; const setupElements = new Set(); From 30b73f9ccfd25bea173ad97e6c6db6bdaa158bc6 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 18 May 2026 18:08:30 +0900 Subject: [PATCH 02/25] Add useSentryTraceProvider flag --- packages/core/src/types/options.ts | 8 +++ packages/node-core/src/sdk/client.ts | 9 ++- packages/node-core/src/sdk/index.ts | 6 +- packages/node/src/sdk/initOtel.ts | 86 +++++++++++++++++++++++++++- 4 files changed, 102 insertions(+), 7 deletions(-) diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index a6029ee86e2f..f065f76fdad0 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -466,6 +466,14 @@ export interface ClientOptions { - public traceProvider: BasicTracerProvider | undefined; + public traceProvider: OpenTelemetryTraceProvider | undefined; public asyncLocalStorageLookup: AsyncLocalStorageLookup | undefined; private _tracer: Tracer | undefined; diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts index 52271ee62363..e4906c5b732e 100644 --- a/packages/node-core/src/sdk/index.ts +++ b/packages/node-core/src/sdk/index.ts @@ -168,7 +168,9 @@ export function validateOpenTelemetrySetup(): void { const required: ReturnType = ['SentryContextManager', 'SentryPropagator']; - if (hasSpansEnabled()) { + const hasSentryTraceProvider = setup.includes('SentryTraceProvider'); + + if (hasSpansEnabled() && !hasSentryTraceProvider) { required.push('SentrySpanProcessor'); } @@ -180,7 +182,7 @@ export function validateOpenTelemetrySetup(): void { } } - if (!setup.includes('SentrySampler')) { + if (!hasSentryTraceProvider && !setup.includes('SentrySampler')) { debug.warn( 'You have to set up the SentrySampler. Without this, the OpenTelemetry & Sentry integration may still work, but sample rates set for the Sentry SDK will not be respected. If you use a custom sampler, make sure to use `wrapSamplingDecision`.', ); diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 2554b98c76f7..9a0e3e05bbe3 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -1,7 +1,7 @@ import { context, propagation, trace } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import { debug as coreDebug } from '@sentry/core'; +import { debug as coreDebug, spanToTraceContext } from '@sentry/core'; import { initializeEsmLoader, type NodeClient, @@ -9,11 +9,16 @@ import { setupOpenTelemetryLogger, } from '@sentry/node-core'; import { + _INTERNAL_getSpanForRecordedException, + applyOtelSpanData, type AsyncLocalStorageLookup, getSentryResource, + type OpenTelemetryTraceProvider, SentryPropagator, SentrySampler, SentrySpanProcessor, + SentryTraceProvider, + setOpenTelemetryContextAsyncContextStrategy, } from '@sentry/opentelemetry'; import { DEBUG_BUILD } from '../debug-build'; import { getOpenTelemetryInstrumentationToPreload } from '../integrations/tracing'; @@ -86,7 +91,12 @@ function getPreloadMethods(integrationNames?: string[]): ((() => void) & { id: s export function setupOtel( client: NodeClient, options: AdditionalOpenTelemetryOptions = {}, -): [BasicTracerProvider, AsyncLocalStorageLookup] { +): [OpenTelemetryTraceProvider | undefined, AsyncLocalStorageLookup | undefined] { + if (client.getOptions()._experiments?.useSentryTraceProvider) { + setOpenTelemetryContextAsyncContextStrategy({ useOpenTelemetrySpanCreation: false }); + return setupSentryTraceProvider(client, options); + } + // Create and configure NodeTracerProvider const provider = new BasicTracerProvider({ sampler: new SentrySampler(client), @@ -110,6 +120,78 @@ export function setupOtel( return [provider, ctxManager.getAsyncLocalStorageLookup()]; } +function setupSentryTraceProvider( + client: NodeClient, + options: AdditionalOpenTelemetryOptions = {}, +): [SentryTraceProvider | undefined, AsyncLocalStorageLookup | undefined] { + if (options.spanProcessors?.length) { + DEBUG_BUILD && + coreDebug.warn( + 'Ignoring `openTelemetrySpanProcessors` because `_experiments.useSentryTraceProvider` is enabled.', + ); + } + + const provider = new SentryTraceProvider({ resource: getSentryResource('node') }); + + if (!trace.setGlobalTracerProvider(provider)) { + DEBUG_BUILD && + coreDebug.warn( + 'Could not register SentryTraceProvider because another OpenTelemetry tracer provider is already registered.', + ); + return [undefined, undefined]; + } + + propagation.setGlobalPropagator(new SentryPropagator()); + + const ctxManager = new SentryContextManager(); + context.setGlobalContextManager(ctxManager); + + client.on('spanEnd', span => { + applyOtelSpanData(span, { finalizeStatus: true }); + }); + + client.addEventProcessor((event, hint) => { + // Some frameworks capture exceptions after the OTel context has already + // unwound. If a provider-created span recorded this exact exception first, + // keep the error event linked to that span instead of the ambient parent. + const span = _INTERNAL_getSpanForRecordedException(hint.originalException); + if (!span) { + return event; + } + + event.contexts = { + ...event.contexts, + trace: spanToTraceContext(span), + }; + + return event; + }); + + client.on('preprocessEvent', event => { + if (event.type !== 'transaction' || client.getOptions().traceLifecycle === 'stream') { + return; + } + + event.contexts = { + ...event.contexts, + ...(typeof event.contexts?.trace?.data?.['http.response.status_code'] === 'number' + ? { + response: { + status_code: event.contexts.trace.data['http.response.status_code'], + ...event.contexts.response, + }, + } + : undefined), + otel: { + resource: provider.resource?.attributes, + ...event.contexts?.otel, + }, + }; + }); + + return [provider, ctxManager.getAsyncLocalStorageLookup()]; +} + /** Just exported for tests. */ export function _clampSpanProcessorTimeout(maxSpanWaitDuration: number | undefined): number | undefined { if (maxSpanWaitDuration == null) { From b6376f426dc3b9f718edc7d7a1911224dbeb2367 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 18 May 2026 18:11:17 +0900 Subject: [PATCH 03/25] Fix HTTP propagation with SentryTraceProvider --- .../src/integrations/http/httpServerSpansIntegration.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts index 48a6bb08897e..b78b2c590f54 100644 --- a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts @@ -2,7 +2,7 @@ import { errorMonitor } from 'node:events'; import type { IncomingHttpHeaders } from 'node:http'; import { context, SpanKind, trace } from '@opentelemetry/api'; import type { RPCMetadata } from '@opentelemetry/core'; -import { getRPCMetadata, isTracingSuppressed, RPCType, setRPCMetadata } from '@opentelemetry/core'; +import { isTracingSuppressed, RPCType, setRPCMetadata } from '@opentelemetry/core'; import { ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_HTTP_ROUTE, @@ -196,7 +196,7 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions isEnded = true; - const newAttributes = getIncomingRequestAttributesOnResponse(request, response); + const newAttributes = getIncomingRequestAttributesOnResponse(request, response, rpcMetadata); span.setAttributes(newAttributes); span.setStatus(status); span.end(); @@ -368,6 +368,7 @@ function isCompressed(headers: IncomingHttpHeaders): boolean { function getIncomingRequestAttributesOnResponse( request: HttpIncomingMessage, response: HttpServerResponse, + rpcMetadata?: RPCMetadata, ): SpanAttributes { // take socket from the request, // since it may be detached from the response object in keep-alive mode @@ -381,7 +382,6 @@ function getIncomingRequestAttributesOnResponse( 'http.status_text': statusMessage?.toUpperCase(), }; - const rpcMetadata = getRPCMetadata(context.active()); if (socket) { const { localAddress, localPort, remoteAddress, remotePort } = socket; // eslint-disable-next-line deprecation/deprecation From b369fa673b03a9767739a19bfac97ac59f580147 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 18 May 2026 18:15:11 +0900 Subject: [PATCH 04/25] Add SentryTraceProvider test coverage --- .../tracing/sentryNonRecordingSpan.test.ts | 12 + packages/core/test/lib/tracing/trace.test.ts | 12 +- .../core/test/lib/utils/traceData.test.ts | 32 +++ packages/node/test/sdk/init.test.ts | 29 +++ .../test/sentryTraceProvider.test.ts | 225 ++++++++++++++++++ .../test/utils/setupCheck.test.ts | 8 + 6 files changed, 312 insertions(+), 6 deletions(-) create mode 100644 packages/opentelemetry/test/sentryTraceProvider.test.ts diff --git a/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts b/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts index 1aeec1893908..93617f706f5f 100644 --- a/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts +++ b/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts @@ -12,6 +12,7 @@ describe('SentryNonRecordingSpan', () => { spanId: expect.any(String), traceId: expect.any(String), traceFlags: TRACE_FLAG_NONE, + sampled: undefined, }); expect(spanIsSampled(span)).toBe(false); @@ -39,4 +40,15 @@ describe('SentryNonRecordingSpan', () => { start_timestamp: 0, }); }); + + it('can carry an explicit negative sampling decision', () => { + const span: Span = new SentryNonRecordingSpan({ sampled: false }); + + expect(span.spanContext()).toEqual({ + spanId: expect.any(String), + traceId: expect.any(String), + traceFlags: TRACE_FLAG_NONE, + sampled: false, + }); + }); }); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 0d0e652e64aa..4d3258eb338a 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -230,16 +230,20 @@ describe('startSpan', () => { setCurrentClient(client); client.init(); + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + }); + const span = startSpan({ name: 'GET users/[id]' }, span => { return span; }); expect(span).toBeDefined(); expect(span).toBeInstanceOf(SentryNonRecordingSpan); + expect(span.spanContext().traceId).toBe('12345678901234567890123456789012'); expect(getDynamicSamplingContextFromSpan(span)).toEqual({ environment: 'production', - sample_rate: '0', - sampled: 'false', trace_id: expect.stringMatching(/[a-f0-9]{32}/), transaction: 'GET users/[id]', }); @@ -882,8 +886,6 @@ describe('startSpanManual', () => { expect(span).toBeInstanceOf(SentryNonRecordingSpan); expect(getDynamicSamplingContextFromSpan(span)).toEqual({ environment: 'production', - sample_rate: '0', - sampled: 'false', trace_id: expect.stringMatching(/[a-f0-9]{32}/), transaction: 'GET users/[id]', }); @@ -1394,8 +1396,6 @@ describe('startInactiveSpan', () => { expect(span).toBeInstanceOf(SentryNonRecordingSpan); expect(getDynamicSamplingContextFromSpan(span)).toEqual({ environment: 'production', - sample_rate: '0', - sampled: 'false', trace_id: expect.stringMatching(/[a-f0-9]{32}/), transaction: 'GET users/[id]', }); diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index b2101502cb43..678eec31d2de 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -11,6 +11,7 @@ import { SentrySpan, setAsyncContextStrategy, setCurrentClient, + startSpan, withActiveSpan, } from '../../../src/'; import { getAsyncContextStrategy } from '../../../src/asyncContext'; @@ -142,6 +143,37 @@ describe('getTraceData', () => { }); }); + it('does not add a sampled flag for an active TwP placeholder span', () => { + setupClient({ tracesSampleRate: undefined }); + + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + }); + + startSpan({ name: 'twp-root' }, () => { + const data = getTraceData(); + + expect(data).toEqual({ + 'sentry-trace': expect.stringMatching(/^12345678901234567890123456789012-[a-f0-9]{16}$/), + baggage: + 'sentry-environment=production,sentry-public_key=123,sentry-trace_id=12345678901234567890123456789012,sentry-transaction=twp-root', + }); + expect(data['sentry-trace']?.split('-')).toHaveLength(2); + }); + }); + + it('keeps an explicit negative sampling decision for an active unsampled span', () => { + setupClient({ tracesSampleRate: 0 }); + + startSpan({ name: 'unsampled-root' }, () => { + const data = getTraceData(); + + expect(data['sentry-trace']).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-0$/); + expect(data.baggage).toContain('sentry-sampled=false'); + }); + }); + it('allows to pass a span directly', () => { setupClient(); diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index 26fe2d9933e6..0fe346b09f88 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -194,6 +194,35 @@ describe('init()', () => { expect(client?.traceProvider).not.toBeDefined(); }); + + it('uses the minimal Sentry trace provider when the experiment is enabled', () => { + init({ dsn: PUBLIC_DSN, _experiments: { useSentryTraceProvider: true } }); + + const client = getClient(); + + expect(client?.traceProvider).toBeInstanceOf(SentryOpentelemetry.SentryTraceProvider); + }); + + it('warns and ignores additional span processors when the minimal Sentry trace provider is enabled', () => { + const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); + + init({ + dsn: PUBLIC_DSN, + _experiments: { useSentryTraceProvider: true }, + openTelemetrySpanProcessors: [ + { + forceFlush: () => Promise.resolve(), + onStart: () => undefined, + onEnd: () => undefined, + shutdown: () => Promise.resolve(), + }, + ], + }); + + expect(warnSpy).toHaveBeenCalledWith( + 'Ignoring `openTelemetrySpanProcessors` because `_experiments.useSentryTraceProvider` is enabled.', + ); + }); }); it('returns initialized client', () => { diff --git a/packages/opentelemetry/test/sentryTraceProvider.test.ts b/packages/opentelemetry/test/sentryTraceProvider.test.ts new file mode 100644 index 000000000000..ffb409ef0558 --- /dev/null +++ b/packages/opentelemetry/test/sentryTraceProvider.test.ts @@ -0,0 +1,225 @@ +import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; +import { + continueTrace, + getActiveSpan, + getDynamicSamplingContextFromSpan, + getRootSpan, + spanToJSON, + SPAN_STATUS_ERROR, + startSpanManual, + type Span, +} from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { SentryAsyncLocalStorageContextManager } from '../src/asyncLocalStorageContextManager'; +import { setOpenTelemetryContextAsyncContextStrategy } from '../src/asyncContextStrategy'; +import { + _INTERNAL_getSpanForRecordedException, + applyOtelSpanData, + SentryTraceProvider, +} from '../src/sentryTraceProvider'; +import { cleanupOtel } from './helpers/mockSdkInit'; +import { init as initTestClient } from './helpers/TestClient'; + +describe('SentryTraceProvider', () => { + beforeEach(() => { + (global as { __SENTRY__?: unknown }).__SENTRY__ = {}; + setOpenTelemetryContextAsyncContextStrategy({ useOpenTelemetrySpanCreation: false }); + initTestClient({ tracesSampleRate: 1 }); + context.setGlobalContextManager(new SentryAsyncLocalStorageContextManager()); + trace.setGlobalTracerProvider(new SentryTraceProvider()); + }); + + afterEach(async () => { + await cleanupOtel(); + }); + + it('creates Sentry spans from the global OpenTelemetry tracer', () => { + const span = trace.getTracer('test').startSpan('SELECT users', { + attributes: { + 'db.system.name': 'postgresql', + 'db.statement': 'SELECT * FROM users', + }, + }); + + expect(spanToJSON(span as Span)).toEqual({ + data: { + 'sentry.origin': 'manual', + 'sentry.op': 'db', + 'sentry.sample_rate': 1, + 'sentry.source': 'task', + 'db.system.name': 'postgresql', + 'db.statement': 'SELECT * FROM users', + }, + description: 'SELECT * FROM users', + op: 'db', + origin: 'manual', + parent_span_id: undefined, + span_id: span.spanContext().spanId, + start_timestamp: expect.any(Number), + status: undefined, + timestamp: undefined, + trace_id: span.spanContext().traceId, + profile_id: undefined, + exclusive_time: undefined, + measurements: undefined, + is_segment: undefined, + segment_id: undefined, + links: undefined, + }); + }); + + it('parents inactive spans to the active OpenTelemetry span', () => { + trace.getTracer('test').startActiveSpan('parent', parent => { + const child = trace.getTracer('test').startSpan('child'); + + expect(spanToJSON(child as Span).parent_span_id).toBe(parent.spanContext().spanId); + }); + }); + + it('sets active OpenTelemetry spans on the Sentry scope', () => { + trace.getTracer('test').startActiveSpan('parent', parent => { + expect(getActiveSpan()).toBe(parent); + }); + }); + + it('syncs manual OpenTelemetry context switches onto the Sentry scope', () => { + const tracer = trace.getTracer('test'); + + tracer.startActiveSpan('parent', parent => { + const child = tracer.startSpan('child'); + const childContext = trace.setSpan(context.active(), child); + + context.with(childContext, () => { + expect(getActiveSpan()).toBe(child); + }); + + expect(getActiveSpan()).toBe(parent); + + child.end(); + parent.end(); + }); + }); + + it('does not replace active core spans with provider-created OpenTelemetry spans', () => { + const tracer = trace.getTracer('test'); + + tracer.startActiveSpan('otel-parent', otelParent => { + startSpanManual({ name: 'sentry-parent' }, sentryParent => { + const otelChild = tracer.startSpan('otel-child'); + const otelChildContext = trace.setSpan(context.active(), otelChild); + + context.with(otelChildContext, () => { + expect(getActiveSpan()).toBe(sentryParent); + }); + + otelChild.end(); + sentryParent.end(); + }); + + otelParent.end(); + }); + }); + + it('parents core spans to the active OpenTelemetry span in context-only mode', () => { + trace.getTracer('test').startActiveSpan('parent', parent => { + startSpanManual({ name: 'child' }, child => { + expect(spanToJSON(child).parent_span_id).toBe(parent.spanContext().spanId); + child.end(); + }); + }); + }); + + it('continues remote OpenTelemetry contexts as root core spans in context-only mode', () => { + const traceId = '12312012123120121231201212312012'; + const parentSpanId = '1121201211212012'; + const remoteContext = trace.setSpanContext(context.active(), { + traceId, + spanId: parentSpanId, + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }); + + context.with(remoteContext, () => { + continueTrace({ sentryTrace: `${traceId}-${parentSpanId}-1`, baggage: undefined }, () => { + startSpanManual({ name: 'server' }, span => { + expect(getRootSpan(span)).toBe(span); + expect(spanToJSON(span)).toMatchObject({ + trace_id: traceId, + parent_span_id: parentSpanId, + }); + expect(getDynamicSamplingContextFromSpan(span)).toEqual({}); + span.end(); + }); + }); + }); + }); + + it('continues remote OpenTelemetry span contexts as root Sentry spans', () => { + const remoteContext = trace.setSpanContext(context.active(), { + traceId: '12312012123120121231201212312012', + spanId: '1121201211212012', + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }); + + const span = trace.getTracer('test').startSpan('server', { kind: SpanKind.SERVER }, remoteContext); + const json = spanToJSON(span as Span); + + expect(json.trace_id).toBe('12312012123120121231201212312012'); + expect(json.parent_span_id).toBe('1121201211212012'); + expect(json.data?.['otel.kind']).toBe('SERVER'); + }); + + it('remembers the closest provider-created span that recorded an exception', () => { + const error = new Error('test error'); + + trace.getTracer('test').startActiveSpan('outer', outer => { + let innerSpan: unknown; + + trace.getTracer('test').startActiveSpan('inner', inner => { + innerSpan = inner; + inner.recordException(error); + inner.end(); + }); + + outer.recordException(error); + + expect(_INTERNAL_getSpanForRecordedException(error)).toBe(innerSpan); + outer.end(); + }); + }); + + it('finalizes span statuses like the OpenTelemetry exporter', () => { + const okSpan = trace.getTracer('test').startSpan('ok'); + applyOtelSpanData(okSpan as Span, { finalizeStatus: true }); + expect(spanToJSON(okSpan as Span).status).toBe('ok'); + + const httpErrorSpan = trace.getTracer('test').startSpan('http-error'); + httpErrorSpan.setAttribute('http.response.status_code', 500); + applyOtelSpanData(httpErrorSpan as Span, { finalizeStatus: true }); + expect(spanToJSON(httpErrorSpan as Span).status).toBe('internal_error'); + + const legacyHttpErrorSpan = trace.getTracer('test').startSpan('legacy-http-error'); + legacyHttpErrorSpan.setAttribute('http.status_code', 500); + applyOtelSpanData(legacyHttpErrorSpan as Span, { finalizeStatus: true }); + expect(spanToJSON(legacyHttpErrorSpan as Span).status).toBe('internal_error'); + expect(spanToJSON(legacyHttpErrorSpan as Span).data).toMatchObject({ + 'http.response.status_code': 500, + 'http.status_code': 500, + }); + + const customErrorSpan = trace.getTracer('test').startSpan('custom-error'); + customErrorSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'This is a custom error' }); + applyOtelSpanData(customErrorSpan as Span, { finalizeStatus: true }); + expect(spanToJSON(customErrorSpan as Span).status).toBe('internal_error'); + }); + + it('does not keep default custom source on provider-created spans', () => { + const span = trace.getTracer('test').startSpan('custom-source'); + span.setAttribute('sentry.source', 'custom'); + + applyOtelSpanData(span as Span, { finalizeStatus: true }); + + expect(spanToJSON(span as Span).data?.['sentry.source']).toBeUndefined(); + }); +}); diff --git a/packages/opentelemetry/test/utils/setupCheck.test.ts b/packages/opentelemetry/test/utils/setupCheck.test.ts index 526945108ba7..ca3073197249 100644 --- a/packages/opentelemetry/test/utils/setupCheck.test.ts +++ b/packages/opentelemetry/test/utils/setupCheck.test.ts @@ -2,6 +2,7 @@ import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { SentrySampler } from '../../src/sampler'; import { SentrySpanProcessor } from '../../src/spanProcessor'; +import { SentryTraceProvider } from '../../src/sentryTraceProvider'; import { openTelemetrySetupCheck } from '../../src/utils/setupCheck'; import { setupOtel } from '../helpers/initOtel'; import { cleanupOtel } from '../helpers/mockSdkInit'; @@ -41,4 +42,11 @@ describe('openTelemetrySetupCheck', () => { const setup = openTelemetrySetupCheck(); expect(setup).toEqual(['SentrySampler', 'SentrySpanProcessor']); }); + + it('returns SentryTraceProvider setup', () => { + new SentryTraceProvider(); + + const setup = openTelemetrySetupCheck(); + expect(setup).toEqual(['SentryTraceProvider']); + }); }); From 4250c2d68c2f21da4fc796d16c971e225ec5df66 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 18 May 2026 19:44:36 +0900 Subject: [PATCH 05/25] Add e2e tests # Conflicts: # dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts # dev-packages/e2e-tests/test-applications/nextjs-14/instrumentation.ts # dev-packages/e2e-tests/test-applications/nextjs-15-basepath/sentry.server.config.ts # dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.server.config.ts # dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts # dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/sentry.server.config.ts # dev-packages/e2e-tests/test-applications/nextjs-16-streaming/sentry.server.config.ts # dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/sentry.server.config.ts # dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts # dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/sentry.server.config.ts # dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts # dev-packages/e2e-tests/test-applications/nextjs-app-dir/instrumentation.ts # dev-packages/e2e-tests/test-applications/nextjs-pages-dir/instrumentation.ts # dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts --- .../test-applications/astro-4/package.json | 9 +++ .../astro-4/sentry.server.config.js | 7 ++ .../test-applications/astro-5/package.json | 9 +++ .../astro-5/sentry.server.config.js | 7 ++ .../test-applications/astro-6/package.json | 13 +++- .../astro-6/sentry.server.config.js | 7 ++ .../aws-serverless/package.json | 9 +++ .../TracingCjs/sentry-trace-provider-auto.mjs | 14 ++++ .../TracingEsm/sentry-trace-provider-auto.mjs | 14 ++++ .../aws-serverless/src/stack.ts | 6 +- .../instrument.mjs | 7 ++ .../package.json | 9 +++ .../create-remix-app-express/instrument.mjs | 7 ++ .../create-remix-app-express/package.json | 9 +++ .../app/entry.server.tsx | 7 ++ .../instrument.server.cjs | 7 ++ .../create-remix-app-v2-non-vite/package.json | 9 +++ .../create-remix-app-v2/instrument.server.cjs | 7 ++ .../create-remix-app-v2/package.json | 9 +++ .../elysia-node/package.json | 13 +++- .../test-applications/elysia-node/src/app.ts | 7 ++ .../test-applications/hono-4/package.json | 15 +++- .../hono-4/src/instrument.node.ts | 7 ++ .../test-applications/nestjs-11/package.json | 9 +++ .../nestjs-11/src/instrument.ts | 7 ++ .../test-applications/nestjs-8/package.json | 9 +++ .../nestjs-8/src/instrument.ts | 7 ++ .../nestjs-basic-with-graphql/package.json | 9 +++ .../src/instrument.ts | 7 ++ .../nestjs-basic/package.json | 9 +++ .../nestjs-basic/src/instrument.ts | 7 ++ .../nestjs-bullmq/package.json | 9 +++ .../nestjs-bullmq/src/instrument.ts | 7 ++ .../nestjs-distributed-tracing/package.json | 9 +++ .../src/instrument.ts | 7 ++ .../nestjs-fastify/package.json | 9 +++ .../nestjs-fastify/src/instrument.ts | 7 ++ .../nestjs-graphql/package.json | 9 +++ .../nestjs-graphql/src/instrument.ts | 7 ++ .../nestjs-microservices/package.json | 9 +++ .../nestjs-microservices/src/instrument.ts | 7 ++ .../nestjs-websockets/package.json | 9 +++ .../nestjs-websockets/src/instrument.ts | 7 ++ .../package.json | 9 +++ .../src/instrument.ts | 7 ++ .../nestjs-with-submodules/package.json | 9 +++ .../nestjs-with-submodules/src/instrument.ts | 7 ++ .../nextjs-13/instrumentation.ts | 7 ++ .../test-applications/nextjs-13/package.json | 7 ++ .../nextjs-14/instrumentation.ts | 7 ++ .../test-applications/nextjs-14/package.json | 7 ++ .../nextjs-15-basepath/package.json | 9 +++ .../sentry.server.config.ts | 7 ++ .../nextjs-15-intl/package.json | 9 +++ .../nextjs-15-intl/sentry.server.config.ts | 7 ++ .../nextjs-15-t3/package.json | 9 +++ .../nextjs-15-t3/sentry.server.config.ts | 7 ++ .../test-applications/nextjs-15/package.json | 7 ++ .../nextjs-15/sentry.server.config.ts | 7 ++ .../nextjs-16-cacheComponents/package.json | 9 ++- .../sentry.server.config.ts | 7 ++ .../nextjs-16-streaming/package.json | 9 +++ .../sentry.server.config.ts | 7 ++ .../nextjs-16-trailing-slash/package.json | 5 ++ .../sentry.server.config.ts | 7 ++ .../nextjs-16-tunnel/package.json | 5 ++ .../nextjs-16-tunnel/sentry.server.config.ts | 7 ++ .../nextjs-16-userfeedback/package.json | 9 +++ .../sentry.server.config.ts | 7 ++ .../test-applications/nextjs-16/package.json | 5 ++ .../nextjs-16/sentry.server.config.ts | 7 ++ .../nextjs-app-dir/instrumentation.ts | 7 ++ .../nextjs-app-dir/package.json | 5 ++ .../nextjs-orpc/package.json | 9 ++- .../nextjs-orpc/sentry.server.config.ts | 7 ++ .../nextjs-pages-dir/instrumentation.ts | 7 ++ .../nextjs-pages-dir/package.json | 5 ++ .../test-applications/nitro-3/instrument.mjs | 7 ++ .../test-applications/nitro-3/package.json | 9 +++ .../node-connect/package.json | 9 +++ .../test-applications/node-connect/src/app.ts | 7 ++ .../node-connect/tests/transactions.test.ts | 78 +++++++++++-------- .../node-express-cjs-preload/package.json | 9 +++ .../node-express-cjs-preload/src/app.js | 7 ++ .../node-express-esm-loader/package.json | 9 +++ .../src/instrument.mjs | 7 ++ .../node-express-esm-preload/package.json | 9 +++ .../node-express-esm-preload/src/app.mjs | 7 ++ .../package.json | 9 +++ .../src/app.ts | 7 ++ .../node-express-mcp-v2/instrument.mjs | 7 ++ .../node-express-mcp-v2/package.json | 9 ++- .../node-express-send-to-sentry/package.json | 9 ++- .../node-express-send-to-sentry/src/app.ts | 7 ++ .../node-express-streaming/package.json | 9 +++ .../node-express-streaming/src/app.ts | 7 ++ .../node-express-v5/package.json | 9 +++ .../node-express-v5/src/app.ts | 7 ++ .../node-express/package.json | 9 +++ .../test-applications/node-express/src/app.ts | 7 ++ .../node-fastify-3/package.json | 9 +++ .../src/app-handle-error-override.ts | 7 ++ .../node-fastify-3/src/app.ts | 7 ++ .../node-fastify-4/package.json | 9 +++ .../src/app-handle-error-override.ts | 7 ++ .../node-fastify-4/src/app.ts | 7 ++ .../node-fastify-5/package.json | 9 +++ .../src/app-handle-error-override.ts | 7 ++ .../node-fastify-5/src/app.ts | 7 ++ .../node-firebase/firestore-app/src/init.ts | 7 ++ .../node-firebase/functions/src/init.ts | 7 ++ .../node-firebase/package.json | 9 +++ .../test-applications/node-hapi/package.json | 9 +++ .../test-applications/node-hapi/src/app.js | 7 ++ .../test-applications/node-koa/index.js | 7 ++ .../test-applications/node-koa/package.json | 9 +++ .../node-profiling-cjs/index.ts | 7 ++ .../node-profiling-cjs/package.json | 9 +++ .../node-profiling-esm/index.ts | 7 ++ .../node-profiling-esm/package.json | 9 +++ .../nuxt-3-dynamic-import/package.json | 9 +++ .../sentry.server.config.ts | 7 ++ .../test-applications/nuxt-3-min/package.json | 9 +++ .../nuxt-3-min/sentry.server.config.ts | 7 ++ .../nuxt-3-top-level-import/package.json | 9 +++ .../sentry.server.config.ts | 7 ++ .../test-applications/nuxt-3/package.json | 11 ++- .../nuxt-3/sentry.server.config.ts | 7 ++ .../test-applications/nuxt-4/package.json | 11 ++- .../nuxt-4/sentry.server.config.ts | 7 ++ .../test-applications/nuxt-5/package.json | 13 +++- .../nuxt-5/sentry.server.config.ts | 7 ++ .../instrument.mjs | 7 ++ .../package.json | 9 +++ .../instrument.mjs | 7 ++ .../package.json | 9 ++- .../instrument.mjs | 7 ++ .../package.json | 11 +++ .../routes/performance/with-middleware.tsx | 3 +- .../react-router-7-framework/instrument.mjs | 7 ++ .../react-router-7-framework/package.json | 5 ++ .../performance/middleware.server.test.ts | 17 ++-- .../remix-server-timing/instrument.server.cjs | 7 ++ .../remix-server-timing/package.json | 9 +++ .../solidstart-dynamic-import/package.json | 9 +++ .../src/instrument.server.ts | 7 ++ .../solidstart-spa/package.json | 9 +++ .../solidstart-spa/src/instrument.server.ts | 7 ++ .../solidstart-top-level-import/package.json | 9 +++ .../src/instrument.server.ts | 7 ++ .../test-applications/solidstart/package.json | 9 +++ .../solidstart/src/instrument.server.ts | 7 ++ .../supabase-nextjs/package.json | 9 +++ .../supabase-nextjs/sentry.server.config.ts | 7 ++ .../sveltekit-2-kit-tracing/package.json | 9 +++ .../src/instrumentation.server.ts | 7 ++ .../sveltekit-2-svelte-5/package.json | 9 +++ .../sveltekit-2-svelte-5/src/hooks.server.ts | 7 ++ .../sveltekit-2.5.0-twp/package.json | 9 +++ .../sveltekit-2.5.0-twp/src/hooks.server.ts | 7 ++ .../sveltekit-2/package.json | 11 ++- .../sveltekit-2/src/hooks.server.ts | 7 ++ .../tanstackstart-react/instrument.server.mjs | 7 ++ .../tanstackstart-react/package.json | 5 ++ .../tsx-express/instrument.mjs | 7 ++ .../tsx-express/package.json | 9 +++ 166 files changed, 1342 insertions(+), 58 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/sentry-trace-provider-auto.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/sentry-trace-provider-auto.mjs diff --git a/dev-packages/e2e-tests/test-applications/astro-4/package.json b/dev-packages/e2e-tests/test-applications/astro-4/package.json index 0afd444f6d3d..e5a3b963dc10 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/package.json +++ b/dev-packages/e2e-tests/test-applications/astro-4/package.json @@ -26,5 +26,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "astro-4 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/astro-4/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-4/sentry.server.config.js index 0662d678dc7c..2599cb4541e0 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/sentry.server.config.js +++ b/dev-packages/e2e-tests/test-applications/astro-4/sentry.server.config.js @@ -4,6 +4,13 @@ Sentry.init({ dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, environment: 'qa', tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), spotlight: true, tunnel: 'http://localhost:3031/', // proxy server }); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/package.json b/dev-packages/e2e-tests/test-applications/astro-5/package.json index 268ce9ed82ca..718c0edf4882 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/package.json +++ b/dev-packages/e2e-tests/test-applications/astro-5/package.json @@ -26,5 +26,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "astro-5 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js index 2b79ec0ed337..e429642768ed 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js +++ b/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js @@ -4,5 +4,12 @@ Sentry.init({ dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, environment: 'qa', tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server }); diff --git a/dev-packages/e2e-tests/test-applications/astro-6/package.json b/dev-packages/e2e-tests/test-applications/astro-6/package.json index 9ba8edb97274..c04b24c0f04b 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6/package.json +++ b/dev-packages/e2e-tests/test-applications/astro-6/package.json @@ -9,7 +9,9 @@ "astro": "astro", "start": "node ./dist/server/entry.mjs", "test:build": "pnpm install && pnpm build", - "test:assert": "TEST_ENV=production playwright test" + "test:build:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "test:assert": "TEST_ENV=production playwright test", + "test:assert:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert" }, "dependencies": { "@astrojs/node": "^10.0.0", @@ -21,5 +23,14 @@ "volta": { "node": "22.22.0", "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "pnpm test:build:sentry-trace-provider", + "assert-command": "pnpm test:assert:sentry-trace-provider", + "label": "astro-6 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/astro-6/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-6/sentry.server.config.js index bc90470cef38..91af273dc4b7 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6/sentry.server.config.js +++ b/dev-packages/e2e-tests/test-applications/astro-6/sentry.server.config.js @@ -4,6 +4,13 @@ Sentry.init({ dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, environment: 'qa', tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server debug: true, }); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/package.json b/dev-packages/e2e-tests/test-applications/aws-serverless/package.json index f74e7a670c50..1f214e20f4f9 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/package.json +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/package.json @@ -21,5 +21,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "aws-serverless (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/sentry-trace-provider-auto.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/sentry-trace-provider-auto.mjs new file mode 100644 index 000000000000..7aa885da2405 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/sentry-trace-provider-auto.mjs @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/aws-serverless'; + +const optionsWithTracingEnabled = process.env.SENTRY_TRACES_SAMPLE_RATE + ? { + tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE), + } + : {}; + +Sentry.init({ + integrations: Sentry.getDefaultIntegrations(optionsWithTracingEnabled), + _experiments: { + useSentryTraceProvider: true, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/sentry-trace-provider-auto.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/sentry-trace-provider-auto.mjs new file mode 100644 index 000000000000..7aa885da2405 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/sentry-trace-provider-auto.mjs @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/aws-serverless'; + +const optionsWithTracingEnabled = process.env.SENTRY_TRACES_SAMPLE_RATE + ? { + tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE), + } + : {}; + +Sentry.init({ + integrations: Sentry.getDefaultIntegrations(optionsWithTracingEnabled), + _experiments: { + useSentryTraceProvider: true, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts index b06bfc360cb9..3bf69e419a72 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts @@ -9,6 +9,10 @@ import { execFileSync } from 'node:child_process'; const LAMBDA_FUNCTIONS_DIR = './src/lambda-functions-npm'; const LAMBDA_FUNCTION_TIMEOUT = 10; +const LAMBDA_AUTO_INIT_IMPORT = + process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? './sentry-trace-provider-auto.mjs' + : '@sentry/aws-serverless/awslambda-auto'; export const SAM_PORT = 3001; /** Match SAM / Docker to this machine so Apple Silicon does not mix arm64 images with an x86_64 template default. */ @@ -113,7 +117,7 @@ export class LocalLambdaStack extends Stack { SENTRY_DSN: dsn, SENTRY_TRACES_SAMPLE_RATE: 1.0, SENTRY_DEBUG: true, - NODE_OPTIONS: `--import=@sentry/aws-serverless/awslambda-auto`, + NODE_OPTIONS: `--import=${LAMBDA_AUTO_INIT_IMPORT}`, }, }, }, diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/instrument.mjs b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/instrument.mjs index b52053445456..290544241a70 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/instrument.mjs @@ -3,6 +3,13 @@ import * as process from 'process'; Sentry.init({ tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, tunnel: 'http://localhost:3031/', // proxy server diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json index a413c5c31ba0..4ba9fa8cb99e 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json @@ -48,5 +48,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "create-remix-app-express-vite-dev (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/instrument.mjs b/dev-packages/e2e-tests/test-applications/create-remix-app-express/instrument.mjs index a23e60d854bd..9f025a174241 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/instrument.mjs @@ -3,6 +3,13 @@ import process from 'process'; Sentry.init({ tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, tunnel: 'http://localhost:3031/', // proxy server diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json index 2dc4f42dd34d..584521625cbe 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json @@ -52,5 +52,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "create-remix-app-express (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/app/entry.server.tsx index 41974897eeae..d3ec2c5e489d 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/app/entry.server.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/app/entry.server.tsx @@ -23,6 +23,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: 'http://localhost:3031/', // proxy server tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); const handleErrorImpl = () => { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/instrument.server.cjs b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/instrument.server.cjs index 557ab8d18f17..7efece8057fb 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/instrument.server.cjs +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/instrument.server.cjs @@ -2,6 +2,13 @@ const Sentry = require('@sentry/remix'); Sentry.init({ tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, tunnel: 'http://localhost:3032/', // proxy server diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json index 38a7e231ddf1..d2ad0a78b1ff 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json @@ -35,5 +35,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "create-remix-app-v2-non-vite (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/instrument.server.cjs b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/instrument.server.cjs index 6d211cac4592..023cd91bd8cd 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/instrument.server.cjs +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/instrument.server.cjs @@ -2,6 +2,13 @@ const Sentry = require('@sentry/remix'); Sentry.init({ tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, tunnel: 'http://localhost:3031/', // proxy server diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json index fd57d2920d5a..8d5734eac613 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json @@ -38,5 +38,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "create-remix-app-v2 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/elysia-node/package.json b/dev-packages/e2e-tests/test-applications/elysia-node/package.json index fcd2a4e8f71b..84e363973b3a 100644 --- a/dev-packages/e2e-tests/test-applications/elysia-node/package.json +++ b/dev-packages/e2e-tests/test-applications/elysia-node/package.json @@ -8,7 +8,9 @@ "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:build": "pnpm install", - "test:assert": "pnpm test" + "test:build:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "test:assert": "pnpm test", + "test:assert:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert" }, "dependencies": { "@sentry/elysia": "file:../../packed/sentry-elysia-packed.tgz", @@ -22,5 +24,14 @@ "volta": { "extends": "../../package.json", "node": "24.11.0" + }, + "sentryTest": { + "variants": [ + { + "build-command": "pnpm test:build:sentry-trace-provider", + "assert-command": "pnpm test:assert:sentry-trace-provider", + "label": "elysia-node (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/elysia-node/src/app.ts b/dev-packages/e2e-tests/test-applications/elysia-node/src/app.ts index 375ca9a29c6d..365ced0c99ac 100644 --- a/dev-packages/e2e-tests/test-applications/elysia-node/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/elysia-node/src/app.ts @@ -8,6 +8,13 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); const app = Sentry.withElysia(new Elysia({ adapter: node() })); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/package.json b/dev-packages/e2e-tests/test-applications/hono-4/package.json index 2cec8fc563bb..2e91f861c35e 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/package.json +++ b/dev-packages/e2e-tests/test-applications/hono-4/package.json @@ -9,7 +9,11 @@ "dev:bun": "bun src/entry.bun.ts", "build": "wrangler deploy --dry-run", "test:build": "pnpm install && pnpm build", - "test:assert": "TEST_ENV=production playwright test" + "test:build:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "test:assert": "TEST_ENV=production playwright test", + "test:assert:node": "RUNTIME=node pnpm test:assert", + "test:assert:bun": "RUNTIME=bun pnpm test:assert", + "test:assert:sentry-trace-provider": "RUNTIME=node E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert" }, "dependencies": { "@sentry/bun": "latest || *", @@ -34,12 +38,17 @@ "sentryTest": { "variants": [ { - "assert-command": "RUNTIME=node pnpm test:assert", + "assert-command": "pnpm test:assert:node", "label": "hono-4 (node)" }, { - "assert-command": "RUNTIME=bun pnpm test:assert", + "assert-command": "pnpm test:assert:bun", "label": "hono-4 (bun)" + }, + { + "build-command": "pnpm test:build:sentry-trace-provider", + "assert-command": "pnpm test:assert:sentry-trace-provider", + "label": "hono-4 (node, sentry-trace-provider)" } ] } diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts index 82f2a3864125..aa70d0a1ace9 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/instrument.node.ts @@ -4,5 +4,12 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, environment: 'qa', tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json index 2a230d9d5a68..3562a5beca55 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json @@ -49,5 +49,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-11 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/instrument.ts index 4f16ebb36d11..439b222f1229 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/src/instrument.ts @@ -5,6 +5,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json index 3bd765774d2e..c38e541d37e5 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json @@ -43,5 +43,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-8 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/instrument.ts index 4f16ebb36d11..439b222f1229 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-8/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/instrument.ts @@ -5,6 +5,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json index e429f8cbb328..263ac0775513 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json @@ -45,5 +45,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-basic-with-graphql (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/src/instrument.ts index f1f4de865435..b7ca310219a8 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/src/instrument.ts @@ -5,4 +5,11 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json index 0640e39f77a1..76275f603133 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json @@ -44,5 +44,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-basic (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts index 4f16ebb36d11..439b222f1229 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts @@ -5,6 +5,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/nestjs-bullmq/package.json b/dev-packages/e2e-tests/test-applications/nestjs-bullmq/package.json index c4cfcd118f53..7a5351a1e945 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-bullmq/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-bullmq/package.json @@ -32,5 +32,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-bullmq (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-bullmq/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-bullmq/src/instrument.ts index 4f16ebb36d11..439b222f1229 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-bullmq/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-bullmq/src/instrument.ts @@ -5,6 +5,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json index c8fe82cff563..acb9281e1123 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json @@ -42,5 +42,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-distributed-tracing (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts index 1cf7b8ee1f76..34864c22d67f 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts @@ -5,6 +5,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], transportOptions: { // We expect the app to send a lot of events in a short time diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json index 720cfe158eae..e644c81d64a9 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json @@ -43,5 +43,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-fastify (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts index 4f16ebb36d11..439b222f1229 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts @@ -5,6 +5,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json index 05a38d691807..130d18ba33ce 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json @@ -45,5 +45,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-graphql (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/instrument.ts index f1f4de865435..b7ca310219a8 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/instrument.ts @@ -5,4 +5,11 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json b/dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json index 4bfc4eee7710..3f0d8619ec21 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json @@ -35,5 +35,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-microservices (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/instrument.ts index e0a1cead1153..42a1c592640e 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/src/instrument.ts @@ -5,6 +5,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { bufferSize: 1000, }, diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json b/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json index c859d4e49791..2860e378eacf 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json @@ -30,5 +30,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-websockets (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/instrument.ts index e0a1cead1153..42a1c592640e 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/instrument.ts @@ -5,6 +5,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { bufferSize: 1000, }, diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json index 35ce0bc009e1..9b6a84f72ce3 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json @@ -41,5 +41,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-with-submodules-decorator (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/instrument.ts index 4f16ebb36d11..439b222f1229 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/instrument.ts @@ -5,6 +5,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json index e9da4c97ae26..9351fffd671a 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json @@ -41,5 +41,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nestjs-with-submodules (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts index 4f16ebb36d11..439b222f1229 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts @@ -5,6 +5,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts index 979e0f0abedb..b70657c5724e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts @@ -7,6 +7,13 @@ export function register() { dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.NEXT_RUNTIME === 'nodejs' && process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), dataCollection: { userInfo: true }, transportOptions: { // We are doing a lot of events at once in this test app diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json index f6137db6843c..82c9562d515c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json @@ -34,6 +34,13 @@ "build-command": "pnpm test:build-latest", "label": "nextjs-13 (latest)" } + ], + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-13 (sentry-trace-provider)" + } ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/instrumentation.ts index 05811c256c2d..bfbe839e6883 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/instrumentation.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/instrumentation.ts @@ -7,6 +7,13 @@ export function register() { dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, + ...(process.env.NEXT_RUNTIME === 'nodejs' && process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), dataCollection: { userInfo: true }, transportOptions: { // We are doing a lot of events at once in this test diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json index 09a928eddfa0..bf64ca2ce398 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json @@ -40,6 +40,13 @@ "build-command": "pnpm test:build-latest", "label": "nextjs-14 (latest)" } + ], + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-14 (sentry-trace-provider)" + } ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json index ce63c62aeefa..2c42348c803b 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json @@ -26,5 +26,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-15-basepath (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/sentry.server.config.ts index 6966e28cacb0..6dcc10359469 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/sentry.server.config.ts @@ -6,6 +6,13 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, dataCollection: { userInfo: true }, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { // We are doing a lot of events at once in this test bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json index d935f67fa39e..6bbf0dd6c927 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json @@ -27,5 +27,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-15-intl (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.server.config.ts index e0bc14d47ffd..33c091a5576d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/sentry.server.config.ts @@ -6,6 +6,13 @@ Sentry.init({ tunnel: `http://localhost:3031/`, tracesSampleRate: 1.0, dataCollection: { userInfo: true }, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { bufferSize: 1000, }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json index 24fde175baea..2cfd893adcae 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json @@ -46,5 +46,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-15-t3 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/sentry.server.config.ts index ad780407a5b7..5d5953afc52c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/sentry.server.config.ts @@ -5,4 +5,11 @@ Sentry.init({ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json index 25fc16f8702e..2cca87cd3542 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json @@ -44,6 +44,13 @@ "assert-command": "pnpm test:prod-turbo && pnpm test:dev-turbo", "label": "nextjs-15 (turbo)" } + ], + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-15 (sentry-trace-provider)" + } ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts index a5e60d7787c6..909898ca251a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts @@ -6,6 +6,13 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, dataCollection: { userInfo: true }, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { // We are doing a lot of events at once in this test bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json index 22beb292ca79..2035cfc1f4e6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json @@ -46,6 +46,13 @@ "extends": "../../package.json" }, "sentryTest": { - "//": "TODO: Add variants for webpack once supported" + "//": "TODO: Add variants for webpack once supported", + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-16-cacheComponents (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/sentry.server.config.ts index 737bfa3e9ec7..7edcadf0f035 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/sentry.server.config.ts @@ -6,6 +6,13 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, dataCollection: { userInfo: true }, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), // debug: true, integrations: [Sentry.vercelAIIntegration()], }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json index 4fbc3bf64c27..79fd792e7a2b 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json @@ -37,5 +37,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-16-streaming (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/sentry.server.config.ts index 924844386908..9528ab9adabb 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/sentry.server.config.ts @@ -7,6 +7,13 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, dataCollection: { userInfo: true }, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), traceLifecycle: 'stream', integrations: [ Sentry.vercelAIIntegration(), diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json index 98896eae5d1b..8d6e6c885928 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json @@ -37,6 +37,11 @@ { "build-command": "pnpm test:build-latest", "label": "nextjs-16-trailing-slash (latest, turbopack)" + }, + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-16-trailing-slash (sentry-trace-provider)" } ] } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/sentry.server.config.ts index 11e0e9e3c30f..8a7cb4dbe34b 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/sentry.server.config.ts @@ -6,4 +6,11 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, dataCollection: { userInfo: true }, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json index 7dfa8f923f6e..ca21224df739 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json @@ -57,6 +57,11 @@ "build-command": "pnpm test:build", "label": "nextjs-16-tunnel (turbopack)", "assert-command": "pnpm test:assert" + }, + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-16-tunnel (sentry-trace-provider)" } ] } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts index 98d677005900..4daf5b6842db 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.server.config.ts @@ -7,5 +7,12 @@ Sentry.init({ // No tunnel option - using tunnelRoute from withSentryConfig tracesSampleRate: 1.0, dataCollection: { userInfo: true }, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), // debug: true, }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/package.json index b30636cd3576..e58498572ded 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/package.json @@ -28,5 +28,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-16-userfeedback (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/sentry.server.config.ts index c6de72d3a791..03f4e458589e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/sentry.server.config.ts @@ -6,6 +6,13 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, dataCollection: { userInfo: true }, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { bufferSize: 1000, }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json index beda2252d915..b99f9e7a8d02 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json @@ -62,6 +62,11 @@ { "build-command": "pnpm test:build-latest", "label": "nextjs-16 (latest, turbopack)" + }, + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-16 (sentry-trace-provider)" } ], "optionalVariants": [ diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts index 8b9eaa651f6d..6dd064f20bb1 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts @@ -7,6 +7,13 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, dataCollection: { userInfo: true }, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), // debug: true, integrations: [Sentry.vercelAIIntegration(), Sentry.nodeRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 })], streamGenAiSpans: true, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/instrumentation.ts index 2f72cbf2ad75..b3de0ecd3786 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/instrumentation.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/instrumentation.ts @@ -7,6 +7,13 @@ export function register() { dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, + ...(process.env.NEXT_RUNTIME === 'nodejs' && process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), dataCollection: { userInfo: true }, transportOptions: { // We are doing a lot of events at once in this test diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json index cb7927e9b0d8..7f1be88ebe99 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -41,6 +41,11 @@ { "build-command": "pnpm test:build-15", "label": "nextjs-app-dir (next@15)" + }, + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-app-dir (sentry-trace-provider)" } ] } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json index 21f835e2ecd4..de89d8aa8c42 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json @@ -43,6 +43,13 @@ "extends": "../../package.json" }, "sentryTest": { - "optional": true + "optional": true, + "optionalVariants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-orpc (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-orpc/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-orpc/sentry.server.config.ts index ad780407a5b7..5d5953afc52c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-orpc/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-orpc/sentry.server.config.ts @@ -5,4 +5,11 @@ Sentry.init({ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/instrumentation.ts index 2f72cbf2ad75..b3de0ecd3786 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/instrumentation.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/instrumentation.ts @@ -7,6 +7,13 @@ export function register() { dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, + ...(process.env.NEXT_RUNTIME === 'nodejs' && process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), dataCollection: { userInfo: true }, transportOptions: { // We are doing a lot of events at once in this test diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json index f677e02dd954..d8cfe9d59e16 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json @@ -42,6 +42,11 @@ { "build-command": "pnpm test:build-15", "label": "nextjs-pages-dir (next@15)" + }, + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nextjs-pages-dir (sentry-trace-provider)" } ], "optionalVariants": [] diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs b/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs index 53b80d309a5b..cc5fa7028c51 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs @@ -5,4 +5,11 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/package.json b/dev-packages/e2e-tests/test-applications/nitro-3/package.json index 85713dccc138..58c8b9f5b14e 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nitro-3/package.json @@ -25,5 +25,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nitro-3 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-connect/package.json b/dev-packages/e2e-tests/test-applications/node-connect/package.json index 729cfbe6c095..8e4d515b6765 100644 --- a/dev-packages/e2e-tests/test-applications/node-connect/package.json +++ b/dev-packages/e2e-tests/test-applications/node-connect/package.json @@ -24,5 +24,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-connect (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-connect/src/app.ts b/dev-packages/e2e-tests/test-applications/node-connect/src/app.ts index 375554845d6f..a3026c71a244 100644 --- a/dev-packages/e2e-tests/test-applications/node-connect/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-connect/src/app.ts @@ -6,6 +6,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, integrations: [], tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], }); diff --git a/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts index 9b06ad052f58..9dca6119b360 100644 --- a/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts @@ -1,6 +1,8 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; +const useSentryTraceProvider = process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1'; + test('Sends an API route transaction', async ({ baseURL }) => { const pageloadTransactionEventPromise = waitForTransaction('node-connect', transactionEvent => { return ( @@ -54,41 +56,51 @@ test('Sends an API route transaction', async ({ baseURL }) => { origin: 'auto.http.otel.http', }); + const manualSpanExpectation = { + data: { + 'sentry.origin': 'manual', + }, + description: 'test-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'manual', + }; + + const connectSpanExpectation = { + data: { + 'sentry.origin': 'auto.http.otel.connect', + 'sentry.op': 'request_handler.connect', + 'http.route': '/test-transaction', + 'connect.type': 'request_handler', + 'connect.name': '/test-transaction', + }, + op: 'request_handler.connect', + description: '/test-transaction', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.http.otel.connect', + }; + expect(transactionEvent).toEqual( expect.objectContaining({ - spans: [ - { - data: { - 'sentry.origin': 'manual', - }, - description: 'test-span', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'manual', - }, - { - data: { - 'sentry.origin': 'auto.http.otel.connect', - 'sentry.op': 'request_handler.connect', - 'http.route': '/test-transaction', - 'connect.type': 'request_handler', - 'connect.name': '/test-transaction', - }, - op: 'request_handler.connect', - description: '/test-transaction', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.http.otel.connect', - }, - ], + spans: useSentryTraceProvider + ? [ + // TODO: Investigate whether transaction span array ordering is expected to stay + // compatible with the legacy OTel exporter path. SentryTraceProvider serializes + // native child spans in start/tree order, so the Connect handler span appears + // before the manual span created inside it. + connectSpanExpectation, + manualSpanExpectation, + ] + : [manualSpanExpectation, connectSpanExpectation], transaction: 'GET /test-transaction', type: 'transaction', transaction_info: { diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json index 125372c4501a..696dddbaeed0 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json @@ -19,5 +19,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-express-cjs-preload (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/src/app.js b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/src/app.js index b41d99ab6440..3f6278256b00 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/src/app.js +++ b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/src/app.js @@ -42,6 +42,13 @@ async function run() { dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); app.listen(port, () => { diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json index a2d2c720e92a..5fb064435314 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json @@ -19,5 +19,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-express-esm-loader (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/instrument.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/instrument.mjs index 544c773e5e7c..f6afd668b278 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/instrument.mjs @@ -5,4 +5,11 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json index 4a602b6bd304..6397a953d3c6 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json @@ -19,5 +19,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-express-esm-preload (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs index cc680866ab1a..69e4f57d5783 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs @@ -57,6 +57,13 @@ async function run() { dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); app.listen(port, () => { diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json index 84afe281c642..0f98c5dadf89 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json @@ -26,5 +26,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-express-incorrect-instrumentation (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/src/app.ts index 2ab5d1ace5a0..41c28299f819 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/src/app.ts @@ -18,6 +18,13 @@ Sentry.init({ debug: !!process.env.DEBUG, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); app.get('/test-exception/:id', function (req, _res) { diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/instrument.mjs b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/instrument.mjs index f3dd95215d03..39e5b0f5bdaf 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/instrument.mjs @@ -6,4 +6,11 @@ Sentry.init({ debug: !!process.env.DEBUG, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json index 4460adbd034c..52cada285930 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json @@ -32,6 +32,13 @@ "extends": "../../package.json" }, "sentryTest": { - "optional": true + "optional": true, + "optionalVariants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-express-mcp-v2 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json index e5ec85096dce..b0f57c15c571 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json @@ -24,6 +24,13 @@ "extends": "../../package.json" }, "sentryTest": { - "optional": true + "optional": true, + "optionalVariants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-express-send-to-sentry (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/src/app.ts index ca5d61f742d9..fa39a66779f7 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/src/app.ts @@ -7,6 +7,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, includeLocalVariables: true, tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), beforeSendTransaction(event) { lastTransactionId = event.event_id; return event; diff --git a/dev-packages/e2e-tests/test-applications/node-express-streaming/package.json b/dev-packages/e2e-tests/test-applications/node-express-streaming/package.json index 77124040ff6f..44783dee360e 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-streaming/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-streaming/package.json @@ -31,5 +31,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-express-streaming (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-express-streaming/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-streaming/src/app.ts index 5a0d1afa4141..49cfb19d9a19 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-streaming/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-streaming/src/app.ts @@ -7,6 +7,13 @@ Sentry.init({ debug: !!process.env.DEBUG, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), enableLogs: true, traceLifecycle: 'stream', integrations: [ diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/package.json b/dev-packages/e2e-tests/test-applications/node-express-v5/package.json index cf33b86e8669..0a7a415876ce 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/package.json @@ -31,5 +31,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-express-v5 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts index 9a7f6f07d8bc..ef59f1449d5b 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts @@ -14,6 +14,13 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, enableLogs: true, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), integrations: [Sentry.nodeRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 })], }); diff --git a/dev-packages/e2e-tests/test-applications/node-express/package.json b/dev-packages/e2e-tests/test-applications/node-express/package.json index 4d2ad1833a58..78580049f846 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express/package.json @@ -31,5 +31,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-express (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts index dc755f95d062..f429777e6767 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts @@ -14,6 +14,13 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, enableLogs: true, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), integrations: [ Sentry.nativeNodeFetchIntegration({ headersToSpanAttributes: { diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json index 3fa36adbbbd5..c6e944333a65 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json @@ -25,5 +25,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-fastify-3 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts index 378ef99fa309..3f7b06aafe11 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts @@ -25,6 +25,13 @@ Sentry.init({ }), ], tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts index 5b4a2f0d16ac..ad057a278584 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts @@ -31,6 +31,13 @@ Sentry.init({ }), ], tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json index 086ec85fac7a..c43933b8baae 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json @@ -25,5 +25,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-fastify-4 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts index 72270efc05de..0287292a710b 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts @@ -25,6 +25,13 @@ Sentry.init({ }), ], tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts index 1c428c0486f9..47a98e1a9964 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts @@ -30,6 +30,13 @@ Sentry.init({ }), ], tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json index dc0fa7770c70..92163b2552ef 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json @@ -25,5 +25,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-fastify-5 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts index 91f0353816bb..39921c6b6ecc 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts @@ -25,6 +25,13 @@ Sentry.init({ }), ], tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts index 610a42f6fc00..71a5c0cd8038 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts @@ -35,6 +35,13 @@ Sentry.init({ }), ], tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], }); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/init.ts b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/init.ts index c3b4a642375a..13c387c15520 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/init.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/init.ts @@ -4,6 +4,13 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), integrations: [Sentry.firebaseIntegration()], defaultIntegrations: false, tunnel: `http://localhost:3031/`, // proxy server diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts index c3b4a642375a..13c387c15520 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts @@ -4,6 +4,13 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), integrations: [Sentry.firebaseIntegration()], defaultIntegrations: false, tunnel: `http://localhost:3031/`, // proxy server diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/package.json index a1d4965e9745..0243e906cb6e 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/package.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/package.json @@ -26,5 +26,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-firebase (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/package.json b/dev-packages/e2e-tests/test-applications/node-hapi/package.json index ae87544644bf..e5675084d44a 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/package.json +++ b/dev-packages/e2e-tests/test-applications/node-hapi/package.json @@ -21,5 +21,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-hapi (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js b/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js index 8b68e8412aba..663c0086609f 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js +++ b/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js @@ -7,6 +7,13 @@ Sentry.init({ debug: !!process.env.DEBUG, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); const Hapi = require('@hapi/hapi'); diff --git a/dev-packages/e2e-tests/test-applications/node-koa/index.js b/dev-packages/e2e-tests/test-applications/node-koa/index.js index 9e800a4fcc99..35b705c2a075 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/index.js +++ b/dev-packages/e2e-tests/test-applications/node-koa/index.js @@ -7,6 +7,13 @@ Sentry.init({ debug: !!process.env.DEBUG, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tracePropagationTargets: ['http://localhost:3030', 'external-allowed'], }); diff --git a/dev-packages/e2e-tests/test-applications/node-koa/package.json b/dev-packages/e2e-tests/test-applications/node-koa/package.json index f4ef47cd0940..7c22dfe4df57 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/package.json +++ b/dev-packages/e2e-tests/test-applications/node-koa/package.json @@ -23,5 +23,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-koa (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/index.ts b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/index.ts index e956a1d9de33..85da32bd4e9d 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/index.ts +++ b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/index.ts @@ -7,6 +7,13 @@ Sentry.init({ dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', integrations: [nodeProfilingIntegration()], tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), profilesSampleRate: 1.0, }); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json index b136ea49dd4c..cc3c3a4e246d 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json @@ -19,5 +19,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-profiling-cjs (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-esm/index.ts b/dev-packages/e2e-tests/test-applications/node-profiling-esm/index.ts index e956a1d9de33..85da32bd4e9d 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling-esm/index.ts +++ b/dev-packages/e2e-tests/test-applications/node-profiling-esm/index.ts @@ -7,6 +7,13 @@ Sentry.init({ dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', integrations: [nodeProfilingIntegration()], tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), profilesSampleRate: 1.0, }); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json index c7e5c39b9807..cec0c2a9d176 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json @@ -19,5 +19,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "node-profiling-esm (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json index a61e1da1bdcd..a472a47537f9 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json @@ -23,5 +23,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nuxt-3-dynamic-import (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.server.config.ts index 729b2296c683..eb662e854b22 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.server.config.ts @@ -4,5 +4,12 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, // Capture 100% of the transactions + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json index 73b0c59e8a24..cfdefe2ad209 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json @@ -33,5 +33,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nuxt-3-min (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.server.config.ts index 729b2296c683..eb662e854b22 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.server.config.ts @@ -4,5 +4,12 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, // Capture 100% of the transactions + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json index 21acb5644735..770b7dbb125b 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json @@ -24,5 +24,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "nuxt-3-top-level-import (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts index f08dea23ae03..33cd7fc4928d 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts @@ -4,6 +4,13 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, // Capture 100% of the transactions + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server debug: !!process.env.DEBUG, }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json index b7481e044b3e..dd2d3f7a98b9 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json @@ -12,8 +12,10 @@ "clean": "npx nuxi cleanup", "test": "playwright test", "test:build": "pnpm install && pnpm build", + "test:build:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", "test:build-canary": "pnpm add nuxt@npm:nuxt-nightly@3x && pnpm add nitropack@npm:nitropack-nightly@latest && pnpm install --force && pnpm build", - "test:assert": "pnpm test" + "test:assert": "pnpm test", + "test:assert:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert" }, "dependencies": { "@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz", @@ -30,6 +32,13 @@ "build-command": "pnpm test:build-canary", "label": "nuxt-3 (canary)" } + ], + "variants": [ + { + "build-command": "pnpm test:build:sentry-trace-provider", + "assert-command": "pnpm test:assert:sentry-trace-provider", + "label": "nuxt-3 (sentry-trace-provider)" + } ] }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts index e04331934f99..a3ee4c3a9ced 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts @@ -4,6 +4,13 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, // Capture 100% of the transactions + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server enableNitroErrorHandler: false, // Error handler is defined in server/plugins/customNitroErrorHandler.ts }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json index 02477111483d..cb6d446911b2 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json @@ -14,8 +14,10 @@ "test:prod": "TEST_ENV=production playwright test", "test:dev": "bash ./nuxt-start-dev-server.bash && TEST_ENV=development playwright test environment", "test:build": "pnpm install && pnpm build", + "test:build:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", "test:build-canary": "pnpm add nuxt@npm:nuxt-nightly@latest && pnpm add nitropack@npm:nitropack-nightly@latest && pnpm install --force && pnpm build", - "test:assert": "pnpm test:prod && pnpm test:dev" + "test:assert": "pnpm test:prod && pnpm test:dev", + "test:assert:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert" }, "dependencies": { "@pinia/nuxt": "^0.5.5", @@ -36,6 +38,13 @@ "build-command": "pnpm test:build-canary", "label": "nuxt-4 (canary)" } + ], + "variants": [ + { + "build-command": "pnpm test:build:sentry-trace-provider", + "assert-command": "pnpm test:assert:sentry-trace-provider", + "label": "nuxt-4 (sentry-trace-provider)" + } ] } } diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts index 26519911072b..228f0dcccbd1 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts @@ -3,5 +3,12 @@ import * as Sentry from '@sentry/nuxt'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, // Capture 100% of the transactions + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/package.json b/dev-packages/e2e-tests/test-applications/nuxt-5/package.json index bff128f66ed9..2428915bf3f6 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/package.json @@ -14,8 +14,10 @@ "test:prod": "TEST_ENV=production playwright test", "test:dev": "bash ./nuxt-start-dev-server.bash && TEST_ENV=development playwright test environment", "test:build": "pnpm install && pnpm build", + "test:build:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", "test:build-canary": "pnpm add nuxt@npm:nuxt-nightly@latest && pnpm add nitro@npm:nitro-nightly@latest && pnpm install --force && pnpm build", - "test:assert": "pnpm test:prod && pnpm test:dev" + "test:assert": "pnpm test:prod && pnpm test:dev", + "test:assert:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert" }, "//": [ "Currently, we need to install the latest version of Nitro and the Nuxt nightlies as those contain Nuxt v5", @@ -32,7 +34,14 @@ "@sentry-internal/test-utils": "link:../../../test-utils" }, "sentryTest": { - "optional": true + "optional": true, + "optionalVariants": [ + { + "build-command": "pnpm test:build:sentry-trace-provider", + "assert-command": "pnpm test:assert:sentry-trace-provider", + "label": "nuxt-5 (sentry-trace-provider)" + } + ] }, "volta": { "extends": "../../package.json", diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/sentry.server.config.ts index 26519911072b..228f0dcccbd1 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/sentry.server.config.ts @@ -3,5 +3,12 @@ import * as Sentry from '@sentry/nuxt'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, // Capture 100% of the transactions + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/instrument.mjs index c16240141b6d..48a130bd25d6 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/instrument.mjs @@ -4,5 +4,12 @@ Sentry.init({ dsn: 'https://username@domain/123', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: `http://localhost:3031/`, // proxy server }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json index 20fdccf46f4c..4c76a7bc3eb6 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json @@ -54,5 +54,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "react-router-7-framework-custom (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs index bb1dad2e5da9..22e362a9b1bd 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs @@ -6,5 +6,12 @@ Sentry.init({ dsn: 'https://username@domain/123', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: `http://localhost:3031/`, // proxy server }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json index b7e2fd8de655..0b4852abbd0c 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json @@ -56,6 +56,13 @@ "extends": "../../package.json" }, "sentryTest": { - "optional": true + "optional": true, + "optionalVariants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "react-router-7-framework-instrumentation (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/instrument.mjs index 48e4b7b61ff3..aef6f9cb866e 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/instrument.mjs @@ -4,5 +4,12 @@ Sentry.init({ dsn: 'https://username@domain/123', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: `http://localhost:3031/`, // proxy server, }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json index 65f4a96b0165..91b44e1e4aac 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json @@ -30,7 +30,9 @@ "typecheck": "react-router typegen && tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:build": "pnpm install && pnpm build", + "test:build:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", "test:assert": "pnpm test:ts && pnpm test:playwright", + "test:assert:sentry-trace-provider": "E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", "test:ts": "pnpm typecheck", "test:playwright": "playwright test" }, @@ -60,5 +62,14 @@ "overrides": { "p-map": "^4.0.0" } + }, + "sentryTest": { + "variants": [ + { + "build-command": "pnpm test:build:sentry-trace-provider", + "assert-command": "pnpm test:assert:sentry-trace-provider", + "label": "react-router-7-framework-node-20-18 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/with-middleware.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/with-middleware.tsx index c86f78e17164..e0cd8c004cf5 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/with-middleware.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/with-middleware.tsx @@ -12,7 +12,8 @@ async function getUser() { } const authMiddleware: Route.MiddlewareFunction = async ({ request, context }, next) => { - Sentry.startSpan({ name: 'authMiddleware', op: 'middleware.auth' }, async () => { + // React Router middleware must keep the async `next()` chain alive until it completes. + await Sentry.startSpan({ name: 'authMiddleware', op: 'middleware.auth' }, async () => { const user: User = await getUser(); context.set(userContext, user); await next(); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs index c16240141b6d..48a130bd25d6 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs @@ -4,5 +4,12 @@ Sentry.init({ dsn: 'https://username@domain/123', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: `http://localhost:3031/`, // proxy server }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json index fde0e1699d6a..539d0ae3e980 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json @@ -61,6 +61,11 @@ { "build-command": "pnpm test:build-latest", "label": "react-router-7-framework (latest)" + }, + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "react-router-7-framework (sentry-trace-provider)" } ] } diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/middleware.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/middleware.server.test.ts index dbce05350ad9..971541eb7536 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/middleware.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/middleware.server.test.ts @@ -12,27 +12,28 @@ test.describe('server - middleware', () => { return transactionEvent.transaction === '/performance/with-middleware'; }); - const customMiddlewareTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'authMiddleware'; - }); - await page.goto(`/performance/with-middleware`); const serverTx = await serverTxPromise; const pageloadTx = await pageloadTxPromise; - const customMiddlewareTx = await customMiddlewareTxPromise; const traceIds = { server: serverTx?.contexts?.trace?.trace_id, pageload: pageloadTx?.contexts?.trace?.trace_id, - customMiddleware: customMiddlewareTx?.contexts?.trace?.trace_id, }; expect(pageloadTx).toBeDefined(); - expect(customMiddlewareTx).toBeDefined(); + + // The app awaits Sentry.startSpan around the middleware `next()` call, so the manual + // middleware span belongs to the request transaction instead of becoming a root transaction. + const customMiddlewareSpans = + serverTx.spans?.filter(span => { + return span.description === 'authMiddleware' && span.op === 'middleware.auth'; + }) ?? []; + + expect(customMiddlewareSpans).toHaveLength(1); // Assert that all transactions belong to the same trace expect(traceIds.server).toBe(traceIds.pageload); - expect(traceIds.server).toBe(traceIds.customMiddleware); }); }); diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/instrument.server.cjs b/dev-packages/e2e-tests/test-applications/remix-server-timing/instrument.server.cjs index 6d211cac4592..023cd91bd8cd 100644 --- a/dev-packages/e2e-tests/test-applications/remix-server-timing/instrument.server.cjs +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/instrument.server.cjs @@ -2,6 +2,13 @@ const Sentry = require('@sentry/remix'); Sentry.init({ tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, tunnel: 'http://localhost:3031/', // proxy server diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json b/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json index fd57d2920d5a..d0f4b3f96fe5 100644 --- a/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json @@ -38,5 +38,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "remix-server-timing (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json index 747162d0bd75..77379c98288d 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json @@ -34,5 +34,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "solidstart-dynamic-import (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts index 3dd5d8933b7b..16897c2903d0 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts @@ -4,6 +4,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, // Capture 100% of the transactions + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server debug: !!process.env.DEBUG, }); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json index a9d1d6b91da3..cf2f7db4f0f9 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json @@ -34,5 +34,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "solidstart-spa (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.ts index 3dd5d8933b7b..16897c2903d0 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.ts @@ -4,6 +4,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, // Capture 100% of the transactions + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server debug: !!process.env.DEBUG, }); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json index c97a130c92b1..fb4a82cddac2 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json @@ -34,5 +34,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "solidstart-top-level-import (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts index 3dd5d8933b7b..16897c2903d0 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts @@ -4,6 +4,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, // Capture 100% of the transactions + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server debug: !!process.env.DEBUG, }); diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json index 7e382b6dc54b..f7e00d311aad 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json @@ -34,5 +34,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "solidstart (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.ts index 3dd5d8933b7b..16897c2903d0 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.ts @@ -4,6 +4,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, // Capture 100% of the transactions + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server debug: !!process.env.DEBUG, }); diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json index c5c86c0d31ae..a572a2780c16 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -36,5 +36,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "supabase-nextjs (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts index 3992352ec961..7201d5d081e1 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/sentry.server.config.ts @@ -9,6 +9,13 @@ Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1, dataCollection: { userInfo: true }, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), tunnel: 'http://localhost:3031/', // proxy server transportOptions: { // We expect the app to send a lot of events in a short time diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json index 12f39178da15..61bb0f73ec40 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json @@ -33,5 +33,14 @@ "type": "module", "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "sveltekit-2-kit-tracing (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/instrumentation.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/instrumentation.server.ts index 136b51a44dee..8c3ea06e3a5f 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/instrumentation.server.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/instrumentation.server.ts @@ -7,5 +7,12 @@ Sentry.init({ debug: !!process.env.DEBUG, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), spotlight: import.meta.env.DEV, }); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json index 50fd974e98b9..b3986dc72a02 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json @@ -33,5 +33,14 @@ "type": "module", "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "sveltekit-2-svelte-5 (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/src/hooks.server.ts index 99bf4a17aa96..ab30c4682fd7 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/src/hooks.server.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/src/hooks.server.ts @@ -8,6 +8,13 @@ Sentry.init({ debug: !!process.env.DEBUG, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), spotlight: import.meta.env.DEV, }); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json index 23f059eaee43..a83c53ee5f82 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json @@ -32,5 +32,14 @@ "type": "module", "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "sveltekit-2.5.0-twp (sentry-trace-provider)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/src/hooks.server.ts index e60e51b25968..4292c5b676b0 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/src/hooks.server.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/src/hooks.server.ts @@ -5,6 +5,13 @@ Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); // not logging anything to console to avoid noise in the test output diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json index 75b1967d802f..538a51fbabc9 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -32,5 +32,14 @@ "volta": { "extends": "../../package.json" }, - "type": "module" + "type": "module", + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "sveltekit-2 (sentry-trace-provider)" + } + ] + } } diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts index 92909c53a24c..719a12efc391 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts @@ -7,6 +7,13 @@ Sentry.init({ debug: !!process.env.DEBUG, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), }); // not logging anything to console to avoid noise in the test output diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/tanstackstart-react/instrument.server.mjs index 8bc20de7578b..a607a0046c5b 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/instrument.server.mjs +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/instrument.server.mjs @@ -5,6 +5,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), transportOptions: { // We expect the app to send a lot of events in a short time bufferSize: 1000, diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json index 0525acfad587..910d2586b994 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json @@ -66,6 +66,11 @@ "label": "tanstackstart-react (tunnel-object)", "build-command": "pnpm test:build:tunnel-object", "assert-command": "pnpm test:assert:tunnel-object" + }, + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "tanstackstart-react (sentry-trace-provider)" } ] } diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/instrument.mjs b/dev-packages/e2e-tests/test-applications/tsx-express/instrument.mjs index ddc96c7c17fc..809ca41f133b 100644 --- a/dev-packages/e2e-tests/test-applications/tsx-express/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/tsx-express/instrument.mjs @@ -6,5 +6,12 @@ Sentry.init({ debug: !!process.env.DEBUG, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + ...(process.env.E2E_USE_SENTRY_TRACE_PROVIDER === '1' + ? { + _experiments: { + useSentryTraceProvider: true, + }, + } + : {}), enableLogs: true, }); diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/package.json b/dev-packages/e2e-tests/test-applications/tsx-express/package.json index 7794d2c7ac52..d12206ae9ceb 100644 --- a/dev-packages/e2e-tests/test-applications/tsx-express/package.json +++ b/dev-packages/e2e-tests/test-applications/tsx-express/package.json @@ -31,5 +31,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:build", + "assert-command": "env E2E_USE_SENTRY_TRACE_PROVIDER=1 pnpm test:assert", + "label": "tsx-express (sentry-trace-provider)" + } + ] } } From df9f187dcd042a97d06187d7afe7b252b8a87954 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 3 Jun 2026 15:26:34 +0200 Subject: [PATCH 06/25] Noop recordException, use core's getActiveSpan in setupEventContextTrace --- packages/core/src/tracing/sentrySpan.ts | 15 ++------- packages/node/src/sdk/initOtel.ts | 20 +---------- .../opentelemetry/src/asyncContextStrategy.ts | 2 -- packages/opentelemetry/src/exports.ts | 2 +- .../opentelemetry/src/sentryTraceProvider.ts | 33 +------------------ .../src/setupEventContextTrace.ts | 3 +- .../test/sentryTraceProvider.test.ts | 29 ++-------------- .../test/utils/setupEventContextTrace.test.ts | 2 ++ 8 files changed, 11 insertions(+), 95 deletions(-) diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 9271c5c855f8..741368be24c3 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -143,19 +143,8 @@ export class SentrySpan implements Span { * @hidden * @internal */ - public recordException(exception: unknown, time?: SpanTimeInput | undefined): void { - const attributes: SpanAttributes = {}; - - if (typeof exception === 'string') { - attributes['exception.message'] = exception; - } else if (exception && typeof exception === 'object') { - const error = exception as { name?: string; message?: string; stack?: string }; - attributes['exception.type'] = error.name; - attributes['exception.message'] = error.message; - attributes['exception.stacktrace'] = error.stack; - } - - this.addEvent('exception', attributes, time); + public recordException(_exception: unknown, _time?: SpanTimeInput | undefined): void { + // noop } /** @inheritdoc */ diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 9a0e3e05bbe3..ac8f93bd768e 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -1,7 +1,7 @@ import { context, propagation, trace } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import { debug as coreDebug, spanToTraceContext } from '@sentry/core'; +import { debug as coreDebug } from '@sentry/core'; import { initializeEsmLoader, type NodeClient, @@ -9,7 +9,6 @@ import { setupOpenTelemetryLogger, } from '@sentry/node-core'; import { - _INTERNAL_getSpanForRecordedException, applyOtelSpanData, type AsyncLocalStorageLookup, getSentryResource, @@ -150,23 +149,6 @@ function setupSentryTraceProvider( applyOtelSpanData(span, { finalizeStatus: true }); }); - client.addEventProcessor((event, hint) => { - // Some frameworks capture exceptions after the OTel context has already - // unwound. If a provider-created span recorded this exact exception first, - // keep the error event linked to that span instead of the ambient parent. - const span = _INTERNAL_getSpanForRecordedException(hint.originalException); - if (!span) { - return event; - } - - event.contexts = { - ...event.contexts, - trace: spanToTraceContext(span), - }; - - return event; - }); - client.on('preprocessEvent', event => { if (event.type !== 'transaction' || client.getOptions().traceLifecycle === 'stream') { return; diff --git a/packages/opentelemetry/src/asyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts index 95cd41310204..efa0eb27feb0 100644 --- a/packages/opentelemetry/src/asyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -177,8 +177,6 @@ export function setOpenTelemetryContextAsyncContextStrategy( if (!useOpenTelemetrySpanCreation) { setAsyncContextStrategy({ ...baseStrategy, - // Keep OTEL Context and Sentry Scope active-span state in sync, but let - // the core tracing implementation create and send spans. withActiveSpan: withActiveSpanContextOnly as typeof defaultWithActiveSpan, }); return; diff --git a/packages/opentelemetry/src/exports.ts b/packages/opentelemetry/src/exports.ts index 3baa495fd446..cb5f1721d823 100644 --- a/packages/opentelemetry/src/exports.ts +++ b/packages/opentelemetry/src/exports.ts @@ -45,7 +45,7 @@ export { wrapContextManagerClass } from './contextManager'; export { SentryPropagator, shouldPropagateTraceForUrl } from './propagator'; export { SentrySpanProcessor } from './spanProcessor'; export { SentrySampler, wrapSamplingDecision } from './sampler'; -export { _INTERNAL_getSpanForRecordedException, applyOtelSpanData, SentryTraceProvider } from './sentryTraceProvider'; +export { applyOtelSpanData, SentryTraceProvider } from './sentryTraceProvider'; export type { OpenTelemetryTraceProvider } from './types'; export { openTelemetrySetupCheck } from './utils/setupCheck'; diff --git a/packages/opentelemetry/src/sentryTraceProvider.ts b/packages/opentelemetry/src/sentryTraceProvider.ts index 88c9a4fd3cf2..adfff453f4bf 100644 --- a/packages/opentelemetry/src/sentryTraceProvider.ts +++ b/packages/opentelemetry/src/sentryTraceProvider.ts @@ -27,7 +27,7 @@ import { startNewTrace, withScope, } from '@sentry/core'; -import type { Span, SpanAttributes, SpanLink, SpanStatus, SpanTimeInput } from '@sentry/core'; +import type { Span, SpanAttributes, SpanLink, SpanStatus } from '@sentry/core'; import { inferSpanData } from './utils/parseSpanDescription'; import { getSamplingDecision } from './utils/getSamplingDecision'; import { setIsSetup } from './utils/setupCheck'; @@ -36,34 +36,12 @@ type SentrySpanWithOtelKind = Span & { kind?: SpanKind }; type SentrySpanWithOtelSourceInference = Span & { _sentryOtelInferSource?: boolean }; type SentryTraceProviderSpan = Span & { _sentryTraceProviderSpan?: true }; -const recordedExceptionSpans = new WeakMap(); - export function isSentryTraceProviderSpan(span: Span | undefined): boolean { return (span as SentryTraceProviderSpan | undefined)?._sentryTraceProviderSpan === true; } -export function _INTERNAL_getSpanForRecordedException(exception: unknown): Span | undefined { - if (exception === null || (typeof exception !== 'object' && typeof exception !== 'function')) { - return undefined; - } - - return recordedExceptionSpans.get(exception); -} - function markSentryTraceProviderSpan(span: Span): Span { addNonEnumerableProperty(span as SentryTraceProviderSpan, '_sentryTraceProviderSpan', true); - const originalRecordException = span.recordException.bind(span); - addNonEnumerableProperty(span, 'recordException', function (this: Span, exception: unknown, time?: SpanTimeInput) { - if (exception !== null && (typeof exception === 'object' || typeof exception === 'function')) { - // Preserve the closest span that recorded this exception. Frameworks like Nest - // may capture after OTel context has unwound to a parent request span. - if (!recordedExceptionSpans.has(exception)) { - recordedExceptionSpans.set(exception, span); - } - } - - return originalRecordException(exception, time); - }); return span; } @@ -308,15 +286,6 @@ export function applyOtelSpanData(span: Span, options: { finalizeStatus?: boolea }); } - if ( - mayInferSource && - !hasCustomSpanName && - attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom' && - (inferred.source === undefined || inferred.source === 'custom') - ) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, undefined); - } - if (options.finalizeStatus) { applyOtelCompatibilityAttributes(span, attributes); applyOtelSpanStatus(span, attributes, spanJSON.status); diff --git a/packages/opentelemetry/src/setupEventContextTrace.ts b/packages/opentelemetry/src/setupEventContextTrace.ts index 1bf9bcb961be..5d3a3c196361 100644 --- a/packages/opentelemetry/src/setupEventContextTrace.ts +++ b/packages/opentelemetry/src/setupEventContextTrace.ts @@ -1,6 +1,5 @@ import type { Client } from '@sentry/core'; -import { getDynamicSamplingContextFromSpan, getRootSpan, spanToTraceContext } from '@sentry/core'; -import { getActiveSpan } from './utils/getActiveSpan'; +import { getActiveSpan, getDynamicSamplingContextFromSpan, getRootSpan, spanToTraceContext } from '@sentry/core'; /** Ensure the `trace` context is set on all events. */ export function setupEventContextTrace(client: Client): void { diff --git a/packages/opentelemetry/test/sentryTraceProvider.test.ts b/packages/opentelemetry/test/sentryTraceProvider.test.ts index ffb409ef0558..c1e1bce7b9c2 100644 --- a/packages/opentelemetry/test/sentryTraceProvider.test.ts +++ b/packages/opentelemetry/test/sentryTraceProvider.test.ts @@ -12,11 +12,7 @@ import { import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { SentryAsyncLocalStorageContextManager } from '../src/asyncLocalStorageContextManager'; import { setOpenTelemetryContextAsyncContextStrategy } from '../src/asyncContextStrategy'; -import { - _INTERNAL_getSpanForRecordedException, - applyOtelSpanData, - SentryTraceProvider, -} from '../src/sentryTraceProvider'; +import { applyOtelSpanData, SentryTraceProvider } from '../src/sentryTraceProvider'; import { cleanupOtel } from './helpers/mockSdkInit'; import { init as initTestClient } from './helpers/TestClient'; @@ -170,25 +166,6 @@ describe('SentryTraceProvider', () => { expect(json.data?.['otel.kind']).toBe('SERVER'); }); - it('remembers the closest provider-created span that recorded an exception', () => { - const error = new Error('test error'); - - trace.getTracer('test').startActiveSpan('outer', outer => { - let innerSpan: unknown; - - trace.getTracer('test').startActiveSpan('inner', inner => { - innerSpan = inner; - inner.recordException(error); - inner.end(); - }); - - outer.recordException(error); - - expect(_INTERNAL_getSpanForRecordedException(error)).toBe(innerSpan); - outer.end(); - }); - }); - it('finalizes span statuses like the OpenTelemetry exporter', () => { const okSpan = trace.getTracer('test').startSpan('ok'); applyOtelSpanData(okSpan as Span, { finalizeStatus: true }); @@ -214,12 +191,12 @@ describe('SentryTraceProvider', () => { expect(spanToJSON(customErrorSpan as Span).status).toBe('internal_error'); }); - it('does not keep default custom source on provider-created spans', () => { + it('keeps default custom source on provider-created spans', () => { const span = trace.getTracer('test').startSpan('custom-source'); span.setAttribute('sentry.source', 'custom'); applyOtelSpanData(span as Span, { finalizeStatus: true }); - expect(spanToJSON(span as Span).data?.['sentry.source']).toBeUndefined(); + expect(spanToJSON(span as Span).data?.['sentry.source']).toBe('custom'); }); }); diff --git a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts index 19c8e178c160..7285877652fa 100644 --- a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts +++ b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts @@ -1,6 +1,7 @@ import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { captureException, setCurrentClient } from '@sentry/core'; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { setOpenTelemetryContextAsyncContextStrategy } from '../../src/asyncContextStrategy'; import { setupEventContextTrace } from '../../src/setupEventContextTrace'; import { setupOtel } from '../helpers/initOtel'; import { cleanupOtel } from '../helpers/mockSdkInit'; @@ -29,6 +30,7 @@ describe('setupEventContextTrace', () => { client.init(); setupEventContextTrace(client); + setOpenTelemetryContextAsyncContextStrategy({ useOpenTelemetrySpanCreation: true }); [provider] = setupOtel(client); }); From 306367e9329a26ed76e69db9ed5b802884919c1b Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Sat, 6 Jun 2026 19:21:58 +0200 Subject: [PATCH 07/25] Allow non-recording spans to carry explicit sampling decisions SentryNonRecordingSpan can now store a `sampled` flag that distinguishes "sampled negative" from "no sampling decision yet" in TwP mode. Trace header generation uses the new `spanToTraceSamplingDecision` helper to propagate this decision instead of always inferring from traceFlags. Also aligns `recordException` signature to use `SpanTimeInput` on both the Span interface and its implementations. Co-Authored-By: Claude --- packages/core/src/tracing/trace.ts | 43 +++++++------------ packages/core/src/utils/spanUtils.ts | 27 +++--------- .../tracing/sentryNonRecordingSpan.test.ts | 14 +++++- 3 files changed, 34 insertions(+), 50 deletions(-) diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 94234007f99c..68300889d341 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -22,17 +22,16 @@ import { hasSpansEnabled } from '../utils/hasSpansEnabled'; import { shouldIgnoreSpan } from '../utils/should-ignore-span'; import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled'; import { parseSampleRate } from '../utils/parseSampleRate'; -import { dropUndefinedKeys } from '../utils/object'; import { generateTraceId } from '../utils/propagationContext'; import { safeMathRandom } from '../utils/randomSafeContext'; import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope'; +import { dropUndefinedKeys } from '../utils/object'; import { addChildSpanToSpan, getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON, - spanToTraceSamplingDecision, } from '../utils/spanUtils'; import { propagationContextFromHeaders, shouldContinueTrace } from '../utils/tracing'; import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; @@ -355,37 +354,25 @@ function createChildOrRootSpan({ const isolationScope = getIsolationScope(); if (!hasSpansEnabled()) { - let propagationContext: { + const propagationContext: { traceId: string; - parentSpanId?: string | undefined; - sampled?: boolean | undefined; - dsc?: Partial | undefined; - }; - - if (parentSpan) { - const parentSpanContext = parentSpan.spanContext(); - - propagationContext = { - traceId: parentSpanContext.traceId, - parentSpanId: parentSpanContext.spanId, - sampled: spanToTraceSamplingDecision(parentSpan), - dsc: undefined, - }; - } else { - propagationContext = { - ...isolationScope.getPropagationContext(), - ...scope.getPropagationContext(), - }; - } + parentSpanId?: string; + dsc?: Partial; + } = parentSpan + ? { + traceId: parentSpan.spanContext().traceId, + parentSpanId: parentSpan.spanContext().spanId, + } + : { + ...isolationScope.getPropagationContext(), + ...scope.getPropagationContext(), + }; const span = new SentryNonRecordingSpan({ traceId: propagationContext.traceId, parentSpanId: propagationContext.parentSpanId, - sampled: propagationContext.sampled, }); - // If this is a root span, we ensure to freeze a DSC - // So we can have at least partial data here if (forceTransaction || !parentSpan) { const dsc = dropUndefinedKeys({ ...(propagationContext.dsc || getDynamicSamplingContextFromSpan(span)), @@ -407,8 +394,8 @@ function createChildOrRootSpan({ return new SentryNonRecordingSpan({ dropReason: 'ignored', - traceId: parentSpan?.spanContext().traceId ?? scope.getPropagationContext().traceId, sampled: false, + traceId: parentSpan?.spanContext().traceId ?? scope.getPropagationContext().traceId, }); } @@ -563,7 +550,7 @@ function _startChildSpan(parentSpan: Span, scope: Scope, spanArguments: SentrySp traceId, sampled, }) - : new SentryNonRecordingSpan({ traceId, sampled }); + : new SentryNonRecordingSpan({ traceId, sampled: false }); addChildSpanToSpan(parentSpan, childSpan); diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index be24d888d65e..c4fca3c1d5df 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -82,18 +82,18 @@ export function spanToTraceContext(span: Span): TraceContext { * Convert a Span to a Sentry trace header. */ export function spanToTraceHeader(span: Span): string { - const { traceId, spanId } = span.spanContext(); - const sampled = spanToTraceSamplingDecision(span); - return generateSentryTraceHeader(traceId, spanId, sampled); + const spanContext = span.spanContext(); + const sampled = 'sampled' in spanContext ? spanContext.sampled : spanIsSampled(span); + return generateSentryTraceHeader(spanContext.traceId, spanContext.spanId, sampled); } /** * Convert a Span to a W3C traceparent header. */ export function spanToTraceparentHeader(span: Span): string { - const { traceId, spanId } = span.spanContext(); - const sampled = spanToTraceSamplingDecision(span); - return generateTraceparentHeader(traceId, spanId, sampled); + const spanContext = span.spanContext(); + const sampled = 'sampled' in spanContext ? spanContext.sampled : spanIsSampled(span); + return generateTraceparentHeader(spanContext.traceId, spanContext.spanId, sampled); } /** @@ -314,21 +314,6 @@ export function spanIsSampled(span: Span): boolean { return traceFlags === TRACE_FLAG_SAMPLED; } -/** - * Returns the sampling decision to propagate for trace headers. - * This intentionally differs from `spanIsSampled`: non-recording spans can - * represent either "sampled false" or "no decision yet" in TwP mode. - */ -export function spanToTraceSamplingDecision(span: Span): boolean | undefined { - const spanContext = span.spanContext(); - - if ('sampled' in spanContext) { - return spanContext.sampled; - } - - return spanIsSampled(span); -} - /** Get the status message to use for a JSON representation of a span. */ export function getStatusMessage(status: SpanStatus | undefined): string | undefined { if (!status || status.code === SPAN_STATUS_UNSET) { diff --git a/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts b/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts index 93617f706f5f..fa774cd9ee2c 100644 --- a/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts +++ b/packages/core/test/lib/tracing/sentryNonRecordingSpan.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { SPAN_STATUS_ERROR } from '../../../src/tracing'; import { SentryNonRecordingSpan } from '../../../src/tracing/sentryNonRecordingSpan'; import type { Span } from '../../../src/types/span'; -import { spanIsSampled, spanToJSON, TRACE_FLAG_NONE } from '../../../src/utils/spanUtils'; +import { spanIsSampled, spanToJSON, spanToTraceHeader, TRACE_FLAG_NONE } from '../../../src/utils/spanUtils'; describe('SentryNonRecordingSpan', () => { it('satisfies the Span interface', () => { @@ -51,4 +51,16 @@ describe('SentryNonRecordingSpan', () => { sampled: false, }); }); + + it('propagates no sampling decision in trace header when sampled is undefined', () => { + const span = new SentryNonRecordingSpan({ traceId: 'aabb', spanId: 'ccdd' }); + const header = spanToTraceHeader(span); + expect(header).toBe('aabb-ccdd'); + }); + + it('propagates sampled=false in trace header when explicitly set', () => { + const span = new SentryNonRecordingSpan({ traceId: 'aabb', spanId: 'ccdd', sampled: false }); + const header = spanToTraceHeader(span); + expect(header).toBe('aabb-ccdd-0'); + }); }); From 2b707308d9c3cd996133aa32867890168748ca9d Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Sun, 7 Jun 2026 04:12:57 +0200 Subject: [PATCH 08/25] WIP --- packages/core/src/tracing/index.ts | 1 + packages/core/src/tracing/sentrySpan.ts | 11 +- packages/core/src/tracing/trace.ts | 22 ++-- packages/node/src/sdk/initOtel.ts | 2 +- .../opentelemetry/src/asyncContextStrategy.ts | 115 +----------------- .../opentelemetry/src/sentryTraceProvider.ts | 38 +++--- ...enhanceDscWithOpenTelemetryRootSpanName.ts | 9 +- .../src/utils/parseSpanDescription.ts | 18 ++- .../test/sentryTraceProvider.test.ts | 60 +-------- .../test/utils/setupEventContextTrace.test.ts | 2 +- 10 files changed, 71 insertions(+), 207 deletions(-) diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 9b56045b37f3..9938e2c97b02 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -8,6 +8,7 @@ export { SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstat export { startSpan, startInactiveSpan, + _INTERNAL_startInactiveSpan, startSpanManual, continueTrace, withActiveSpan, diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 741368be24c3..f1856069d9d1 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -200,7 +200,16 @@ export class SentrySpan implements Span { */ public updateName(name: string): this { this._name = name; - this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + // Only set source to 'custom' when the span already has a source. + // OTel instrumentations call updateName() on spans they create — in + // SentryTraceProvider mode those are SentrySpans, not OTel SDK spans. + // Child spans start without a source so that applyOtelSpanData can + // infer the correct one (e.g. 'route', 'task') at span end. + // Users who want to mark a name as intentionally chosen should use + // updateSpanName() which always sets source via setAttributes(). + if (this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== undefined) { + this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + } return this; } diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 68300889d341..472896a07e55 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -26,13 +26,7 @@ import { generateTraceId } from '../utils/propagationContext'; import { safeMathRandom } from '../utils/randomSafeContext'; import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope'; import { dropUndefinedKeys } from '../utils/object'; -import { - addChildSpanToSpan, - getRootSpan, - spanIsSampled, - spanTimeInputToSeconds, - spanToJSON, -} from '../utils/spanUtils'; +import { addChildSpanToSpan, getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; import { propagationContextFromHeaders, shouldContinueTrace } from '../utils/tracing'; import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { logSpanStart } from './logSpans'; @@ -197,6 +191,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span { return acs.startInactiveSpan(options); } + return _startInactiveSpanImpl(options); +} + +/** + * Internal version of startInactiveSpan that bypasses the ACS check. + * Used by SentryTraceProvider to create spans without triggering recursion + * through ACS overrides. + * @hidden + */ +export function _INTERNAL_startInactiveSpan(options: StartSpanOptions): Span { + return _startInactiveSpanImpl(options); +} + +function _startInactiveSpanImpl(options: StartSpanOptions): Span { const spanArguments = parseSentrySpanArguments(options); const { forceTransaction, parentSpan: customParentSpan } = options; diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index ac8f93bd768e..63326f1ea5a6 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -92,7 +92,7 @@ export function setupOtel( options: AdditionalOpenTelemetryOptions = {}, ): [OpenTelemetryTraceProvider | undefined, AsyncLocalStorageLookup | undefined] { if (client.getOptions()._experiments?.useSentryTraceProvider) { - setOpenTelemetryContextAsyncContextStrategy({ useOpenTelemetrySpanCreation: false }); + setOpenTelemetryContextAsyncContextStrategy(); return setupSentryTraceProvider(client, options); } diff --git a/packages/opentelemetry/src/asyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts index efa0eb27feb0..71344841e22e 100644 --- a/packages/opentelemetry/src/asyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -1,38 +1,23 @@ import * as api from '@opentelemetry/api'; -import type { Scope, Span, withActiveSpan as defaultWithActiveSpan } from '@sentry/core'; -import { - _INTERNAL_safeMathRandom, - _INTERNAL_setSpanForScope, - baggageHeaderToDynamicSamplingContext, - getDefaultCurrentScope, - getDefaultIsolationScope, - setAsyncContextStrategy, -} from '@sentry/core'; +import type { Scope, withActiveSpan as defaultWithActiveSpan } from '@sentry/core'; +import { getDefaultCurrentScope, getDefaultIsolationScope, setAsyncContextStrategy } from '@sentry/core'; import { SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY, SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, SENTRY_FORK_SET_SCOPE_CONTEXT_KEY, - SENTRY_TRACE_STATE_DSC, - SENTRY_TRACE_STATE_SAMPLE_RAND, } from './constants'; import { continueTrace, startInactiveSpan, startNewTrace, startSpan, startSpanManual, withActiveSpan } from './trace'; import type { CurrentScopes } from './types'; import { getContextFromScope, getScopesFromContext } from './utils/contextData'; -import { getSamplingDecision } from './utils/getSamplingDecision'; import { getActiveSpan } from './utils/getActiveSpan'; import { getTraceData } from './utils/getTraceData'; import { suppressTracing } from './utils/suppressTracing'; -import { isSentryTraceProviderSpan } from './sentryTraceProvider'; /** * Sets the async context strategy to use follow the OTEL context under the hood. * We handle forking a hub inside of our custom OTEL Context Manager (./otelContextManager.ts) */ -export function setOpenTelemetryContextAsyncContextStrategy( - options: { useOpenTelemetrySpanCreation?: boolean } = {}, -): void { - const { useOpenTelemetrySpanCreation = true } = options; - +export function setOpenTelemetryContextAsyncContextStrategy(): void { function getScopes(): CurrentScopes { const ctx = api.context.active(); const scopes = getScopesFromContext(ctx); @@ -52,11 +37,6 @@ export function setOpenTelemetryContextAsyncContextStrategy( function withScope(callback: (scope: Scope) => T): T { const ctx = api.context.active(); - // We depend on the otelContextManager to handle the context/hub - // We set the `SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY` context value, which is picked up by - // the OTEL context manager, which uses the presence of this key to determine if it should - // fork the isolation scope, or not - // as by default, we don't want to fork this, unless triggered explicitly by `withScope` return api.context.with(ctx, () => { return callback(getCurrentScope()); }); @@ -65,9 +45,6 @@ export function setOpenTelemetryContextAsyncContextStrategy( function withSetScope(scope: Scope, callback: (scope: Scope) => T): T { const ctx = getContextFromScope(scope) || api.context.active(); - // We depend on the otelContextManager to handle the context/hub - // We set the `SENTRY_FORK_SET_SCOPE_CONTEXT_KEY` context value, which is picked up by - // the OTEL context manager, which picks up this scope as the current scope return api.context.with(ctx.setValue(SENTRY_FORK_SET_SCOPE_CONTEXT_KEY, scope), () => { return callback(scope); }); @@ -76,10 +53,6 @@ export function setOpenTelemetryContextAsyncContextStrategy( function withIsolationScope(callback: (isolationScope: Scope) => T): T { const ctx = api.context.active(); - // We depend on the otelContextManager to handle the context/hub - // We set the `SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY` context value, which is picked up by - // the OTEL context manager, which uses the presence of this key to determine if it should - // fork the isolation scope, or not return api.context.with(ctx.setValue(SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY, true), () => { return callback(getIsolationScope()); }); @@ -88,102 +61,26 @@ export function setOpenTelemetryContextAsyncContextStrategy( function withSetIsolationScope(isolationScope: Scope, callback: (isolationScope: Scope) => T): T { const ctx = api.context.active(); - // We depend on the otelContextManager to handle the context/hub - // We set the `SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY` context value, which is picked up by - // the OTEL context manager, which uses the presence of this key to determine if it should - // fork the isolation scope, or not return api.context.with(ctx.setValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, isolationScope), () => { return callback(getIsolationScope()); }); } function getCurrentScope(): Scope { - const scope = getScopes().scope; - if (!useOpenTelemetrySpanCreation) { - syncOpenTelemetrySpanWithScope(scope); - } - return scope; + return getScopes().scope; } function getIsolationScope(): Scope { return getScopes().isolationScope; } - function withActiveSpanContextOnly(span: Span | null, callback: (scope: Scope) => T): T { - const ctx = span - ? api.trace.setSpan(api.context.active(), span as api.Span) - : api.trace.deleteSpan(api.context.active()); - - return api.context.with(ctx, () => { - const scope = getCurrentScope(); - _INTERNAL_setSpanForScope(scope, span || undefined); - return callback(scope); - }); - } - - function syncOpenTelemetrySpanWithScope(scope: Scope): void { - const activeSpan = api.trace.getSpan(api.context.active()) as Span | undefined; - - if (!activeSpan) { - return; - } - - const scopeSpan = scope.getScopeData().span; - if (scopeSpan === activeSpan) { - return; - } - - const activeSpanContext = activeSpan.spanContext(); - if (activeSpanContext.isRemote) { - if (scopeSpan) { - return; - } - - // A remote OTel span context represents an incoming parent, not a local span - // we can finish and send. Store it as propagation context so the next core - // root span continues the trace and becomes the transaction segment. - const dsc = - baggageHeaderToDynamicSamplingContext(activeSpanContext.traceState?.get(SENTRY_TRACE_STATE_DSC)) ?? {}; - const sampleRandString = activeSpanContext.traceState?.get(SENTRY_TRACE_STATE_SAMPLE_RAND) ?? dsc?.sample_rand; - const sampleRand = typeof sampleRandString === 'string' ? Number(sampleRandString) : undefined; - - scope.setPropagationContext({ - traceId: activeSpanContext.traceId, - parentSpanId: activeSpanContext.spanId, - sampled: getSamplingDecision(activeSpanContext), - dsc, - sampleRand: - typeof sampleRand === 'number' && !Number.isNaN(sampleRand) ? sampleRand : _INTERNAL_safeMathRandom(), - }); - return; - } - - if (scopeSpan && !isSentryTraceProviderSpan(scopeSpan)) { - return; - } - - _INTERNAL_setSpanForScope(scope, activeSpan); - } - - const baseStrategy = { + setAsyncContextStrategy({ withScope, withSetScope, withSetIsolationScope, withIsolationScope, getCurrentScope, getIsolationScope, - }; - - if (!useOpenTelemetrySpanCreation) { - setAsyncContextStrategy({ - ...baseStrategy, - withActiveSpan: withActiveSpanContextOnly as typeof defaultWithActiveSpan, - }); - return; - } - - setAsyncContextStrategy({ - ...baseStrategy, startSpan, startSpanManual, startInactiveSpan, @@ -192,8 +89,6 @@ export function setOpenTelemetryContextAsyncContextStrategy( getTraceData, continueTrace, startNewTrace, - // The types here don't fully align, because our own `Span` type is narrower - // than the OTEL one - but this is OK for here, as we now we'll only have OTEL spans passed around withActiveSpan: withActiveSpan as typeof defaultWithActiveSpan, }); } diff --git a/packages/opentelemetry/src/sentryTraceProvider.ts b/packages/opentelemetry/src/sentryTraceProvider.ts index adfff453f4bf..8bf5481b9c73 100644 --- a/packages/opentelemetry/src/sentryTraceProvider.ts +++ b/packages/opentelemetry/src/sentryTraceProvider.ts @@ -23,7 +23,7 @@ import { spanToJSON, SPAN_STATUS_ERROR, SPAN_STATUS_OK, - startInactiveSpan, + _INTERNAL_startInactiveSpan, startNewTrace, withScope, } from '@sentry/core'; @@ -34,16 +34,6 @@ import { setIsSetup } from './utils/setupCheck'; type SentrySpanWithOtelKind = Span & { kind?: SpanKind }; type SentrySpanWithOtelSourceInference = Span & { _sentryOtelInferSource?: boolean }; -type SentryTraceProviderSpan = Span & { _sentryTraceProviderSpan?: true }; - -export function isSentryTraceProviderSpan(span: Span | undefined): boolean { - return (span as SentryTraceProviderSpan | undefined)?._sentryTraceProviderSpan === true; -} - -function markSentryTraceProviderSpan(span: Span): Span { - addNonEnumerableProperty(span as SentryTraceProviderSpan, '_sentryTraceProviderSpan', true); - return span; -} const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE = 'http.response.status_code'; const LEGACY_HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE = 'http.status_code'; @@ -137,7 +127,7 @@ class SentryTracer implements Tracer { return context.with(parentContext, () => { const span = this._startSentrySpan(name, options, parentSpan, ctx !== undefined); - markSentryTraceProviderSpan(span); + applyOtelSpanKind(span, options.kind); if (options.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === undefined) { addNonEnumerableProperty(span as SentrySpanWithOtelSourceInference, '_sentryOtelInferSource', true); @@ -195,7 +185,7 @@ class SentryTracer implements Tracer { }; if (options.root) { - return startNewTrace(() => startInactiveSpan({ ...sentryOptions, parentSpan: null })); + return startNewTrace(() => _INTERNAL_startInactiveSpan({ ...sentryOptions, parentSpan: null })); } if (parentSpan?.spanContext().isRemote) { @@ -203,17 +193,17 @@ class SentryTracer implements Tracer { } if (parentSpan) { - return startInactiveSpan({ ...sentryOptions, parentSpan: parentSpan as unknown as Span }); + return _INTERNAL_startInactiveSpan({ ...sentryOptions, parentSpan: parentSpan as unknown as Span }); } - return startInactiveSpan({ + return _INTERNAL_startInactiveSpan({ ...sentryOptions, parentSpan: hasExplicitContext ? null : undefined, }); } private _startRootSpanWithRemoteParent( - options: Parameters[0], + options: Parameters[0], parentSpan: OpenTelemetrySpan, ): Span { const { spanId, traceId } = parentSpan.spanContext(); @@ -231,17 +221,14 @@ class SentryTracer implements Tracer { }); _INTERNAL_setSpanForScope(scope, undefined); - return startInactiveSpan({ ...options, parentSpan: null }); + return _INTERNAL_startInactiveSpan({ ...options, parentSpan: null }); }); } private _createNonRecordingSpan(parentSpan: OpenTelemetrySpan | undefined): OpenTelemetrySpan { - const span = new SentryNonRecordingSpan({ + return new SentryNonRecordingSpan({ traceId: parentSpan?.spanContext().traceId, - }); - markSentryTraceProviderSpan(span); - - return span as OpenTelemetrySpan; + }) as OpenTelemetrySpan; } } @@ -266,9 +253,16 @@ export function applyOtelSpanData(span: Span, options: { finalizeStatus?: boolea span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, inferred.op); } + // Don't apply 'url' source at creation time — only at span end (finalizeStatus). + // At creation, http.route may not be set yet, so inference falls back to 'url'. + // Keeping the default 'custom' source from _startRootSpan allows + // enhanceDscWithOpenTelemetryRootSpanName to include the transaction name in + // the DSC. At span end, http.route is typically available and inference returns + // 'route' instead. If it's still 'url', it's applied then. const shouldApplyInferredSource = inferred.source !== undefined && inferred.source !== 'custom' && + (options.finalizeStatus || inferred.source !== 'url') && (spanJSON.parent_span_id === undefined || kind === SpanKind.SERVER); if ( diff --git a/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts b/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts index 7fb080119d3b..028dba699ab8 100644 --- a/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts +++ b/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts @@ -2,7 +2,6 @@ import type { Client } from '@sentry/core'; import { hasSpansEnabled, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; import { getSamplingDecision } from './getSamplingDecision'; import { parseSpanDescription } from './parseSpanDescription'; -import { spanHasName } from './spanTypes'; /** * Setup a DSC handler on the passed client, @@ -24,9 +23,11 @@ export function enhanceDscWithOpenTelemetryRootSpanName(client: Client): void { const attributes = jsonSpan.data; const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; - const { description } = spanHasName(rootSpan) ? parseSpanDescription(rootSpan) : { description: undefined }; - if (source !== 'url' && description) { - dsc.transaction = description; + if (jsonSpan.description) { + const { description } = parseSpanDescription(rootSpan); + if (source !== 'url' && description) { + dsc.transaction = description; + } } // Also ensure sampling decision is correctly inferred diff --git a/packages/opentelemetry/src/utils/parseSpanDescription.ts b/packages/opentelemetry/src/utils/parseSpanDescription.ts index fc0f92143516..6aaeb4b0f221 100644 --- a/packages/opentelemetry/src/utils/parseSpanDescription.ts +++ b/packages/opentelemetry/src/utils/parseSpanDescription.ts @@ -14,7 +14,7 @@ import { SEMATTRS_MESSAGING_SYSTEM, SEMATTRS_RPC_SERVICE, } from '@opentelemetry/semantic-conventions'; -import type { SpanAttributes, TransactionSource } from '@sentry/core'; +import type { Span, SpanAttributes, TransactionSource } from '@sentry/core'; import { getSanitizedUrlString, parseUrl, @@ -22,6 +22,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + spanToJSON, stripUrlQueryAndFragment, } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '../semanticAttributes'; @@ -104,10 +105,19 @@ export function inferSpanData(spanName: string, attributes: SpanAttributes, kind * Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306 */ export function parseSpanDescription(span: AbstractSpan): SpanDescription { - const attributes = spanHasAttributes(span) ? span.attributes : {}; - const name = spanHasName(span) ? span.name : ''; - const kind = getSpanKind(span); + let attributes: Attributes; + let name: string; + + if (spanHasAttributes(span)) { + attributes = span.attributes; + name = spanHasName(span) ? span.name : ''; + } else { + const json = typeof (span as Span).spanContext === 'function' ? spanToJSON(span as Span) : undefined; + attributes = json?.data || {}; + name = spanHasName(span) ? span.name : json?.description || ''; + } + const kind = getSpanKind(span); return inferSpanData(name, attributes, kind); } diff --git a/packages/opentelemetry/test/sentryTraceProvider.test.ts b/packages/opentelemetry/test/sentryTraceProvider.test.ts index c1e1bce7b9c2..24bc4a99b564 100644 --- a/packages/opentelemetry/test/sentryTraceProvider.test.ts +++ b/packages/opentelemetry/test/sentryTraceProvider.test.ts @@ -1,14 +1,5 @@ import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; -import { - continueTrace, - getActiveSpan, - getDynamicSamplingContextFromSpan, - getRootSpan, - spanToJSON, - SPAN_STATUS_ERROR, - startSpanManual, - type Span, -} from '@sentry/core'; +import { getActiveSpan, spanToJSON, SPAN_STATUS_ERROR, startSpanManual, type Span } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { SentryAsyncLocalStorageContextManager } from '../src/asyncLocalStorageContextManager'; import { setOpenTelemetryContextAsyncContextStrategy } from '../src/asyncContextStrategy'; @@ -19,7 +10,7 @@ import { init as initTestClient } from './helpers/TestClient'; describe('SentryTraceProvider', () => { beforeEach(() => { (global as { __SENTRY__?: unknown }).__SENTRY__ = {}; - setOpenTelemetryContextAsyncContextStrategy({ useOpenTelemetrySpanCreation: false }); + setOpenTelemetryContextAsyncContextStrategy(); initTestClient({ tracesSampleRate: 1 }); context.setGlobalContextManager(new SentryAsyncLocalStorageContextManager()); trace.setGlobalTracerProvider(new SentryTraceProvider()); @@ -96,27 +87,7 @@ describe('SentryTraceProvider', () => { }); }); - it('does not replace active core spans with provider-created OpenTelemetry spans', () => { - const tracer = trace.getTracer('test'); - - tracer.startActiveSpan('otel-parent', otelParent => { - startSpanManual({ name: 'sentry-parent' }, sentryParent => { - const otelChild = tracer.startSpan('otel-child'); - const otelChildContext = trace.setSpan(context.active(), otelChild); - - context.with(otelChildContext, () => { - expect(getActiveSpan()).toBe(sentryParent); - }); - - otelChild.end(); - sentryParent.end(); - }); - - otelParent.end(); - }); - }); - - it('parents core spans to the active OpenTelemetry span in context-only mode', () => { + it('parents core spans to the active OpenTelemetry span', () => { trace.getTracer('test').startActiveSpan('parent', parent => { startSpanManual({ name: 'child' }, child => { expect(spanToJSON(child).parent_span_id).toBe(parent.spanContext().spanId); @@ -125,31 +96,6 @@ describe('SentryTraceProvider', () => { }); }); - it('continues remote OpenTelemetry contexts as root core spans in context-only mode', () => { - const traceId = '12312012123120121231201212312012'; - const parentSpanId = '1121201211212012'; - const remoteContext = trace.setSpanContext(context.active(), { - traceId, - spanId: parentSpanId, - isRemote: true, - traceFlags: TraceFlags.SAMPLED, - }); - - context.with(remoteContext, () => { - continueTrace({ sentryTrace: `${traceId}-${parentSpanId}-1`, baggage: undefined }, () => { - startSpanManual({ name: 'server' }, span => { - expect(getRootSpan(span)).toBe(span); - expect(spanToJSON(span)).toMatchObject({ - trace_id: traceId, - parent_span_id: parentSpanId, - }); - expect(getDynamicSamplingContextFromSpan(span)).toEqual({}); - span.end(); - }); - }); - }); - }); - it('continues remote OpenTelemetry span contexts as root Sentry spans', () => { const remoteContext = trace.setSpanContext(context.active(), { traceId: '12312012123120121231201212312012', diff --git a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts index 7285877652fa..9cda157d0e34 100644 --- a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts +++ b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts @@ -30,7 +30,7 @@ describe('setupEventContextTrace', () => { client.init(); setupEventContextTrace(client); - setOpenTelemetryContextAsyncContextStrategy({ useOpenTelemetrySpanCreation: true }); + setOpenTelemetryContextAsyncContextStrategy(); [provider] = setupOtel(client); }); From 2bb6a3c9e7f802a8b0e52655fd9e03836f218cde Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 8 Jun 2026 13:13:13 +0200 Subject: [PATCH 09/25] Make sure getSamplingDecision reads sampled flag from spancontext --- packages/opentelemetry/src/utils/getSamplingDecision.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/opentelemetry/src/utils/getSamplingDecision.ts b/packages/opentelemetry/src/utils/getSamplingDecision.ts index 216b5249224e..e0025ea21c88 100644 --- a/packages/opentelemetry/src/utils/getSamplingDecision.ts +++ b/packages/opentelemetry/src/utils/getSamplingDecision.ts @@ -13,6 +13,12 @@ import { SENTRY_TRACE_STATE_DSC, SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING } from export function getSamplingDecision(spanContext: SpanContext): boolean | undefined { const { traceFlags, traceState } = spanContext; + // SentrySpans carry an explicit `sampled` property on their span context. + // This is the most direct source of truth and avoids the need for trace state. + if ('sampled' in spanContext && (spanContext as { sampled?: boolean }).sampled !== undefined) { + return (spanContext as { sampled?: boolean }).sampled; + } + const sampledNotRecording = traceState ? traceState.get(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING) === '1' : false; // If trace flag is `SAMPLED`, we interpret this as sampled From bbbed4ed1e74682d7ed0be057918b70173010fcf Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 8 Jun 2026 13:44:12 +0200 Subject: [PATCH 10/25] Fix cf integration tests --- dev-packages/cloudflare-integration-tests/expect.ts | 9 ++++++--- .../suites/hono-integration/test.ts | 2 +- .../cloudflare-integration-tests/suites/hono-sdk/test.ts | 2 +- .../suites/tracing/headers/test.ts | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/expect.ts b/dev-packages/cloudflare-integration-tests/expect.ts index c3e2bd007436..2a10cdc22b10 100644 --- a/dev-packages/cloudflare-integration-tests/expect.ts +++ b/dev-packages/cloudflare-integration-tests/expect.ts @@ -61,7 +61,11 @@ export function expectedEvent(event: Event, { sdk }: { sdk: 'cloudflare' | 'hono export function eventEnvelope( event: Event, - { includeSampleRand = false, sdk = 'cloudflare' }: { includeSampleRand?: boolean; sdk?: 'cloudflare' | 'hono' } = {}, + { + includeSamplingFields = false, + includeSampleRand = false, + sdk = 'cloudflare', + }: { includeSamplingFields?: boolean; includeSampleRand?: boolean; sdk?: 'cloudflare' | 'hono' } = {}, ): Envelope { return [ { @@ -72,9 +76,8 @@ export function eventEnvelope( environment: event.environment || 'production', public_key: 'public', trace_id: UUID_MATCHER, - sample_rate: expect.any(String), + ...(includeSamplingFields && { sample_rate: expect.any(String), sampled: expect.any(String) }), ...(includeSampleRand && { sample_rand: expect.stringMatching(/^[01](\.\d+)?$/) }), - sampled: expect.any(String), transaction: expect.any(String), }, }, diff --git a/dev-packages/cloudflare-integration-tests/suites/hono-integration/test.ts b/dev-packages/cloudflare-integration-tests/suites/hono-integration/test.ts index 0cf4f1dec328..e69cb0951c39 100644 --- a/dev-packages/cloudflare-integration-tests/suites/hono-integration/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/hono-integration/test.ts @@ -28,7 +28,7 @@ it('Hono app captures errors', async ({ signal }) => { url: expect.any(String), }, }, - { includeSampleRand: true }, + { includeSamplingFields: true, includeSampleRand: true }, ), ) // Second envelope: transaction event diff --git a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts index c1f17ddb6d19..bbaa75aae4e8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts @@ -39,7 +39,7 @@ it('Hono app captures parametrized errors (Hono SDK)', async ({ signal }) => { }, ], }, - { includeSampleRand: true, sdk: 'hono' }, + { includeSamplingFields: true, includeSampleRand: true, sdk: 'hono' }, ), ) diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/headers/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/headers/test.ts index d92fde438eb8..1128b9106e74 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/headers/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/headers/test.ts @@ -9,8 +9,8 @@ it('Tracing headers', async ({ signal }) => { const [SERVER_URL, closeTestServer] = await createTestServer() .get('/', headers => { expect(headers['baggage']).toEqual(expect.any(String)); - expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-0$/)); - expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-00$/)); }) .start(); From cd032d638059266fff2b595266e9aa6212e0d639 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 8 Jun 2026 13:55:29 +0200 Subject: [PATCH 11/25] Handle parent_span_id --- packages/core/src/tracing/sentryNonRecordingSpan.ts | 10 ++++++++++ packages/core/src/utils/spanUtils.ts | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/core/src/tracing/sentryNonRecordingSpan.ts b/packages/core/src/tracing/sentryNonRecordingSpan.ts index e009d53a2ce1..81b02c55badc 100644 --- a/packages/core/src/tracing/sentryNonRecordingSpan.ts +++ b/packages/core/src/tracing/sentryNonRecordingSpan.ts @@ -21,6 +21,7 @@ interface SentryNonRecordingSpanArguments extends SentrySpanArguments { export class SentryNonRecordingSpan implements Span { private _traceId: string; private _spanId: string; + private _parentSpanId: string | undefined; private _sampled: boolean | undefined; /** @@ -33,6 +34,7 @@ export class SentryNonRecordingSpan implements Span { public constructor(spanContext: SentryNonRecordingSpanArguments = {}) { this._traceId = spanContext.traceId || generateTraceId(); this._spanId = spanContext.spanId || generateSpanId(); + this._parentSpanId = spanContext.parentSpanId; this._sampled = spanContext.sampled; this.dropReason = spanContext.dropReason; } @@ -94,6 +96,14 @@ export class SentryNonRecordingSpan implements Span { return this; } + /** + * @hidden + * @internal + */ + public get parentSpanId(): string | undefined { + return this._parentSpanId; + } + /** * This should generally not be used, * but we need it for being compliant with the OTEL Span interface. diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index c4fca3c1d5df..40685099dc8b 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -197,10 +197,11 @@ export function spanToJSON(span: Span): SpanJSON { } // Finally, at least we have `spanContext()`.... - // This should not actually happen in reality, but we need to handle it for type safety. + // This covers SentryNonRecordingSpan and any other non-SDK span types. return { span_id, trace_id, + parent_span_id: (span as { parentSpanId?: string }).parentSpanId, start_timestamp: 0, data: {}, }; From 87ed1f03217c4714c24eed23b0a115fa46b5c617 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 8 Jun 2026 15:58:10 +0200 Subject: [PATCH 12/25] Fix scope leaking --- packages/core/src/tracing/trace.ts | 28 +++++++++++++++---- .../opentelemetry/src/sentryTraceProvider.ts | 25 ++++++++++------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 472896a07e55..cea2e45e40e8 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -409,7 +409,7 @@ function createChildOrRootSpan({ let span: Span; if (parentSpan && !forceTransaction) { - span = _startChildSpan(parentSpan, scope, spanArguments); + span = _startChildSpan(parentSpan, scope, spanArguments, isolationScope); addChildSpanToSpan(parentSpan, span); } else if (parentSpan) { // If we forced a transaction but have a parent span, make sure to continue from the parent span, not the scope @@ -425,6 +425,7 @@ function createChildOrRootSpan({ }, scope, parentSampled, + isolationScope, ); freezeDscOnSpan(span, dsc); @@ -447,6 +448,7 @@ function createChildOrRootSpan({ }, scope, parentSampled, + isolationScope, ); if (dsc) { @@ -456,8 +458,6 @@ function createChildOrRootSpan({ logSpanStart(span); - setCapturedScopesOnSpan(span, scope, isolationScope); - return span; } @@ -488,7 +488,12 @@ function getAcs(): AsyncContextStrategy { return getAsyncContextStrategy(carrier); } -function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parentSampled?: boolean): SentrySpan { +function _startRootSpan( + spanArguments: SentrySpanArguments, + scope: Scope, + parentSampled?: boolean, + isolationScope?: Scope, +): SentrySpan { const client = getClient(); const options: Partial = client?.getOptions() || {}; @@ -535,6 +540,10 @@ function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parent client.recordDroppedEvent('sample_rate', hasSpanStreamingEnabled(client) ? 'span' : 'transaction'); } + if (isolationScope) { + setCapturedScopesOnSpan(rootSpan, scope, isolationScope); + } + if (client) { client.emit('spanStart', rootSpan); } @@ -546,7 +555,12 @@ function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parent * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. * This inherits the sampling decision from the parent span. */ -function _startChildSpan(parentSpan: Span, scope: Scope, spanArguments: SentrySpanArguments): Span { +function _startChildSpan( + parentSpan: Span, + scope: Scope, + spanArguments: SentrySpanArguments, + isolationScope?: Scope, +): Span { const { spanId, traceId } = parentSpan.spanContext(); const isTracingSuppressed = _isTracingSuppressed(scope); const sampled = isTracingSuppressed ? false : spanIsSampled(parentSpan); @@ -583,6 +597,10 @@ function _startChildSpan(parentSpan: Span, scope: Scope, spanArguments: SentrySp } } + if (isolationScope) { + setCapturedScopesOnSpan(childSpan, scope, isolationScope); + } + client.emit('spanStart', childSpan); // If it has an endTimestamp, it's already ended if (spanArguments.endTimestamp) { diff --git a/packages/opentelemetry/src/sentryTraceProvider.ts b/packages/opentelemetry/src/sentryTraceProvider.ts index 8bf5481b9c73..3dc58f903ae5 100644 --- a/packages/opentelemetry/src/sentryTraceProvider.ts +++ b/packages/opentelemetry/src/sentryTraceProvider.ts @@ -19,6 +19,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SentryNonRecordingSpan, + getCapturedScopesOnSpan, getSpanStatusFromHttpCode, spanToJSON, SPAN_STATUS_ERROR, @@ -28,6 +29,7 @@ import { withScope, } from '@sentry/core'; import type { Span, SpanAttributes, SpanLink, SpanStatus } from '@sentry/core'; +import { SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY } from './constants'; import { inferSpanData } from './utils/parseSpanDescription'; import { getSamplingDecision } from './utils/getSamplingDecision'; import { setIsSetup } from './utils/setupCheck'; @@ -125,16 +127,14 @@ class SentryTracer implements Tracer { return this._createNonRecordingSpan(parentSpan); } - return context.with(parentContext, () => { - const span = this._startSentrySpan(name, options, parentSpan, ctx !== undefined); + const span = this._startSentrySpan(name, options, parentSpan, ctx !== undefined); - applyOtelSpanKind(span, options.kind); - if (options.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === undefined) { - addNonEnumerableProperty(span as SentrySpanWithOtelSourceInference, '_sentryOtelInferSource', true); - } - applyOtelSpanData(span); - return span as OpenTelemetrySpan; - }); + applyOtelSpanKind(span, options.kind); + if (options.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === undefined) { + addNonEnumerableProperty(span as SentrySpanWithOtelSourceInference, '_sentryOtelInferSource', true); + } + applyOtelSpanData(span); + return span as OpenTelemetrySpan; } /** @inheritdoc */ @@ -163,7 +163,12 @@ class SentryTracer implements Tracer { ) as F; const span = this.startSpan(name, options, ctx); - const ctxWithSpan = trace.setSpan(ctx, span); + let ctxWithSpan = trace.setSpan(ctx, span); + + const capturedIsolationScope = getCapturedScopesOnSpan(span as unknown as Span).isolationScope; + if (capturedIsolationScope) { + ctxWithSpan = ctxWithSpan.setValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, capturedIsolationScope); + } return context.with(ctxWithSpan, () => { _INTERNAL_setSpanForScope(getCurrentScope(), span as unknown as Span); From 29edad8b653985077a102fad767cae3cebdd7f5a Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 12:03:28 +0200 Subject: [PATCH 13/25] =?UTF-8?q?Revert=20idle=20span=20DSC=20changes=20?= =?UTF-8?q?=E2=80=94=20not=20needed=20for=20SentryTraceProvider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- packages/core/src/tracing/idleSpan.ts | 7 +++++-- packages/core/test/lib/tracing/idleSpan.test.ts | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/src/tracing/idleSpan.ts b/packages/core/src/tracing/idleSpan.ts index f6f3bab6ecdd..22ba70b81a65 100644 --- a/packages/core/src/tracing/idleSpan.ts +++ b/packages/core/src/tracing/idleSpan.ts @@ -6,7 +6,6 @@ import type { Span } from '../types/span'; import type { StartSpanOptions } from '../types/startSpanOptions'; import { debug } from '../utils/debug-logger'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; -import { dropUndefinedKeys } from '../utils/object'; import { shouldIgnoreSpan } from '../utils/should-ignore-span'; import { _setSpanForScope } from '../utils/spanOnScope'; import { @@ -125,7 +124,11 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti if (!client || !hasSpansEnabled()) { const span = new SentryNonRecordingSpan(); - const dsc = dropUndefinedKeys(getDynamicSamplingContextFromSpan(span)) satisfies Partial; + const dsc = { + sample_rate: '0', + sampled: 'false', + ...getDynamicSamplingContextFromSpan(span), + } satisfies Partial; freezeDscOnSpan(span, dsc); return span; diff --git a/packages/core/test/lib/tracing/idleSpan.test.ts b/packages/core/test/lib/tracing/idleSpan.test.ts index ffa92939db26..29aaa63c2bb0 100644 --- a/packages/core/test/lib/tracing/idleSpan.test.ts +++ b/packages/core/test/lib/tracing/idleSpan.test.ts @@ -62,11 +62,12 @@ describe('startIdleSpan', () => { const idleSpan = startIdleSpan({ name: 'foo' }); expect(idleSpan).toBeDefined(); expect(idleSpan).toBeInstanceOf(SentryNonRecordingSpan); - // DSC is still set on the span, but tracing-without-performance should - // preserve deferred sampling instead of freezing an explicit negative decision. + // DSC is still correctly set on the span expect(getDynamicSamplingContextFromSpan(idleSpan)).toEqual({ environment: 'production', public_key: '123', + sample_rate: '0', + sampled: 'false', trace_id: expect.stringMatching(/[a-f0-9]{32}/), }); From fbe96b20a233c93667c25e05beae973cfc479e65 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 16:04:05 +0200 Subject: [PATCH 14/25] fix(core): Preserve continued-trace DSC transaction for TwP root spans In Tracing without Performance mode, a root non-recording span froze a DSC whose `transaction` was always the local span name, overwriting the frozen transaction of a continued trace. Keep the incoming/derived transaction and only fall back to the local span name when there is none. Co-Authored-By: Claude --- packages/core/src/tracing/trace.ts | 7 ++++-- .../core/test/lib/utils/traceData.test.ts | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index cea2e45e40e8..9ed620f9f744 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -382,9 +382,12 @@ function createChildOrRootSpan({ }); if (forceTransaction || !parentSpan) { + const incomingDsc = propagationContext.dsc || getDynamicSamplingContextFromSpan(span); + // A continued trace's DSC is frozen and must win - only fall back to the + // local span name when there is no incoming/derived transaction. const dsc = dropUndefinedKeys({ - ...(propagationContext.dsc || getDynamicSamplingContextFromSpan(span)), - transaction: spanArguments.name, + ...incomingDsc, + transaction: incomingDsc.transaction ?? spanArguments.name, }) satisfies Partial; freezeDscOnSpan(span, dsc); } diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index 678eec31d2de..a90c9b0debdc 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -174,6 +174,31 @@ describe('getTraceData', () => { }); }); + it('preserves a continued trace DSC transaction when starting a TwP span', () => { + setupClient({ tracesSampleRate: undefined }); + + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + dsc: { + environment: 'production', + public_key: '123', + trace_id: '12345678901234567890123456789012', + transaction: 'upstream-root', + sampled: 'true', + sample_rate: '0.5', + }, + }); + + startSpan({ name: 'db.query' }, () => { + const data = getTraceData(); + + // The local span name must not overwrite the frozen DSC of the continued trace. + expect(data.baggage).toContain('sentry-transaction=upstream-root'); + expect(data.baggage).not.toContain('db.query'); + }); + }); + it('allows to pass a span directly', () => { setupClient(); From 8949df52e08995a1aa29646f11d4120ce386b4c7 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 16:14:28 +0200 Subject: [PATCH 15/25] docs(core): Document the SentryNonRecordingSpan parentSpanId getter Explain that the getter exists so the generic `spanToJSON` fallback can surface `parent_span_id` for non-recording spans. Co-Authored-By: Claude --- packages/core/src/tracing/sentryNonRecordingSpan.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/tracing/sentryNonRecordingSpan.ts b/packages/core/src/tracing/sentryNonRecordingSpan.ts index 81b02c55badc..539fe8f3e283 100644 --- a/packages/core/src/tracing/sentryNonRecordingSpan.ts +++ b/packages/core/src/tracing/sentryNonRecordingSpan.ts @@ -97,6 +97,10 @@ export class SentryNonRecordingSpan implements Span { } /** + * Exposes the parent span id so the generic `spanToJSON` fallback (which reads + * `.parentSpanId` off the instance) can surface `parent_span_id` for non-recording spans. + * Unlike `SentrySpan`, these have no serialization of their own. + * * @hidden * @internal */ From 732606c35808cec9d96737f1dc1d8577183c7111 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 16:24:24 +0200 Subject: [PATCH 16/25] fix(opentelemetry): Link suppressed-parent non-recording spans to their parent When tracing is suppressed, `_createNonRecordingSpan` copied only the parent's `traceId` and dropped its `spanId`, so a child span under a suppressed active parent did not carry `parent_span_id` in span JSON or trace headers. Pass the parent `spanId` as `parentSpanId` so the parent link is preserved. Co-Authored-By: Claude --- packages/opentelemetry/src/sentryTraceProvider.ts | 4 +++- .../opentelemetry/test/sentryTraceProvider.test.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/opentelemetry/src/sentryTraceProvider.ts b/packages/opentelemetry/src/sentryTraceProvider.ts index 3dc58f903ae5..5f6217484949 100644 --- a/packages/opentelemetry/src/sentryTraceProvider.ts +++ b/packages/opentelemetry/src/sentryTraceProvider.ts @@ -231,8 +231,10 @@ class SentryTracer implements Tracer { } private _createNonRecordingSpan(parentSpan: OpenTelemetrySpan | undefined): OpenTelemetrySpan { + const parentSpanContext = parentSpan?.spanContext(); return new SentryNonRecordingSpan({ - traceId: parentSpan?.spanContext().traceId, + traceId: parentSpanContext?.traceId, + parentSpanId: parentSpanContext?.spanId, }) as OpenTelemetrySpan; } } diff --git a/packages/opentelemetry/test/sentryTraceProvider.test.ts b/packages/opentelemetry/test/sentryTraceProvider.test.ts index 24bc4a99b564..a54be888083e 100644 --- a/packages/opentelemetry/test/sentryTraceProvider.test.ts +++ b/packages/opentelemetry/test/sentryTraceProvider.test.ts @@ -1,4 +1,5 @@ import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; import { getActiveSpan, spanToJSON, SPAN_STATUS_ERROR, startSpanManual, type Span } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { SentryAsyncLocalStorageContextManager } from '../src/asyncLocalStorageContextManager'; @@ -63,6 +64,19 @@ describe('SentryTraceProvider', () => { }); }); + it('links non-recording spans to a suppressed active parent', () => { + trace.getTracer('test').startActiveSpan('parent', parent => { + const suppressedContext = suppressTracing(context.active()); + const child = trace.getTracer('test').startSpan('child', {}, suppressedContext); + + expect(child.isRecording()).toBe(false); + expect(spanToJSON(child as Span).trace_id).toBe(parent.spanContext().traceId); + expect(spanToJSON(child as Span).parent_span_id).toBe(parent.spanContext().spanId); + + parent.end(); + }); + }); + it('sets active OpenTelemetry spans on the Sentry scope', () => { trace.getTracer('test').startActiveSpan('parent', parent => { expect(getActiveSpan()).toBe(parent); From ec84d60ba8f7bc14ccb363b27ac87ef60562d05d Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 16:41:11 +0200 Subject: [PATCH 17/25] fix(core): Capture scopes on non-recording spans from createChildOrRootSpan The scope-capture move into `_startRootSpan`/`_startChildSpan` left the non-recording return paths (tracing disabled, ignored spans, and the no-client child) without captured scopes. SentryTraceProvider.startActiveSpan reads those scopes to fork the isolation scope onto the OTel context, so attach them on every span returned from createChildOrRootSpan. Co-Authored-By: Claude --- packages/core/src/tracing/trace.ts | 17 ++++++++++++----- packages/core/test/lib/tracing/trace.test.ts | 7 +++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 9ed620f9f744..1dc8a7cb8bc7 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -392,6 +392,10 @@ function createChildOrRootSpan({ freezeDscOnSpan(span, dsc); } + // Capture scopes even on non-recording spans so consumers (e.g. SentryTraceProvider) + // can read them to fork the isolation scope onto the active context. + setCapturedScopesOnSpan(span, scope, isolationScope); + return span; } @@ -403,11 +407,14 @@ function createChildOrRootSpan({ client?.recordDroppedEvent('ignored', 'span'); } - return new SentryNonRecordingSpan({ + const ignoredSpan = new SentryNonRecordingSpan({ dropReason: 'ignored', sampled: false, traceId: parentSpan?.spanContext().traceId ?? scope.getPropagationContext().traceId, }); + setCapturedScopesOnSpan(ignoredSpan, scope, isolationScope); + + return ignoredSpan; } let span: Span; @@ -579,6 +586,10 @@ function _startChildSpan( addChildSpanToSpan(parentSpan, childSpan); + if (isolationScope) { + setCapturedScopesOnSpan(childSpan, scope, isolationScope); + } + const client = getClient(); if (!client) { @@ -600,10 +611,6 @@ function _startChildSpan( } } - if (isolationScope) { - setCapturedScopesOnSpan(childSpan, scope, isolationScope); - } - client.emit('spanStart', childSpan); // If it has an endTimestamp, it's already ended if (spanArguments.endTimestamp) { diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 4d3258eb338a..0271807dc7d5 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -16,6 +16,7 @@ import { import { getAsyncContextStrategy } from '../../../src/asyncContext'; import { continueTrace, + getCapturedScopesOnSpan, getDynamicSamplingContextFromSpan, registerSpanErrorInstrumentation, SentrySpan, @@ -1399,6 +1400,12 @@ describe('startInactiveSpan', () => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), transaction: 'GET users/[id]', }); + + // Non-recording spans must still carry the captured scopes, so consumers like + // SentryTraceProvider can read them to fork the isolation scope onto the context. + const captured = getCapturedScopesOnSpan(span); + expect(captured.scope).toBe(getCurrentScope()); + expect(captured.isolationScope).toBe(getIsolationScope()); }); it('creates & finishes span', async () => { From 70d54800e557e17441c8e36b8245d0a8e73f8e5d Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 17:46:05 +0200 Subject: [PATCH 18/25] fix(opentelemetry): Mark SentryTraceProvider as set up only after registration The provider called `setIsSetup('SentryTraceProvider')` in its constructor, before `trace.setGlobalTracerProvider` ran. If registration failed (another tracer provider already registered), setup validation still treated the minimal provider as configured while the global tracer was a different implementation, skipping the required SentrySpanProcessor/SentrySampler checks. Move the marking to `setupSentryTraceProvider` after a successful registration. Co-Authored-By: Claude --- packages/node/src/sdk/initOtel.ts | 6 ++++++ packages/node/test/sdk/init.test.ts | 19 +++++++++++++++++++ packages/opentelemetry/src/exports.ts | 2 +- .../opentelemetry/src/sentryTraceProvider.ts | 2 -- .../test/utils/setupCheck.test.ts | 13 +++++++++++-- 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 63326f1ea5a6..54faa7f02b9f 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -17,6 +17,7 @@ import { SentrySampler, SentrySpanProcessor, SentryTraceProvider, + setIsSetup, setOpenTelemetryContextAsyncContextStrategy, } from '@sentry/opentelemetry'; import { DEBUG_BUILD } from '../debug-build'; @@ -140,6 +141,11 @@ function setupSentryTraceProvider( return [undefined, undefined]; } + // Only mark the provider as set up once it is actually the registered global + // tracer provider, so setup validation doesn't skip required checks when + // registration failed. + setIsSetup('SentryTraceProvider'); + propagation.setGlobalPropagator(new SentryPropagator()); const ctxManager = new SentryContextManager(); diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index 0fe346b09f88..266adce5f677 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -1,3 +1,4 @@ +import { trace } from '@opentelemetry/api'; import type { Integration } from '@sentry/core'; import { debug, SDK_VERSION } from '@sentry/core'; import * as SentryOpentelemetry from '@sentry/opentelemetry'; @@ -223,6 +224,24 @@ describe('init()', () => { 'Ignoring `openTelemetrySpanProcessors` because `_experiments.useSentryTraceProvider` is enabled.', ); }); + + it('does not mark SentryTraceProvider as set up when global registration fails', () => { + // Simulate another OpenTelemetry tracer provider already being registered. + const setGlobalSpy = vi.spyOn(trace, 'setGlobalTracerProvider').mockReturnValue(false); + const setIsSetupSpy = vi.spyOn(SentryOpentelemetry, 'setIsSetup'); + const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); + + init({ dsn: PUBLIC_DSN, _experiments: { useSentryTraceProvider: true } }); + + expect(getClient()?.traceProvider).not.toBeDefined(); + expect(setIsSetupSpy).not.toHaveBeenCalledWith('SentryTraceProvider'); + expect(warnSpy).toHaveBeenCalledWith( + 'Could not register SentryTraceProvider because another OpenTelemetry tracer provider is already registered.', + ); + + setGlobalSpy.mockRestore(); + setIsSetupSpy.mockRestore(); + }); }); it('returns initialized client', () => { diff --git a/packages/opentelemetry/src/exports.ts b/packages/opentelemetry/src/exports.ts index cb5f1721d823..47e1a1cb92aa 100644 --- a/packages/opentelemetry/src/exports.ts +++ b/packages/opentelemetry/src/exports.ts @@ -48,7 +48,7 @@ export { SentrySampler, wrapSamplingDecision } from './sampler'; export { applyOtelSpanData, SentryTraceProvider } from './sentryTraceProvider'; export type { OpenTelemetryTraceProvider } from './types'; -export { openTelemetrySetupCheck } from './utils/setupCheck'; +export { openTelemetrySetupCheck, setIsSetup } from './utils/setupCheck'; export { getSentryResource } from './resource'; diff --git a/packages/opentelemetry/src/sentryTraceProvider.ts b/packages/opentelemetry/src/sentryTraceProvider.ts index 5f6217484949..eaa6a10cbe77 100644 --- a/packages/opentelemetry/src/sentryTraceProvider.ts +++ b/packages/opentelemetry/src/sentryTraceProvider.ts @@ -32,7 +32,6 @@ import type { Span, SpanAttributes, SpanLink, SpanStatus } from '@sentry/core'; import { SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY } from './constants'; import { inferSpanData } from './utils/parseSpanDescription'; import { getSamplingDecision } from './utils/getSamplingDecision'; -import { setIsSetup } from './utils/setupCheck'; type SentrySpanWithOtelKind = Span & { kind?: SpanKind }; type SentrySpanWithOtelSourceInference = Span & { _sentryOtelInferSource?: boolean }; @@ -90,7 +89,6 @@ export class SentryTraceProvider implements TracerProvider { public constructor(options: { resource?: { attributes: SpanAttributes } } = {}) { this.resource = options.resource; - setIsSetup('SentryTraceProvider'); } /** @inheritdoc */ diff --git a/packages/opentelemetry/test/utils/setupCheck.test.ts b/packages/opentelemetry/test/utils/setupCheck.test.ts index ca3073197249..498fa0c8a767 100644 --- a/packages/opentelemetry/test/utils/setupCheck.test.ts +++ b/packages/opentelemetry/test/utils/setupCheck.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { SentrySampler } from '../../src/sampler'; import { SentrySpanProcessor } from '../../src/spanProcessor'; import { SentryTraceProvider } from '../../src/sentryTraceProvider'; -import { openTelemetrySetupCheck } from '../../src/utils/setupCheck'; +import { openTelemetrySetupCheck, setIsSetup } from '../../src/utils/setupCheck'; import { setupOtel } from '../helpers/initOtel'; import { cleanupOtel } from '../helpers/mockSdkInit'; import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; @@ -43,9 +43,18 @@ describe('openTelemetrySetupCheck', () => { expect(setup).toEqual(['SentrySampler', 'SentrySpanProcessor']); }); - it('returns SentryTraceProvider setup', () => { + it('does not mark SentryTraceProvider as set up on construction', () => { + // Construction must not mark setup — that only happens once the provider is + // successfully registered as the global tracer provider. Otherwise setup + // validation would skip required checks even when registration failed. new SentryTraceProvider(); + expect(openTelemetrySetupCheck()).toEqual([]); + }); + + it('returns SentryTraceProvider setup once it is marked as set up', () => { + setIsSetup('SentryTraceProvider'); + const setup = openTelemetrySetupCheck(); expect(setup).toEqual(['SentryTraceProvider']); }); From 5969f40d12b1d7e58f0e83c165d50c325de39e6c Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 18:19:21 +0200 Subject: [PATCH 19/25] fix(core): Freeze a continued trace's DSC as-is in TwP mode Align idle spans and `startSpan`/`startInactiveSpan` in Tracing without Performance mode: when continuing a trace, freeze the incoming DSC as-is (even an empty `{}` from a `sentry-trace` header without baggage) instead of merging in the local span name or fabricating client fields. Only a new trace derives the DSC from the client and attaches the local span name. Idle spans now defer the sampling decision in TwP like `startSpan`. Co-Authored-By: Claude --- packages/core/src/tracing/idleSpan.ts | 30 ++++++-- packages/core/src/tracing/trace.ts | 16 ++-- .../core/test/lib/tracing/idleSpan.test.ts | 73 +++++++++++++++++-- packages/core/test/lib/tracing/trace.test.ts | 23 ++++++ 4 files changed, 122 insertions(+), 20 deletions(-) diff --git a/packages/core/src/tracing/idleSpan.ts b/packages/core/src/tracing/idleSpan.ts index 22ba70b81a65..a4afe5370d4e 100644 --- a/packages/core/src/tracing/idleSpan.ts +++ b/packages/core/src/tracing/idleSpan.ts @@ -1,4 +1,4 @@ -import { getClient, getCurrentScope } from '../currentScopes'; +import { getClient, getCurrentScope, getIsolationScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import { SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON } from '../semanticAttributes'; import type { DynamicSamplingContext } from '../types/envelope'; @@ -6,6 +6,7 @@ import type { Span } from '../types/span'; import type { StartSpanOptions } from '../types/startSpanOptions'; import { debug } from '../utils/debug-logger'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; +import { dropUndefinedKeys } from '../utils/object'; import { shouldIgnoreSpan } from '../utils/should-ignore-span'; import { _setSpanForScope } from '../utils/spanOnScope'; import { @@ -120,21 +121,34 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti } = options; const client = getClient(); + const scope = getCurrentScope(); if (!client || !hasSpansEnabled()) { - const span = new SentryNonRecordingSpan(); + const propagationContext = { + ...getIsolationScope().getPropagationContext(), + ...scope.getPropagationContext(), + }; + + const span = new SentryNonRecordingSpan({ + traceId: propagationContext.traceId, + parentSpanId: propagationContext.parentSpanId, + }); - const dsc = { - sample_rate: '0', - sampled: 'false', - ...getDynamicSamplingContextFromSpan(span), - } satisfies Partial; + // In TwP mode, leave the sampling decision deferred (like `startSpan`) so baggage and the + // `sentry-trace` header agree. A continued trace's DSC is frozen and wins as-is, even when + // it's an empty `{}` (a `sentry-trace` header without baggage): we are not head of trace, so + // we neither fabricate client fields nor inject the local span name. Only a new trace derives + // the DSC from the client and attaches the local span name. + const dsc = (propagationContext.dsc ?? + dropUndefinedKeys({ + ...getDynamicSamplingContextFromSpan(span), + transaction: startSpanOptions.name, + })) satisfies Partial; freezeDscOnSpan(span, dsc); return span; } - const scope = getCurrentScope(); const previousActiveSpan = getActiveSpan(); const span = _startIdleSpan(startSpanOptions); diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 1dc8a7cb8bc7..9f985b91a18d 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -382,13 +382,15 @@ function createChildOrRootSpan({ }); if (forceTransaction || !parentSpan) { - const incomingDsc = propagationContext.dsc || getDynamicSamplingContextFromSpan(span); - // A continued trace's DSC is frozen and must win - only fall back to the - // local span name when there is no incoming/derived transaction. - const dsc = dropUndefinedKeys({ - ...incomingDsc, - transaction: incomingDsc.transaction ?? spanArguments.name, - }) satisfies Partial; + // A continued trace's DSC is frozen and must win as-is, even when it's an empty `{}` + // (a `sentry-trace` header without baggage): we are not head of trace, so we neither + // fabricate client fields nor inject the local span name. Only when starting a new + // trace do we derive the DSC from the client and attach the local span name. + const dsc = (propagationContext.dsc ?? + dropUndefinedKeys({ + ...getDynamicSamplingContextFromSpan(span), + transaction: spanArguments.name, + })) satisfies Partial; freezeDscOnSpan(span, dsc); } diff --git a/packages/core/test/lib/tracing/idleSpan.test.ts b/packages/core/test/lib/tracing/idleSpan.test.ts index 29aaa63c2bb0..ce6e5f8f8d4e 100644 --- a/packages/core/test/lib/tracing/idleSpan.test.ts +++ b/packages/core/test/lib/tracing/idleSpan.test.ts @@ -10,7 +10,9 @@ import { SentryNonRecordingSpan, SentrySpan, setCurrentClient, + spanToBaggageHeader, spanToJSON, + spanToTraceHeader, startInactiveSpan, startSpan, startSpanManual, @@ -59,22 +61,83 @@ describe('startIdleSpan', () => { setCurrentClient(client); client.init(); + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + }); + const idleSpan = startIdleSpan({ name: 'foo' }); expect(idleSpan).toBeDefined(); expect(idleSpan).toBeInstanceOf(SentryNonRecordingSpan); - // DSC is still correctly set on the span + + // Continues the trace from the scope, with the sampling decision deferred (no `sampled`/`sample_rate`). + expect(idleSpan.spanContext().traceId).toBe('12345678901234567890123456789012'); expect(getDynamicSamplingContextFromSpan(idleSpan)).toEqual({ environment: 'production', public_key: '123', - sample_rate: '0', - sampled: 'false', - trace_id: expect.stringMatching(/[a-f0-9]{32}/), + trace_id: '12345678901234567890123456789012', + transaction: 'foo', }); - // not set as active span, though + // baggage and the `sentry-trace` header agree: neither asserts a sampling decision. + expect(spanToTraceHeader(idleSpan)).toBe(`12345678901234567890123456789012-${idleSpan.spanContext().spanId}`); + expect(spanToBaggageHeader(idleSpan)).not.toContain('sentry-sampled'); + expect(spanToBaggageHeader(idleSpan)).not.toContain('sentry-sample_rate'); + expect(getActiveSpan()).toBe(undefined); }); + it('preserves a continued trace DSC transaction when tracing is disabled', () => { + const options = getDefaultTestClientOptions({ dsn }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + dsc: { + environment: 'production', + public_key: '123', + trace_id: '12345678901234567890123456789012', + transaction: 'upstream-root', + sampled: 'true', + sample_rate: '0.5', + }, + }); + + const idleSpan = startIdleSpan({ name: 'foo' }); + + // The continued trace's frozen DSC wins over the local idle span name. + expect(getDynamicSamplingContextFromSpan(idleSpan)).toEqual({ + environment: 'production', + public_key: '123', + trace_id: '12345678901234567890123456789012', + transaction: 'upstream-root', + sampled: 'true', + sample_rate: '0.5', + }); + }); + + it('freezes a continued trace empty DSC as-is when tracing is disabled', () => { + const options = getDefaultTestClientOptions({ dsn }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + + // A continued `sentry-trace` without baggage yields an empty frozen DSC marker. + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + dsc: {}, + }); + + const idleSpan = startIdleSpan({ name: 'foo' }); + + // We are not head of trace: don't fabricate client fields or inject the local transaction. + expect(getDynamicSamplingContextFromSpan(idleSpan)).toEqual({}); + }); + it('does not finish idle span if there are still active activities', () => { const idleSpan = startIdleSpan({ name: 'foo' }); expect(idleSpan).toBeDefined(); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 0271807dc7d5..b5ddb95c9a8d 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -250,6 +250,29 @@ describe('startSpan', () => { }); }); + it('freezes a continued trace empty DSC as-is when tracing is disabled', () => { + const options = getDefaultTestClientOptions({}); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + // A continued `sentry-trace` without baggage yields an empty frozen DSC marker. + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + dsc: {}, + }); + + const span = startSpan({ name: 'GET users/[id]' }, span => { + return span; + }); + + expect(span).toBeInstanceOf(SentryNonRecordingSpan); + expect(span.spanContext().traceId).toBe('12345678901234567890123456789012'); + // We are not head of trace: don't fabricate client fields or inject the local transaction. + expect(getDynamicSamplingContextFromSpan(span)).toEqual({}); + }); + it('creates & finishes span', async () => { const span = startSpan({ name: 'GET users/[id]' }, span => { expect(span).toBeDefined(); From 3a31d377fc60e362fd9dcfe9a8d4b0fd779b6ef0 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 18:31:10 +0200 Subject: [PATCH 20/25] fix(opentelemetry): Capture scopes on suppressed non-recording spans When tracing is suppressed, `startSpan` returns `_createNonRecordingSpan` directly, bypassing the `_INTERNAL_startInactiveSpan`/`createChildOrRootSpan` path where scopes are captured. As a result `startActiveSpan` read no captured isolation scope and couldn't fork it onto the OTel context, breaking scope isolation for work inside suppressed active spans. Capture the current scope and isolation scope on the non-recording span, mirroring `createChildOrRootSpan`. Co-Authored-By: Claude --- .../opentelemetry/src/sentryTraceProvider.ts | 10 ++++++++-- .../test/sentryTraceProvider.test.ts | 20 ++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/opentelemetry/src/sentryTraceProvider.ts b/packages/opentelemetry/src/sentryTraceProvider.ts index eaa6a10cbe77..b9ff65ac0841 100644 --- a/packages/opentelemetry/src/sentryTraceProvider.ts +++ b/packages/opentelemetry/src/sentryTraceProvider.ts @@ -15,12 +15,14 @@ import { addNonEnumerableProperty, getCurrentScope, getDynamicSamplingContextFromSpan, + getIsolationScope, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SentryNonRecordingSpan, getCapturedScopesOnSpan, getSpanStatusFromHttpCode, + setCapturedScopesOnSpan, spanToJSON, SPAN_STATUS_ERROR, SPAN_STATUS_OK, @@ -230,10 +232,14 @@ class SentryTracer implements Tracer { private _createNonRecordingSpan(parentSpan: OpenTelemetrySpan | undefined): OpenTelemetrySpan { const parentSpanContext = parentSpan?.spanContext(); - return new SentryNonRecordingSpan({ + const span = new SentryNonRecordingSpan({ traceId: parentSpanContext?.traceId, parentSpanId: parentSpanContext?.spanId, - }) as OpenTelemetrySpan; + }); + // Capture the scopes (mirroring `createChildOrRootSpan`) so `startActiveSpan` can + // fork the isolation scope onto the OTel context for work inside a suppressed span. + setCapturedScopesOnSpan(span, getCurrentScope(), getIsolationScope()); + return span as OpenTelemetrySpan; } } diff --git a/packages/opentelemetry/test/sentryTraceProvider.test.ts b/packages/opentelemetry/test/sentryTraceProvider.test.ts index a54be888083e..37336903f435 100644 --- a/packages/opentelemetry/test/sentryTraceProvider.test.ts +++ b/packages/opentelemetry/test/sentryTraceProvider.test.ts @@ -1,6 +1,14 @@ import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; -import { getActiveSpan, spanToJSON, SPAN_STATUS_ERROR, startSpanManual, type Span } from '@sentry/core'; +import { + getActiveSpan, + getCapturedScopesOnSpan, + spanToJSON, + SPAN_STATUS_ERROR, + startSpanManual, + type Span, + withIsolationScope, +} from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { SentryAsyncLocalStorageContextManager } from '../src/asyncLocalStorageContextManager'; import { setOpenTelemetryContextAsyncContextStrategy } from '../src/asyncContextStrategy'; @@ -77,6 +85,16 @@ describe('SentryTraceProvider', () => { }); }); + it('captures scopes on suppressed spans so startActiveSpan can fork the isolation scope', () => { + withIsolationScope(isolationScope => { + const suppressedContext = suppressTracing(context.active()); + const span = trace.getTracer('test').startSpan('child', {}, suppressedContext); + + // Without captured scopes, startActiveSpan cannot fork the isolation scope onto the context. + expect(getCapturedScopesOnSpan(span as unknown as Span).isolationScope).toBe(isolationScope); + }); + }); + it('sets active OpenTelemetry spans on the Sentry scope', () => { trace.getTracer('test').startActiveSpan('parent', parent => { expect(getActiveSpan()).toBe(parent); From d642cfc7b7e29fd782aae2a281e92c37d91b669c Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 18:43:09 +0200 Subject: [PATCH 21/25] fix(core): Capture scopes on the non-recording idle span placeholder In Tracing without Performance mode `startIdleSpan` builds a non-recording placeholder and freezes its DSC but never attached the current/isolation scopes, unlike the parallel non-recording paths in `createChildOrRootSpan`. Capture them so consumers like SentryTraceProvider can read the scopes off the placeholder. Co-Authored-By: Claude --- packages/core/src/tracing/idleSpan.ts | 5 +++++ packages/core/test/lib/tracing/idleSpan.test.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/packages/core/src/tracing/idleSpan.ts b/packages/core/src/tracing/idleSpan.ts index a4afe5370d4e..59680b46cfba 100644 --- a/packages/core/src/tracing/idleSpan.ts +++ b/packages/core/src/tracing/idleSpan.ts @@ -22,6 +22,7 @@ import { SentryNonRecordingSpan } from './sentryNonRecordingSpan'; import { SentrySpan } from './sentrySpan'; import { SPAN_STATUS_ERROR, SPAN_STATUS_OK } from './spanstatus'; import { startInactiveSpan } from './trace'; +import { setCapturedScopesOnSpan } from './utils'; export const TRACING_DEFAULTS = { idleTimeout: 1_000, @@ -146,6 +147,10 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti })) satisfies Partial; freezeDscOnSpan(span, dsc); + // Capture scopes even on this non-recording placeholder so consumers (e.g. SentryTraceProvider) + // can read them, mirroring the non-recording paths in `createChildOrRootSpan`. + setCapturedScopesOnSpan(span, scope, getIsolationScope()); + return span; } diff --git a/packages/core/test/lib/tracing/idleSpan.test.ts b/packages/core/test/lib/tracing/idleSpan.test.ts index ce6e5f8f8d4e..38a8ccd3c87d 100644 --- a/packages/core/test/lib/tracing/idleSpan.test.ts +++ b/packages/core/test/lib/tracing/idleSpan.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getActiveSpan, + getCapturedScopesOnSpan, getClient, getCurrentScope, getDynamicSamplingContextFromSpan, @@ -84,6 +85,10 @@ describe('startIdleSpan', () => { expect(spanToBaggageHeader(idleSpan)).not.toContain('sentry-sampled'); expect(spanToBaggageHeader(idleSpan)).not.toContain('sentry-sample_rate'); + // Scopes are captured on the placeholder so consumers (e.g. SentryTraceProvider) can read them. + expect(getCapturedScopesOnSpan(idleSpan).scope).toBe(getCurrentScope()); + expect(getCapturedScopesOnSpan(idleSpan).isolationScope).toBe(getIsolationScope()); + expect(getActiveSpan()).toBe(undefined); }); From 5917babab2b961d8e5d35da4bdbe3eb3fed154b8 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 20:40:58 +0200 Subject: [PATCH 22/25] fix(core): Pass parent span id to unsampled and ignored non-recording spans Non-recording child spans (unsampled) and ignored spans now carry their parent's span id, so `spanToJSON` surfaces `parent_span_id` for them. Also store captured scopes on the TwP placeholder in `startSpan`/`startSpanManual`. Co-Authored-By: Claude --- packages/core/src/tracing/trace.ts | 3 +- packages/core/test/lib/tracing/trace.test.ts | 59 ++++++++++++++++++-- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 9f985b91a18d..108dff33c57d 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -413,6 +413,7 @@ function createChildOrRootSpan({ dropReason: 'ignored', sampled: false, traceId: parentSpan?.spanContext().traceId ?? scope.getPropagationContext().traceId, + parentSpanId: parentSpan?.spanContext().spanId ?? scope.getPropagationContext().parentSpanId, }); setCapturedScopesOnSpan(ignoredSpan, scope, isolationScope); @@ -584,7 +585,7 @@ function _startChildSpan( traceId, sampled, }) - : new SentryNonRecordingSpan({ traceId, sampled: false }); + : new SentryNonRecordingSpan({ traceId, parentSpanId: spanId, sampled: false }); addChildSpanToSpan(parentSpan, childSpan); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index b5ddb95c9a8d..ad66c7c113f5 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { + getCapturedScopesOnSpan, getCurrentScope, getGlobalScope, getIsolationScope, @@ -16,7 +17,6 @@ import { import { getAsyncContextStrategy } from '../../../src/asyncContext'; import { continueTrace, - getCapturedScopesOnSpan, getDynamicSamplingContextFromSpan, registerSpanErrorInstrumentation, SentrySpan, @@ -236,7 +236,9 @@ describe('startSpan', () => { sampleRand: 0.42, }); + let scopeInCallback: Scope | undefined; const span = startSpan({ name: 'GET users/[id]' }, span => { + scopeInCallback = getCurrentScope(); return span; }); @@ -248,6 +250,11 @@ describe('startSpan', () => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), transaction: 'GET users/[id]', }); + + // Scopes are captured on the placeholder so consumers (e.g. SentryTraceProvider) can read them. + // `startSpan` forks the scope, so the captured scope is the one active inside the callback. + expect(getCapturedScopesOnSpan(span).scope).toBe(scopeInCallback); + expect(getCapturedScopesOnSpan(span).isolationScope).toBe(getIsolationScope()); }); it('freezes a continued trace empty DSC as-is when tracing is disabled', () => { @@ -273,6 +280,22 @@ describe('startSpan', () => { expect(getDynamicSamplingContextFromSpan(span)).toEqual({}); }); + it('exposes parent_span_id on an unsampled non-recording child span', () => { + const options = getDefaultTestClientOptions({ tracesSampleRate: 0 }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + startSpan({ name: 'parent' }, parentSpan => { + startSpan({ name: 'child' }, childSpan => { + expect(childSpan).toBeInstanceOf(SentryNonRecordingSpan); + expect(spanIsSampled(childSpan)).toBe(false); + // The non-recording child still links to its parent so `spanToJSON` can surface it. + expect(spanToJSON(childSpan).parent_span_id).toBe(parentSpan.spanContext().spanId); + }); + }); + }); + it('creates & finishes span', async () => { const span = startSpan({ name: 'GET users/[id]' }, span => { expect(span).toBeDefined(); @@ -902,7 +925,9 @@ describe('startSpanManual', () => { setCurrentClient(client); client.init(); + let scopeInCallback: Scope | undefined; const span = startSpanManual({ name: 'GET users/[id]' }, span => { + scopeInCallback = getCurrentScope(); return span; }); @@ -913,6 +938,11 @@ describe('startSpanManual', () => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), transaction: 'GET users/[id]', }); + + // Scopes are captured on the placeholder so consumers (e.g. SentryTraceProvider) can read them. + // `startSpanManual` forks the scope, so the captured scope is the one active inside the callback. + expect(getCapturedScopesOnSpan(span).scope).toBe(scopeInCallback); + expect(getCapturedScopesOnSpan(span).isolationScope).toBe(getIsolationScope()); }); it('creates & finishes span', async () => { @@ -1424,11 +1454,9 @@ describe('startInactiveSpan', () => { transaction: 'GET users/[id]', }); - // Non-recording spans must still carry the captured scopes, so consumers like - // SentryTraceProvider can read them to fork the isolation scope onto the context. - const captured = getCapturedScopesOnSpan(span); - expect(captured.scope).toBe(getCurrentScope()); - expect(captured.isolationScope).toBe(getIsolationScope()); + // Scopes are captured on the placeholder so consumers (e.g. SentryTraceProvider) can read them. + expect(getCapturedScopesOnSpan(span).scope).toBe(getCurrentScope()); + expect(getCapturedScopesOnSpan(span).isolationScope).toBe(getIsolationScope()); }); it('creates & finishes span', async () => { @@ -2584,6 +2612,25 @@ describe('ignoreSpans (core path, streaming)', () => { expect(spyOnDroppedEvent).toHaveBeenCalledWith('ignored', 'span'); }); + it('exposes parent_span_id on an ignored child span', () => { + const options = getDefaultTestClientOptions({ + tracesSampleRate: 1, + traceLifecycle: 'stream', + ignoreSpans: ['ignored-child'], + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + startSpan({ name: 'root' }, rootSpan => { + startSpan({ name: 'ignored-child' }, span => { + expect(span).toBeInstanceOf(SentryNonRecordingSpan); + // The ignored span still links to its parent so `spanToJSON` can surface it. + expect(spanToJSON(span).parent_span_id).toBe(rootSpan.spanContext().spanId); + }); + }); + }); + it('children of ignored child spans parent to grandparent', () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 1, From d34ae34d69620b3705686f6d4c90a35fb4d5721f Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 20:52:11 +0200 Subject: [PATCH 23/25] fix(core): Don't add url-source span names to the TwP DSC Mirror `getDynamicSamplingContextFromSpan`: when a TwP placeholder span's source is "url", omit the local span name from the frozen DSC (URLs may contain PII). Applies to both `createChildOrRootSpan` and `startIdleSpan`. Co-Authored-By: Claude --- packages/core/src/tracing/idleSpan.ts | 13 +++++++++-- packages/core/src/tracing/trace.ts | 8 ++++++- .../core/test/lib/tracing/idleSpan.test.ts | 23 +++++++++++++++++++ packages/core/test/lib/tracing/trace.test.ts | 23 +++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tracing/idleSpan.ts b/packages/core/src/tracing/idleSpan.ts index 59680b46cfba..c8949eae5a15 100644 --- a/packages/core/src/tracing/idleSpan.ts +++ b/packages/core/src/tracing/idleSpan.ts @@ -1,6 +1,9 @@ import { getClient, getCurrentScope, getIsolationScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; -import { SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON } from '../semanticAttributes'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '../semanticAttributes'; import type { DynamicSamplingContext } from '../types/envelope'; import type { Span } from '../types/span'; import type { StartSpanOptions } from '../types/startSpanOptions'; @@ -140,10 +143,16 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti // it's an empty `{}` (a `sentry-trace` header without baggage): we are not head of trace, so // we neither fabricate client fields nor inject the local span name. Only a new trace derives // the DSC from the client and attaches the local span name. + // As in `getDynamicSamplingContextFromSpan`, skip the span name when its source is + // "url" because URLs might contain PII. + // TODO(v11): Only read `SEMANTIC_ATTRIBUTE_SENTRY_SOURCE` again, once we renamed it to `sentry.span.source` + const source = + startSpanOptions.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] ?? + startSpanOptions.attributes?.['sentry.span.source']; const dsc = (propagationContext.dsc ?? dropUndefinedKeys({ ...getDynamicSamplingContextFromSpan(span), - transaction: startSpanOptions.name, + transaction: source === 'url' ? undefined : startSpanOptions.name, })) satisfies Partial; freezeDscOnSpan(span, dsc); diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 108dff33c57d..3d2e81dd6fa9 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -386,10 +386,16 @@ function createChildOrRootSpan({ // (a `sentry-trace` header without baggage): we are not head of trace, so we neither // fabricate client fields nor inject the local span name. Only when starting a new // trace do we derive the DSC from the client and attach the local span name. + // As in `getDynamicSamplingContextFromSpan`, skip the span name when its source is + // "url" because URLs might contain PII. + // TODO(v11): Only read `SEMANTIC_ATTRIBUTE_SENTRY_SOURCE` again, once we renamed it to `sentry.span.source` + const source = + spanArguments.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] ?? + spanArguments.attributes?.['sentry.span.source']; const dsc = (propagationContext.dsc ?? dropUndefinedKeys({ ...getDynamicSamplingContextFromSpan(span), - transaction: spanArguments.name, + transaction: source === 'url' ? undefined : spanArguments.name, })) satisfies Partial; freezeDscOnSpan(span, dsc); } diff --git a/packages/core/test/lib/tracing/idleSpan.test.ts b/packages/core/test/lib/tracing/idleSpan.test.ts index 38a8ccd3c87d..3b3be52d5c89 100644 --- a/packages/core/test/lib/tracing/idleSpan.test.ts +++ b/packages/core/test/lib/tracing/idleSpan.test.ts @@ -8,6 +8,7 @@ import { getGlobalScope, getIsolationScope, SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SentryNonRecordingSpan, SentrySpan, setCurrentClient, @@ -143,6 +144,28 @@ describe('startIdleSpan', () => { expect(getDynamicSamplingContextFromSpan(idleSpan)).toEqual({}); }); + it('does not add a url-source span name to the DSC when tracing is disabled', () => { + const options = getDefaultTestClientOptions({ dsn }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + + // Mirrors a browser pageload/navigation span, whose name is the URL path. + const idleSpan = startIdleSpan({ + name: '/users/123e4567-e89b-12d3-a456-426614174000', + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, + }); + + expect(idleSpan).toBeInstanceOf(SentryNonRecordingSpan); + // URLs might contain PII, so the span name must not end up in the DSC. + expect(getDynamicSamplingContextFromSpan(idleSpan)).toEqual({ + environment: 'production', + public_key: '123', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }); + expect(spanToBaggageHeader(idleSpan)).not.toContain('sentry-transaction'); + }); + it('does not finish idle span if there are still active activities', () => { const idleSpan = startIdleSpan({ name: 'foo' }); expect(idleSpan).toBeDefined(); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index ad66c7c113f5..0e065d5c2333 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -9,6 +9,7 @@ import { Scope, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setAsyncContextStrategy, setCurrentClient, spanToJSON, @@ -280,6 +281,28 @@ describe('startSpan', () => { expect(getDynamicSamplingContextFromSpan(span)).toEqual({}); }); + it('does not add a url-source span name to the DSC when tracing is disabled', () => { + const options = getDefaultTestClientOptions({}); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + const span = startSpan( + { + name: '/users/123e4567-e89b-12d3-a456-426614174000', + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, + }, + span => span, + ); + + expect(span).toBeInstanceOf(SentryNonRecordingSpan); + // URLs might contain PII, so the span name must not end up in the DSC. + expect(getDynamicSamplingContextFromSpan(span)).toEqual({ + environment: 'production', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }); + }); + it('exposes parent_span_id on an unsampled non-recording child span', () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 0 }); client = new TestClient(options); From 79e99198421cbbe9cdc3c8e988eccae28afc6fe1 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 21:02:05 +0200 Subject: [PATCH 24/25] fix(node): Let span-derived response status_code win in TwP preprocessEvent In the SentryTraceProvider transaction `preprocessEvent` hook, the response context spread `...event.contexts.response` after setting `status_code` from `http.response.status_code`, so a pre-existing `response.status_code` overrode the span-derived value. Spread the existing response first so the span-derived status_code wins. Co-Authored-By: Claude --- packages/node/src/sdk/initOtel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 54faa7f02b9f..aa55d291e00c 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -165,8 +165,8 @@ function setupSentryTraceProvider( ...(typeof event.contexts?.trace?.data?.['http.response.status_code'] === 'number' ? { response: { - status_code: event.contexts.trace.data['http.response.status_code'], ...event.contexts.response, + status_code: event.contexts.trace.data['http.response.status_code'], }, } : undefined), From 0506397a3510808ad56f0fd7b4b8ef2702d6edeb Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 22:23:26 +0200 Subject: [PATCH 25/25] test(cloudflare): Cover url-source DSC omission in integration tests Add an `includeTransaction` option to the `eventEnvelope` helper and use it where the http.server span source is `url` (the TwP DSC omits the span name as raw URLs may contain PII). Adds a dedicated dsc-url-source suite proving the name is absent from the DSC even when tracing is enabled and a transaction is recorded. Co-Authored-By: Claude --- .../cloudflare-integration-tests/expect.ts | 12 ++- .../suites/double-instrumentation/test.ts | 40 +++++---- .../suites/integrations/http-server/test.ts | 86 +++++++++++-------- .../suites/tracing/dsc-url-source/index.ts | 20 +++++ .../suites/tracing/dsc-url-source/test.ts | 55 ++++++++++++ .../tracing/dsc-url-source/wrangler.jsonc | 6 ++ 6 files changed, 164 insertions(+), 55 deletions(-) create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/index.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/test.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/wrangler.jsonc diff --git a/dev-packages/cloudflare-integration-tests/expect.ts b/dev-packages/cloudflare-integration-tests/expect.ts index 2a10cdc22b10..aa442722e154 100644 --- a/dev-packages/cloudflare-integration-tests/expect.ts +++ b/dev-packages/cloudflare-integration-tests/expect.ts @@ -64,8 +64,14 @@ export function eventEnvelope( { includeSamplingFields = false, includeSampleRand = false, + includeTransaction = true, sdk = 'cloudflare', - }: { includeSamplingFields?: boolean; includeSampleRand?: boolean; sdk?: 'cloudflare' | 'hono' } = {}, + }: { + includeSamplingFields?: boolean; + includeSampleRand?: boolean; + includeTransaction?: boolean; + sdk?: 'cloudflare' | 'hono'; + } = {}, ): Envelope { return [ { @@ -78,7 +84,9 @@ export function eventEnvelope( trace_id: UUID_MATCHER, ...(includeSamplingFields && { sample_rate: expect.any(String), sampled: expect.any(String) }), ...(includeSampleRand && { sample_rand: expect.stringMatching(/^[01](\.\d+)?$/) }), - transaction: expect.any(String), + // In TwP mode the span name is omitted from the DSC when the span source is `url` + // (raw URLs may contain PII), mirroring `getDynamicSamplingContextFromSpan`. + ...(includeTransaction && { transaction: expect.any(String) }), }, }, [[{ type: 'event' }, expectedEvent(event, { sdk })]], diff --git a/dev-packages/cloudflare-integration-tests/suites/double-instrumentation/test.ts b/dev-packages/cloudflare-integration-tests/suites/double-instrumentation/test.ts index 453cdb8a09f4..ab0b2155349b 100644 --- a/dev-packages/cloudflare-integration-tests/suites/double-instrumentation/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/double-instrumentation/test.ts @@ -5,26 +5,30 @@ import { createRunner } from '../../runner'; it('Only sends one error event when withSentry is called twice', async ({ signal }) => { const runner = createRunner(__dirname) .expect( - eventEnvelope({ - level: 'error', - exception: { - values: [ - { - type: 'Error', - value: 'Test error from double-instrumented worker', - stacktrace: { - frames: expect.any(Array), + eventEnvelope( + { + level: 'error', + exception: { + values: [ + { + type: 'Error', + value: 'Test error from double-instrumented worker', + stacktrace: { + frames: expect.any(Array), + }, + mechanism: { type: 'auto.http.cloudflare', handled: false }, }, - mechanism: { type: 'auto.http.cloudflare', handled: false }, - }, - ], + ], + }, + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.any(String), + }, }, - request: { - headers: expect.any(Object), - method: 'GET', - url: expect.any(String), - }, - }), + // `/error` resolves to a raw URL span (source `url`), so the TwP DSC omits the span name. + { includeTransaction: false }, + ), ) // The http.server span produces a transaction envelope that is sent in parallel with the // error event. Either can arrive first at the mock server, so ignore it here to keep the diff --git a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/test.ts b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/test.ts index 6773a4cf297e..f36849462a42 100644 --- a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/test.ts @@ -5,16 +5,20 @@ import { createRunner } from '../../../runner'; it('Captures JSON request body', async ({ signal }) => { const runner = createRunner(__dirname) .expect( - eventEnvelope({ - level: 'info', - message: 'POST JSON request', - request: { - headers: expect.any(Object), - method: 'POST', - url: expect.stringContaining('/post-json'), - data: '{"username":"test","action":"login"}', + eventEnvelope( + { + level: 'info', + message: 'POST JSON request', + request: { + headers: expect.any(Object), + method: 'POST', + url: expect.stringContaining('/post-json'), + data: '{"username":"test","action":"login"}', + }, }, - }), + // Raw URL span (source `url`), so the TwP DSC omits the span name. + { includeTransaction: false }, + ), ) .start(signal); @@ -29,16 +33,20 @@ it('Captures JSON request body', async ({ signal }) => { it('Captures form-urlencoded request body', async ({ signal }) => { const runner = createRunner(__dirname) .expect( - eventEnvelope({ - level: 'info', - message: 'POST form request', - request: { - headers: expect.any(Object), - method: 'POST', - url: expect.stringContaining('/post-form'), - data: 'username=test&password=secret', + eventEnvelope( + { + level: 'info', + message: 'POST form request', + request: { + headers: expect.any(Object), + method: 'POST', + url: expect.stringContaining('/post-form'), + data: 'username=test&password=secret', + }, }, - }), + // Raw URL span (source `url`), so the TwP DSC omits the span name. + { includeTransaction: false }, + ), ) .start(signal); @@ -53,16 +61,20 @@ it('Captures form-urlencoded request body', async ({ signal }) => { it('Captures plain text request body', async ({ signal }) => { const runner = createRunner(__dirname) .expect( - eventEnvelope({ - level: 'info', - message: 'POST text request', - request: { - headers: expect.any(Object), - method: 'POST', - url: expect.stringContaining('/post-text'), - data: 'This is plain text content', + eventEnvelope( + { + level: 'info', + message: 'POST text request', + request: { + headers: expect.any(Object), + method: 'POST', + url: expect.stringContaining('/post-text'), + data: 'This is plain text content', + }, }, - }), + // Raw URL span (source `url`), so the TwP DSC omits the span name. + { includeTransaction: false }, + ), ) .start(signal); @@ -77,15 +89,19 @@ it('Captures plain text request body', async ({ signal }) => { it('Does not capture body for POST without content', async ({ signal }) => { const runner = createRunner(__dirname) .expect( - eventEnvelope({ - level: 'info', - message: 'POST no body request', - request: { - headers: expect.any(Object), - method: 'POST', - url: expect.stringContaining('/post-no-body'), + eventEnvelope( + { + level: 'info', + message: 'POST no body request', + request: { + headers: expect.any(Object), + method: 'POST', + url: expect.stringContaining('/post-no-body'), + }, }, - }), + // Raw URL span (source `url`), so the TwP DSC omits the span name. + { includeTransaction: false }, + ), ) .start(signal); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/index.ts new file mode 100644 index 000000000000..2f914b401772 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/index.ts @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/cloudflare'; + +interface Env { + SENTRY_DSN: string; +} + +// Tracing is enabled (not TwP), but the route is a raw, non-parametrized URL so the +// http.server span source is `url`. The span name must therefore be omitted from the +// DSC (raw URLs may contain PII), even though a real transaction is recorded. +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + tracesSampleRate: 1.0, + }), + { + async fetch(_request, _env, _ctx) { + throw new Error('Test error from URL-source worker'); + }, + }, +); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/test.ts new file mode 100644 index 000000000000..14ea900e179a --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/test.ts @@ -0,0 +1,55 @@ +import { expect, it } from 'vitest'; +import { eventEnvelope } from '../../../expect'; +import { createRunner } from '../../../runner'; + +it('omits the span name from the DSC for url-source spans when tracing is enabled', async ({ signal }) => { + const runner = createRunner(__dirname) + // Error event: because tracing is enabled, the DSC carries the sampling fields. But the span + // source is `url`, so the span name is omitted from the DSC (raw URLs may contain PII). + .expect( + eventEnvelope( + { + level: 'error', + exception: { + values: [ + { + type: 'Error', + value: 'Test error from URL-source worker', + stacktrace: { + frames: expect.any(Array), + }, + mechanism: { type: 'auto.http.cloudflare', handled: false }, + }, + ], + }, + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.any(String), + }, + }, + { includeSamplingFields: true, includeSampleRand: true, includeTransaction: false }, + ), + ) + // Transaction event: proves we are NOT in TwP — the span is recorded with a `url` source and + // carries the name on the event itself, even though it is intentionally absent from the DSC. + .expect(envelope => { + const transactionEvent = envelope[1]?.[0]?.[1]; + expect(transactionEvent).toEqual( + expect.objectContaining({ + type: 'transaction', + transaction: 'GET /error', + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'http.server', + data: expect.objectContaining({ 'sentry.source': 'url' }), + }), + }), + }), + ); + }) + .unordered() + .start(signal); + await runner.makeRequest('get', '/error', { expectError: true }); + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/wrangler.jsonc new file mode 100644 index 000000000000..d6be01281f0c --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "worker-name", + "compatibility_date": "2025-06-17", + "main": "index.ts", + "compatibility_flags": ["nodejs_compat"], +}