Skip to content

Commit 2187b99

Browse files
authored
feat(opentelemetry): Support forceTransaction in OTEL (#10807)
This implements `forceTransaction` for otel/node.
1 parent afcec2d commit 2187b99

File tree

2 files changed

+372
-8
lines changed

2 files changed

+372
-8
lines changed

packages/opentelemetry/src/trace.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import type { Context, Span, SpanOptions, Tracer } from '@opentelemetry/api';
1+
import type { Context, Span, SpanContext, SpanOptions, Tracer } from '@opentelemetry/api';
2+
import { TraceFlags } from '@opentelemetry/api';
23
import { context } from '@opentelemetry/api';
34
import { SpanStatusCode, trace } from '@opentelemetry/api';
4-
import { suppressTracing } from '@opentelemetry/core';
5+
import { TraceState, suppressTracing } from '@opentelemetry/core';
56
import { SDK_VERSION, getClient, getCurrentScope, handleCallbackErrors } from '@sentry/core';
67
import type { Client, Scope } from '@sentry/types';
8+
import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils';
9+
import { SENTRY_TRACE_STATE_DSC } from './constants';
710

811
import { InternalSentrySemanticAttributes } from './semanticAttributes';
912
import type { OpenTelemetryClient, OpenTelemetrySpanContext } from './types';
1013
import { getContextFromScope } from './utils/contextData';
14+
import { getDynamicSamplingContextFromSpan } from './utils/dynamicSamplingContext';
15+
import { getRootSpan } from './utils/getActiveSpan';
1116
import { setSpanMetadata } from './utils/spanData';
1217

1318
/**
@@ -24,7 +29,7 @@ export function startSpan<T>(options: OpenTelemetrySpanContext, callback: (span:
2429

2530
const { name } = options;
2631

27-
const activeCtx = getContext(options.scope);
32+
const activeCtx = getContext(options.scope, options.forceTransaction);
2833
const shouldSkipSpan = options.onlyIfParent && !trace.getSpan(activeCtx);
2934
const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx;
3035

@@ -57,7 +62,7 @@ export function startSpanManual<T>(options: OpenTelemetrySpanContext, callback:
5762

5863
const { name } = options;
5964

60-
const activeCtx = getContext(options.scope);
65+
const activeCtx = getContext(options.scope, options.forceTransaction);
6166
const shouldSkipSpan = options.onlyIfParent && !trace.getSpan(activeCtx);
6267
const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx;
6368

@@ -95,7 +100,7 @@ export function startInactiveSpan(options: OpenTelemetrySpanContext): Span {
95100

96101
const { name } = options;
97102

98-
const activeCtx = getContext(options.scope);
103+
const activeCtx = getContext(options.scope, options.forceTransaction);
99104
const shouldSkipSpan = options.onlyIfParent && !trace.getSpan(activeCtx);
100105
const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx;
101106

@@ -166,7 +171,50 @@ function ensureTimestampInMilliseconds(timestamp: number): number {
166171
return isMs ? timestamp * 1000 : timestamp;
167172
}
168173

169-
function getContext(scope?: Scope): Context {
174+
function getContext(scope: Scope | undefined, forceTransaction: boolean | undefined): Context {
175+
const ctx = getContextForScope(scope);
176+
177+
if (!forceTransaction) {
178+
return ctx;
179+
}
180+
181+
// Else we need to "fix" the context to have no parent span
182+
const parentSpan = trace.getSpan(ctx);
183+
184+
// If there is no parent span, all good, nothing to do!
185+
if (!parentSpan) {
186+
return ctx;
187+
}
188+
189+
// Else, we need to do two things:
190+
// 1. Unset the parent span from the context, so we'll create a new root span
191+
// 2. Ensure the propagation context is correct, so we'll continue from the parent span
192+
const ctxWithoutSpan = trace.deleteSpan(ctx);
193+
194+
const { spanId, traceId, traceFlags } = parentSpan.spanContext();
195+
// eslint-disable-next-line no-bitwise
196+
const sampled = Boolean(traceFlags & TraceFlags.SAMPLED);
197+
198+
const rootSpan = getRootSpan(parentSpan);
199+
const dsc = getDynamicSamplingContextFromSpan(rootSpan);
200+
const dscString = dynamicSamplingContextToSentryBaggageHeader(dsc);
201+
202+
const traceState = dscString ? new TraceState().set(SENTRY_TRACE_STATE_DSC, dscString) : undefined;
203+
204+
const spanContext: SpanContext = {
205+
traceId,
206+
spanId,
207+
isRemote: true,
208+
traceFlags: sampled ? TraceFlags.SAMPLED : TraceFlags.NONE,
209+
traceState,
210+
};
211+
212+
const ctxWithSpanContext = trace.setSpanContext(ctxWithoutSpan, spanContext);
213+
214+
return ctxWithSpanContext;
215+
}
216+
217+
function getContextForScope(scope?: Scope): Context {
170218
if (scope) {
171219
const ctx = getContextFromScope(scope);
172220
if (ctx) {

0 commit comments

Comments
 (0)