Skip to content

Commit 372e405

Browse files
authored
feat(core): Allow to pass forceTransaction to startSpan() APIs (#10749)
This will ensure a span is sent as a transaction to Sentry. This only implements this option for the core implementation, not yet for OTEL - that is a follow up here: #10807
1 parent 1eeea62 commit 372e405

File tree

5 files changed

+402
-58
lines changed

5 files changed

+402
-58
lines changed

packages/core/src/tracing/trace.ts

Lines changed: 71 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import type { Hub, Scope, Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types';
22

33
import { addNonEnumerableProperty, dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils';
4+
import { getDynamicSamplingContextFromSpan } from '.';
45
import { getCurrentScope, getIsolationScope, withScope } from '../currentScopes';
56

67
import { DEBUG_BUILD } from '../debug-build';
78
import { getCurrentHub } from '../hub';
89
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
910
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
10-
import { spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
11+
import { spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
1112

1213
/**
1314
* Wraps a function with a transaction/span and finishes the span after the function is done.
@@ -21,7 +22,7 @@ import { spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
2122
* and the `span` returned from the callback will be undefined.
2223
*/
2324
export function startSpan<T>(context: StartSpanOptions, callback: (span: Span | undefined) => T): T {
24-
const ctx = normalizeContext(context);
25+
const spanContext = normalizeContext(context);
2526

2627
return withScope(context.scope, scope => {
2728
// eslint-disable-next-line deprecation/deprecation
@@ -30,10 +31,14 @@ export function startSpan<T>(context: StartSpanOptions, callback: (span: Span |
3031
const parentSpan = scope.getSpan();
3132

3233
const shouldSkipSpan = context.onlyIfParent && !parentSpan;
33-
const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx);
34-
35-
// eslint-disable-next-line deprecation/deprecation
36-
scope.setSpan(activeSpan);
34+
const activeSpan = shouldSkipSpan
35+
? undefined
36+
: createChildSpanOrTransaction(hub, {
37+
parentSpan,
38+
spanContext,
39+
forceTransaction: context.forceTransaction,
40+
scope,
41+
});
3742

3843
return handleCallbackErrors(
3944
() => callback(activeSpan),
@@ -66,7 +71,7 @@ export function startSpanManual<T>(
6671
context: StartSpanOptions,
6772
callback: (span: Span | undefined, finish: () => void) => T,
6873
): T {
69-
const ctx = normalizeContext(context);
74+
const spanContext = normalizeContext(context);
7075

7176
return withScope(context.scope, scope => {
7277
// eslint-disable-next-line deprecation/deprecation
@@ -75,10 +80,14 @@ export function startSpanManual<T>(
7580
const parentSpan = scope.getSpan();
7681

7782
const shouldSkipSpan = context.onlyIfParent && !parentSpan;
78-
const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx);
79-
80-
// eslint-disable-next-line deprecation/deprecation
81-
scope.setSpan(activeSpan);
83+
const activeSpan = shouldSkipSpan
84+
? undefined
85+
: createChildSpanOrTransaction(hub, {
86+
parentSpan,
87+
spanContext,
88+
forceTransaction: context.forceTransaction,
89+
scope,
90+
});
8291

8392
function finishAndSetSpan(): void {
8493
activeSpan && activeSpan.end();
@@ -114,7 +123,7 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined {
114123
return undefined;
115124
}
116125

117-
const ctx = normalizeContext(context);
126+
const spanContext = normalizeContext(context);
118127
// eslint-disable-next-line deprecation/deprecation
119128
const hub = getCurrentHub();
120129
const parentSpan = context.scope
@@ -128,41 +137,19 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined {
128137
return undefined;
129138
}
130139

131-
const isolationScope = getIsolationScope();
132-
const scope = getCurrentScope();
133-
134-
let span: Span | undefined;
135-
136-
if (parentSpan) {
137-
// eslint-disable-next-line deprecation/deprecation
138-
span = parentSpan.startChild(ctx);
139-
} else {
140-
const { traceId, dsc, parentSpanId, sampled } = {
141-
...isolationScope.getPropagationContext(),
142-
...scope.getPropagationContext(),
143-
};
144-
145-
// eslint-disable-next-line deprecation/deprecation
146-
span = hub.startTransaction({
147-
traceId,
148-
parentSpanId,
149-
parentSampled: sampled,
150-
...ctx,
151-
metadata: {
152-
dynamicSamplingContext: dsc,
153-
// eslint-disable-next-line deprecation/deprecation
154-
...ctx.metadata,
155-
},
156-
});
157-
}
158-
159-
if (parentSpan) {
160-
addChildSpanToSpan(parentSpan, span);
161-
}
140+
const scope = context.scope || getCurrentScope();
162141

163-
setCapturedScopesOnSpan(span, scope, isolationScope);
142+
// Even though we don't actually want to make this span active on the current scope,
143+
// we need to make it active on a temporary scope that we use for event processing
144+
// as otherwise, it won't pick the correct span for the event when processing it
145+
const temporaryScope = scope.clone();
164146

165-
return span;
147+
return createChildSpanOrTransaction(hub, {
148+
parentSpan,
149+
spanContext,
150+
forceTransaction: context.forceTransaction,
151+
scope: temporaryScope,
152+
});
166153
}
167154

168155
/**
@@ -277,20 +264,47 @@ export const continueTrace: ContinueTrace = <V>(
277264

278265
function createChildSpanOrTransaction(
279266
hub: Hub,
280-
parentSpan: Span | undefined,
281-
ctx: TransactionContext,
267+
{
268+
parentSpan,
269+
spanContext,
270+
forceTransaction,
271+
scope,
272+
}: {
273+
parentSpan: Span | undefined;
274+
spanContext: TransactionContext;
275+
forceTransaction?: boolean;
276+
scope: Scope;
277+
},
282278
): Span | undefined {
283279
if (!hasTracingEnabled()) {
284280
return undefined;
285281
}
286282

287283
const isolationScope = getIsolationScope();
288-
const scope = getCurrentScope();
289284

290285
let span: Span | undefined;
291-
if (parentSpan) {
286+
if (parentSpan && !forceTransaction) {
287+
// eslint-disable-next-line deprecation/deprecation
288+
span = parentSpan.startChild(spanContext);
289+
addChildSpanToSpan(parentSpan, span);
290+
} else if (parentSpan) {
291+
// If we forced a transaction but have a parent span, make sure to continue from the parent span, not the scope
292+
const dsc = getDynamicSamplingContextFromSpan(parentSpan);
293+
const { traceId, spanId: parentSpanId } = parentSpan.spanContext();
294+
const sampled = spanIsSampled(parentSpan);
295+
292296
// eslint-disable-next-line deprecation/deprecation
293-
span = parentSpan.startChild(ctx);
297+
span = hub.startTransaction({
298+
traceId,
299+
parentSpanId,
300+
parentSampled: sampled,
301+
...spanContext,
302+
metadata: {
303+
dynamicSamplingContext: dsc,
304+
// eslint-disable-next-line deprecation/deprecation
305+
...spanContext.metadata,
306+
},
307+
});
294308
} else {
295309
const { traceId, dsc, parentSpanId, sampled } = {
296310
...isolationScope.getPropagationContext(),
@@ -302,18 +316,20 @@ function createChildSpanOrTransaction(
302316
traceId,
303317
parentSpanId,
304318
parentSampled: sampled,
305-
...ctx,
319+
...spanContext,
306320
metadata: {
307321
dynamicSamplingContext: dsc,
308322
// eslint-disable-next-line deprecation/deprecation
309-
...ctx.metadata,
323+
...spanContext.metadata,
310324
},
311325
});
312326
}
313327

314-
if (parentSpan) {
315-
addChildSpanToSpan(parentSpan, span);
316-
}
328+
// We always set this as active span on the scope
329+
// In the case of this being an inactive span, we ensure to pass a detached scope in here in the first place
330+
// But by having this here, we can ensure that the lookup through `getCapturedScopesOnSpan` results in the correct scope & span combo
331+
// eslint-disable-next-line deprecation/deprecation
332+
scope.setSpan(span);
317333

318334
setCapturedScopesOnSpan(span, scope, isolationScope);
319335

packages/core/src/tracing/transaction.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,9 @@ export class Transaction extends SentrySpan implements TransactionInterface {
301301
...metadata,
302302
capturedSpanScope,
303303
capturedSpanIsolationScope,
304-
dynamicSamplingContext: getDynamicSamplingContextFromSpan(this),
304+
...dropUndefinedKeys({
305+
dynamicSamplingContext: getDynamicSamplingContextFromSpan(this),
306+
}),
305307
},
306308
_metrics_summary: getMetricSummaryJsonForSpan(this),
307309
...(source && {

0 commit comments

Comments
 (0)