diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index b3eb10f686d7..b7f00e14baef 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -222,8 +222,11 @@ export abstract class BaseClient implements Client { let eventId: string | undefined = hint && hint.event_id; + const sdkProcessingMetadata = event.sdkProcessingMetadata || {}; + const capturedSpanScope: Scope | undefined = sdkProcessingMetadata.capturedSpanScope; + this._process( - this._captureEvent(event, hint, scope).then(result => { + this._captureEvent(event, hint, capturedSpanScope || scope).then(result => { eventId = result; }), ); @@ -753,7 +756,10 @@ export abstract class BaseClient implements Client { const dataCategory: DataCategory = eventType === 'replay_event' ? 'replay' : eventType; - return this._prepareEvent(event, hint, scope) + const sdkProcessingMetadata = event.sdkProcessingMetadata || {}; + const capturedSpanIsolationScope: Scope | undefined = sdkProcessingMetadata.capturedSpanIsolationScope; + + return this._prepareEvent(event, hint, scope, capturedSpanIsolationScope) .then(prepared => { if (prepared === null) { this.recordDroppedEvent('event_processor', dataCategory, event); diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 1f8b45d5fd97..832180ef3c72 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,6 +1,6 @@ -import type { Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types'; +import type { Scope, Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types'; -import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; +import { addNonEnumerableProperty, dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import { getCurrentScope, withScope } from '../exports'; @@ -189,20 +189,22 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { return undefined; } + const isolationScope = getIsolationScope(); + const scope = getCurrentScope(); + + let span: Span | undefined; + if (parentSpan) { // eslint-disable-next-line deprecation/deprecation - return parentSpan.startChild(ctx); + span = parentSpan.startChild(ctx); } else { - const isolationScope = getIsolationScope(); - const scope = getCurrentScope(); - const { traceId, dsc, parentSpanId, sampled } = { ...isolationScope.getPropagationContext(), ...scope.getPropagationContext(), }; // eslint-disable-next-line deprecation/deprecation - return hub.startTransaction({ + span = hub.startTransaction({ traceId, parentSpanId, parentSampled: sampled, @@ -214,6 +216,10 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { }, }); } + + setCapturedScopesOnSpan(span, scope, isolationScope); + + return span; } /** @@ -335,20 +341,21 @@ function createChildSpanOrTransaction( return undefined; } + const isolationScope = getIsolationScope(); + const scope = getCurrentScope(); + + let span: Span | undefined; if (parentSpan) { // eslint-disable-next-line deprecation/deprecation - return parentSpan.startChild(ctx); + span = parentSpan.startChild(ctx); } else { - const isolationScope = getIsolationScope(); - const scope = getCurrentScope(); - const { traceId, dsc, parentSpanId, sampled } = { ...isolationScope.getPropagationContext(), ...scope.getPropagationContext(), }; // eslint-disable-next-line deprecation/deprecation - return hub.startTransaction({ + span = hub.startTransaction({ traceId, parentSpanId, parentSampled: sampled, @@ -360,6 +367,10 @@ function createChildSpanOrTransaction( }, }); } + + setCapturedScopesOnSpan(span, scope, isolationScope); + + return span; } /** @@ -379,3 +390,28 @@ function normalizeContext(context: StartSpanOptions): TransactionContext { return context; } + +const SCOPE_ON_START_SPAN_FIELD = '_sentryScope'; +const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope'; + +type SpanWithScopes = Span & { + [SCOPE_ON_START_SPAN_FIELD]?: Scope; + [ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: Scope; +}; + +function setCapturedScopesOnSpan(span: Span | undefined, scope: Scope, isolationScope: Scope): void { + if (span) { + addNonEnumerableProperty(span, ISOLATION_SCOPE_ON_START_SPAN_FIELD, isolationScope); + addNonEnumerableProperty(span, SCOPE_ON_START_SPAN_FIELD, scope); + } +} + +/** + * Grabs the scope and isolation scope off a span that were active when the span was started. + */ +export function getCapturedScopesOnSpan(span: Span): { scope?: Scope; isolationScope?: Scope } { + return { + scope: (span as SpanWithScopes)[SCOPE_ON_START_SPAN_FIELD], + isolationScope: (span as SpanWithScopes)[ISOLATION_SCOPE_ON_START_SPAN_FIELD], + }; +} diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 490714636fe9..026723929471 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -19,6 +19,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE import { spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils'; import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { Span as SpanClass, SpanRecorder } from './span'; +import { getCapturedScopesOnSpan } from './trace'; /** JSDoc */ export class Transaction extends SpanClass implements TransactionInterface { @@ -303,6 +304,8 @@ export class Transaction extends SpanClass implements TransactionInterface { }); } + const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = getCapturedScopesOnSpan(this); + // eslint-disable-next-line deprecation/deprecation const { metadata } = this; // eslint-disable-next-line deprecation/deprecation @@ -324,6 +327,8 @@ export class Transaction extends SpanClass implements TransactionInterface { type: 'transaction', sdkProcessingMetadata: { ...metadata, + capturedSpanScope, + capturedSpanIsolationScope, dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), }, ...(source && { diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 265a34195f71..4c9190e56b6a 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,3 +1,4 @@ +import type { Span as SpanType } from '@sentry/types'; import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_OP, @@ -387,9 +388,50 @@ describe('startSpan', () => { transactionContext: expect.objectContaining({ name: 'outer', parentSampled: undefined }), }); }); + + it('includes the scope at the time the span was started when finished', async () => { + const transactionEventPromise = new Promise(resolve => { + setCurrentClient( + new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + beforeSendTransaction(event) { + resolve(event); + return event; + }, + }), + ), + ); + }); + + withScope(scope1 => { + scope1.setTag('scope', 1); + startSpanManual({ name: 'my-span' }, span => { + withScope(scope2 => { + scope2.setTag('scope', 2); + span?.end(); + }); + }); + }); + + expect(await transactionEventPromise).toMatchObject({ + tags: { + scope: 1, + }, + }); + }); }); describe('startSpanManual', () => { + beforeEach(() => { + const options = getDefaultTestClientOptions({ tracesSampleRate: 1 }); + client = new TestClient(options); + hub = new Hub(client); + // eslint-disable-next-line deprecation/deprecation + makeMain(hub); + }); + it('creates & finishes span', async () => { startSpanManual({ name: 'GET users/[id]' }, (span, finish) => { expect(span).toBeDefined(); @@ -492,6 +534,14 @@ describe('startSpanManual', () => { }); describe('startInactiveSpan', () => { + beforeEach(() => { + const options = getDefaultTestClientOptions({ tracesSampleRate: 1 }); + client = new TestClient(options); + hub = new Hub(client); + // eslint-disable-next-line deprecation/deprecation + makeMain(hub); + }); + it('creates & finishes span', async () => { const span = startInactiveSpan({ name: 'GET users/[id]' }); @@ -571,6 +621,41 @@ describe('startInactiveSpan', () => { expect(span).toBeDefined(); }); }); + + it('includes the scope at the time the span was started when finished', async () => { + const transactionEventPromise = new Promise(resolve => { + setCurrentClient( + new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + beforeSendTransaction(event) { + resolve(event); + return event; + }, + }), + ), + ); + }); + + let span: SpanType | undefined; + + withScope(scope => { + scope.setTag('scope', 1); + span = startInactiveSpan({ name: 'my-span' }); + }); + + withScope(scope => { + scope.setTag('scope', 2); + span?.end(); + }); + + expect(await transactionEventPromise).toMatchObject({ + tags: { + scope: 1, + }, + }); + }); }); describe('continueTrace', () => { diff --git a/packages/node/test/performance.test.ts b/packages/node/test/performance.test.ts index 0f57dd4166e6..513a3e95a7c0 100644 --- a/packages/node/test/performance.test.ts +++ b/packages/node/test/performance.test.ts @@ -1,5 +1,13 @@ -import { setAsyncContextStrategy, setCurrentClient, startSpan, startSpanManual } from '@sentry/core'; -import type { TransactionEvent } from '@sentry/types'; +import { + setAsyncContextStrategy, + setCurrentClient, + startInactiveSpan, + startSpan, + startSpanManual, + withIsolationScope, + withScope, +} from '@sentry/core'; +import type { Span, TransactionEvent } from '@sentry/types'; import { NodeClient, defaultStackParser } from '../src'; import { setNodeAsyncContextStrategy } from '../src/async'; import { getDefaultNodeClientOptions } from './helper/node-client-options'; @@ -147,4 +155,90 @@ describe('startSpanManual()', () => { expect(transactionEvent.spans).toContainEqual(expect.objectContaining({ description: 'second' })); }); + + it('should use the scopes at time of creation instead of the scopes at time of termination', async () => { + const transactionEventPromise = new Promise(resolve => { + setCurrentClient( + new NodeClient( + getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + tracesSampleRate: 1, + beforeSendTransaction: event => { + resolve(event); + return null; + }, + dsn, + }), + ), + ); + }); + + withIsolationScope(isolationScope1 => { + isolationScope1.setTag('isolationScope', 1); + withScope(scope1 => { + scope1.setTag('scope', 1); + startSpanManual({ name: 'my-span' }, span => { + withIsolationScope(isolationScope2 => { + isolationScope2.setTag('isolationScope', 2); + withScope(scope2 => { + scope2.setTag('scope', 2); + span?.end(); + }); + }); + }); + }); + }); + + expect(await transactionEventPromise).toMatchObject({ + tags: { + scope: 1, + isolationScope: 1, + }, + }); + }); +}); + +describe('startInactiveSpan()', () => { + it('should use the scopes at time of creation instead of the scopes at time of termination', async () => { + const transactionEventPromise = new Promise(resolve => { + setCurrentClient( + new NodeClient( + getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + tracesSampleRate: 1, + beforeSendTransaction: event => { + resolve(event); + return null; + }, + dsn, + }), + ), + ); + }); + + let span: Span | undefined; + + withIsolationScope(isolationScope => { + isolationScope.setTag('isolationScope', 1); + withScope(scope => { + scope.setTag('scope', 1); + span = startInactiveSpan({ name: 'my-span' }); + }); + }); + + withIsolationScope(isolationScope => { + isolationScope.setTag('isolationScope', 2); + withScope(scope => { + scope.setTag('scope', 2); + span?.end(); + }); + }); + + expect(await transactionEventPromise).toMatchObject({ + tags: { + scope: 1, + isolationScope: 1, + }, + }); + }); });