diff --git a/packages/opentelemetry-node/src/constants.ts b/packages/opentelemetry-node/src/constants.ts index 5c179dc5335b..55f386f2b39f 100644 --- a/packages/opentelemetry-node/src/constants.ts +++ b/packages/opentelemetry-node/src/constants.ts @@ -1,3 +1,9 @@ +import { createContextKey } from '@opentelemetry/api'; + export const SENTRY_TRACE_HEADER = 'sentry-trace'; export const SENTRY_BAGGAGE_HEADER = 'baggage'; + +export const SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY = createContextKey('SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY'); + +export const SENTRY_TRACE_PARENT_CONTEXT_KEY = createContextKey('SENTRY_TRACE_PARENT_CONTEXT_KEY'); diff --git a/packages/opentelemetry-node/src/propagator.ts b/packages/opentelemetry-node/src/propagator.ts index ab85a2f08d23..e028377a2dfa 100644 --- a/packages/opentelemetry-node/src/propagator.ts +++ b/packages/opentelemetry-node/src/propagator.ts @@ -8,9 +8,18 @@ import { TraceFlags, } from '@opentelemetry/api'; import { isTracingSuppressed } from '@opentelemetry/core'; -import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; +import { + baggageHeaderToDynamicSamplingContext, + dynamicSamplingContextToSentryBaggageHeader, + extractTraceparentData, +} from '@sentry/utils'; -import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from './constants'; +import { + SENTRY_BAGGAGE_HEADER, + SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, + SENTRY_TRACE_HEADER, + SENTRY_TRACE_PARENT_CONTEXT_KEY, +} from './constants'; import { SENTRY_SPAN_PROCESSOR_MAP } from './spanprocessor'; /** @@ -44,8 +53,31 @@ export class SentryPropagator implements TextMapPropagator { /** * @inheritDoc */ - public extract(context: Context, _carrier: unknown, _getter: TextMapGetter): Context { - return context; + public extract(context: Context, carrier: unknown, getter: TextMapGetter): Context { + let newContext = context; + + const maybeSentryTraceHeader: string | string[] | undefined = getter.get(carrier, SENTRY_TRACE_HEADER); + if (maybeSentryTraceHeader) { + const header = Array.isArray(maybeSentryTraceHeader) ? maybeSentryTraceHeader[0] : maybeSentryTraceHeader; + const traceparentData = extractTraceparentData(header); + newContext = newContext.setValue(SENTRY_TRACE_PARENT_CONTEXT_KEY, traceparentData); + if (traceparentData) { + const traceFlags = traceparentData.parentSampled ? TraceFlags.SAMPLED : TraceFlags.NONE; + const spanContext = { + traceId: traceparentData.traceId || '', + spanId: traceparentData.parentSpanId || '', + isRemote: true, + traceFlags, + }; + newContext = trace.setSpanContext(newContext, spanContext); + } + } + + const maybeBaggageHeader = getter.get(carrier, SENTRY_BAGGAGE_HEADER); + const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(maybeBaggageHeader); + newContext = newContext.setValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, dynamicSamplingContext); + + return newContext; } /** diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index de5f39b3a7fc..197a17587327 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -2,9 +2,10 @@ import { Context } from '@opentelemetry/api'; import { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { getCurrentHub, withScope } from '@sentry/core'; import { Transaction } from '@sentry/tracing'; -import { Span as SentrySpan, TransactionContext } from '@sentry/types'; +import { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types'; import { logger } from '@sentry/utils'; +import { SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, SENTRY_TRACE_PARENT_CONTEXT_KEY } from './constants'; import { isSentryRequestSpan } from './utils/is-sentry-request'; import { mapOtelStatus } from './utils/map-otel-status'; import { parseSpanDescription } from './utils/parse-otel-span-description'; @@ -22,7 +23,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { /** * @inheritDoc */ - public onStart(otelSpan: OtelSpan, _parentContext: Context): void { + public onStart(otelSpan: OtelSpan, parentContext: Context): void { const hub = getCurrentHub(); if (!hub) { __DEBUG_BUILD__ && logger.error('SentrySpanProcessor has triggered onStart before a hub has been setup.'); @@ -51,7 +52,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, sentryChildSpan); } else { - const traceCtx = getTraceData(otelSpan); + const traceCtx = getTraceData(otelSpan, parentContext); const transaction = hub.startTransaction({ name: otelSpan.name, ...traceCtx, @@ -117,13 +118,27 @@ export class SentrySpanProcessor implements OtelSpanProcessor { } } -function getTraceData(otelSpan: OtelSpan): Partial { +function getTraceData(otelSpan: OtelSpan, parentContext: Context): Partial { const spanContext = otelSpan.spanContext(); const traceId = spanContext.traceId; const spanId = spanContext.spanId; const parentSpanId = otelSpan.parentSpanId; - return { spanId, traceId, parentSpanId }; + const traceparentData = parentContext.getValue(SENTRY_TRACE_PARENT_CONTEXT_KEY) as TraceparentData | undefined; + const dynamicSamplingContext = parentContext.getValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY) as + | Partial + | undefined; + + return { + spanId, + traceId, + parentSpanId, + metadata: { + // only set dynamic sampling context if sentry-trace header was set + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + source: 'custom', + }, + }; } function finishTransactionWithContextFromOtelData(transaction: Transaction, otelSpan: OtelSpan): void { diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index fd8369ea9fd0..3e85a76033cf 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -1,10 +1,15 @@ -import { defaultTextMapSetter, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api'; +import { defaultTextMapGetter, defaultTextMapSetter, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; import { Hub, makeMain } from '@sentry/core'; import { addExtensionMethods, Transaction } from '@sentry/tracing'; import { TransactionContext } from '@sentry/types'; -import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from '../src/constants'; +import { + SENTRY_BAGGAGE_HEADER, + SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, + SENTRY_TRACE_HEADER, + SENTRY_TRACE_PARENT_CONTEXT_KEY, +} from '../src/constants'; import { SentryPropagator } from '../src/propagator'; import { SENTRY_SPAN_PROCESSOR_MAP } from '../src/spanprocessor'; @@ -204,4 +209,57 @@ describe('SentryPropagator', () => { }); }); }); + + describe('extract', () => { + it('sets sentry span context on the context', () => { + const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(trace.getSpanContext(context)).toEqual({ + isRemote: true, + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + }); + }); + + it('sets defined sentry trace header on context', () => { + const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_TRACE_PARENT_CONTEXT_KEY)).toEqual({ + parentSampled: true, + parentSpanId: '6e0c63257de34c92', + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + }); + }); + + it('sets undefined sentry trace header on context', () => { + const sentryTraceHeader = undefined; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_TRACE_PARENT_CONTEXT_KEY)).toEqual(undefined); + }); + + it('sets defined dynamic sampling context on context', () => { + const baggage = + 'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dsc-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b'; + carrier[SENTRY_BAGGAGE_HEADER] = baggage; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY)).toEqual({ + environment: 'production', + public_key: 'abc', + release: '1.0.0', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + transaction: 'dsc-transaction', + }); + }); + + it('sets undefined dynamic sampling context on context', () => { + const baggage = ''; + carrier[SENTRY_BAGGAGE_HEADER] = baggage; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY)).toEqual(undefined); + }); + }); });