Skip to content

Commit a10187d

Browse files
authored
feat(core): Allow to pass forceTransaction to startSpan() APIs (backport) (#10819)
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 This is a backport to v7 of #10749
1 parent 38976f3 commit a10187d

File tree

4 files changed

+403
-52
lines changed

4 files changed

+403
-52
lines changed

packages/core/src/tracing/trace.ts

Lines changed: 79 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import type { Scope, Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types';
22

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

56
import { DEBUG_BUILD } from '../debug-build';
67
import { getCurrentScope, withScope } from '../exports';
78
import type { Hub } from '../hub';
89
import { runWithAsyncContext } from '../hub';
910
import { getIsolationScope } from '../hub';
1011
import { getCurrentHub } from '../hub';
12+
import type { Scope as ScopeClass } from '../scope';
1113
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
1214
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
13-
import { spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
15+
import { spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
1416

1517
/**
1618
* Wraps a function with a transaction/span and finishes the span after the function is done.
@@ -40,8 +42,13 @@ export function trace<T>(
4042
// eslint-disable-next-line deprecation/deprecation
4143
const parentSpan = scope.getSpan();
4244

43-
const ctx = normalizeContext(context);
44-
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
45+
const spanContext = normalizeContext(context);
46+
const activeSpan = createChildSpanOrTransaction(hub, {
47+
parentSpan,
48+
spanContext,
49+
forceTransaction: false,
50+
scope,
51+
});
4552

4653
// eslint-disable-next-line deprecation/deprecation
4754
scope.setSpan(activeSpan);
@@ -73,7 +80,7 @@ export function trace<T>(
7380
* and the `span` returned from the callback will be undefined.
7481
*/
7582
export function startSpan<T>(context: StartSpanOptions, callback: (span: Span | undefined) => T): T {
76-
const ctx = normalizeContext(context);
83+
const spanContext = normalizeContext(context);
7784

7885
return runWithAsyncContext(() => {
7986
return withScope(context.scope, scope => {
@@ -83,10 +90,14 @@ export function startSpan<T>(context: StartSpanOptions, callback: (span: Span |
8390
const parentSpan = scope.getSpan();
8491

8592
const shouldSkipSpan = context.onlyIfParent && !parentSpan;
86-
const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx);
87-
88-
// eslint-disable-next-line deprecation/deprecation
89-
scope.setSpan(activeSpan);
93+
const activeSpan = shouldSkipSpan
94+
? undefined
95+
: createChildSpanOrTransaction(hub, {
96+
parentSpan,
97+
spanContext,
98+
forceTransaction: context.forceTransaction,
99+
scope,
100+
});
90101

91102
return handleCallbackErrors(
92103
() => callback(activeSpan),
@@ -125,7 +136,7 @@ export function startSpanManual<T>(
125136
context: StartSpanOptions,
126137
callback: (span: Span | undefined, finish: () => void) => T,
127138
): T {
128-
const ctx = normalizeContext(context);
139+
const spanContext = normalizeContext(context);
129140

130141
return runWithAsyncContext(() => {
131142
return withScope(context.scope, scope => {
@@ -135,10 +146,14 @@ export function startSpanManual<T>(
135146
const parentSpan = scope.getSpan();
136147

137148
const shouldSkipSpan = context.onlyIfParent && !parentSpan;
138-
const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx);
139-
140-
// eslint-disable-next-line deprecation/deprecation
141-
scope.setSpan(activeSpan);
149+
const activeSpan = shouldSkipSpan
150+
? undefined
151+
: createChildSpanOrTransaction(hub, {
152+
parentSpan,
153+
spanContext,
154+
forceTransaction: context.forceTransaction,
155+
scope,
156+
});
142157

143158
function finishAndSetSpan(): void {
144159
activeSpan && activeSpan.end();
@@ -175,7 +190,7 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined {
175190
return undefined;
176191
}
177192

178-
const ctx = normalizeContext(context);
193+
const spanContext = normalizeContext(context);
179194
// eslint-disable-next-line deprecation/deprecation
180195
const hub = getCurrentHub();
181196
const parentSpan = context.scope
@@ -189,37 +204,19 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined {
189204
return undefined;
190205
}
191206

192-
const isolationScope = getIsolationScope();
193-
const scope = getCurrentScope();
194-
195-
let span: Span | undefined;
196-
197-
if (parentSpan) {
198-
// eslint-disable-next-line deprecation/deprecation
199-
span = parentSpan.startChild(ctx);
200-
} else {
201-
const { traceId, dsc, parentSpanId, sampled } = {
202-
...isolationScope.getPropagationContext(),
203-
...scope.getPropagationContext(),
204-
};
205-
206-
// eslint-disable-next-line deprecation/deprecation
207-
span = hub.startTransaction({
208-
traceId,
209-
parentSpanId,
210-
parentSampled: sampled,
211-
...ctx,
212-
metadata: {
213-
dynamicSamplingContext: dsc,
214-
// eslint-disable-next-line deprecation/deprecation
215-
...ctx.metadata,
216-
},
217-
});
218-
}
207+
const scope = context.scope || getCurrentScope();
219208

220-
setCapturedScopesOnSpan(span, scope, isolationScope);
209+
// Even though we don't actually want to make this span active on the current scope,
210+
// we need to make it active on a temporary scope that we use for event processing
211+
// as otherwise, it won't pick the correct span for the event when processing it
212+
const temporaryScope = (scope as ScopeClass).clone();
221213

222-
return span;
214+
return createChildSpanOrTransaction(hub, {
215+
parentSpan,
216+
spanContext,
217+
forceTransaction: context.forceTransaction,
218+
scope: temporaryScope,
219+
});
223220
}
224221

225222
/**
@@ -334,20 +331,46 @@ export const continueTrace: ContinueTrace = <V>(
334331

335332
function createChildSpanOrTransaction(
336333
hub: Hub,
337-
parentSpan: Span | undefined,
338-
ctx: TransactionContext,
334+
{
335+
parentSpan,
336+
spanContext,
337+
forceTransaction,
338+
scope,
339+
}: {
340+
parentSpan: Span | undefined;
341+
spanContext: TransactionContext;
342+
forceTransaction?: boolean;
343+
scope: Scope;
344+
},
339345
): Span | undefined {
340346
if (!hasTracingEnabled()) {
341347
return undefined;
342348
}
343349

344350
const isolationScope = getIsolationScope();
345-
const scope = getCurrentScope();
346351

347352
let span: Span | undefined;
348-
if (parentSpan) {
353+
if (parentSpan && !forceTransaction) {
354+
// eslint-disable-next-line deprecation/deprecation
355+
span = parentSpan.startChild(spanContext);
356+
} else if (parentSpan) {
357+
// If we forced a transaction but have a parent span, make sure to continue from the parent span, not the scope
358+
const dsc = getDynamicSamplingContextFromSpan(parentSpan);
359+
const { traceId, spanId: parentSpanId } = parentSpan.spanContext();
360+
const sampled = spanIsSampled(parentSpan);
361+
349362
// eslint-disable-next-line deprecation/deprecation
350-
span = parentSpan.startChild(ctx);
363+
span = hub.startTransaction({
364+
traceId,
365+
parentSpanId,
366+
parentSampled: sampled,
367+
...spanContext,
368+
metadata: {
369+
dynamicSamplingContext: dsc,
370+
// eslint-disable-next-line deprecation/deprecation
371+
...spanContext.metadata,
372+
},
373+
});
351374
} else {
352375
const { traceId, dsc, parentSpanId, sampled } = {
353376
...isolationScope.getPropagationContext(),
@@ -359,15 +382,21 @@ function createChildSpanOrTransaction(
359382
traceId,
360383
parentSpanId,
361384
parentSampled: sampled,
362-
...ctx,
385+
...spanContext,
363386
metadata: {
364387
dynamicSamplingContext: dsc,
365388
// eslint-disable-next-line deprecation/deprecation
366-
...ctx.metadata,
389+
...spanContext.metadata,
367390
},
368391
});
369392
}
370393

394+
// We always set this as active span on the scope
395+
// In the case of this being an inactive span, we ensure to pass a detached scope in here in the first place
396+
// But by having this here, we can ensure that the lookup through `getCapturedScopesOnSpan` results in the correct scope & span combo
397+
// eslint-disable-next-line deprecation/deprecation
398+
scope.setSpan(span);
399+
371400
setCapturedScopesOnSpan(span, scope, isolationScope);
372401

373402
return span;

packages/core/src/tracing/transaction.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,9 @@ export class Transaction extends SpanClass implements TransactionInterface {
330330
...metadata,
331331
capturedSpanScope,
332332
capturedSpanIsolationScope,
333-
dynamicSamplingContext: getDynamicSamplingContextFromSpan(this),
333+
...dropUndefinedKeys({
334+
dynamicSamplingContext: getDynamicSamplingContextFromSpan(this),
335+
}),
334336
},
335337
_metrics_summary: getMetricSummaryJsonForSpan(this),
336338
...(source && {

0 commit comments

Comments
 (0)