Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,11 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {

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;
}),
);
Expand Down Expand Up @@ -753,7 +756,10 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {

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);
Expand Down
60 changes: 48 additions & 12 deletions packages/core/src/tracing/trace.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -214,6 +216,10 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined {
},
});
}

setCapturedScopesOnSpan(span, scope, isolationScope);

return span;
}

/**
Expand Down Expand Up @@ -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,
Expand All @@ -360,6 +367,10 @@ function createChildSpanOrTransaction(
},
});
}

setCapturedScopesOnSpan(span, scope, isolationScope);

return span;
}

/**
Expand All @@ -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],
};
}
5 changes: 5 additions & 0 deletions packages/core/src/tracing/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -324,6 +327,8 @@ export class Transaction extends SpanClass implements TransactionInterface {
type: 'transaction',
sdkProcessingMetadata: {
...metadata,
capturedSpanScope,
capturedSpanIsolationScope,
dynamicSamplingContext: getDynamicSamplingContextFromSpan(this),
},
...(source && {
Expand Down
85 changes: 85 additions & 0 deletions packages/core/test/lib/tracing/trace.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Span as SpanType } from '@sentry/types';
import {
Hub,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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]' });

Expand Down Expand Up @@ -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', () => {
Expand Down
98 changes: 96 additions & 2 deletions packages/node/test/performance.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<TransactionEvent>(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<TransactionEvent>(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,
},
});
});
});