Skip to content

Commit becc6cb

Browse files
committed
feat(otel): Add extract functionality to SentryPropagator
1 parent 7bba4d2 commit becc6cb

File tree

4 files changed

+122
-11
lines changed

4 files changed

+122
-11
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import { createContextKey } from '@opentelemetry/api';
2+
13
export const SENTRY_TRACE_HEADER = 'sentry-trace';
24

35
export const SENTRY_BAGGAGE_HEADER = 'baggage';
6+
7+
export const SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY = createContextKey('SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY');
8+
9+
export const SENTRY_TRACE_PARENT_CONTEXT_KEY = createContextKey('SENTRY_TRACE_PARENT_CONTEXT_KEY');

packages/opentelemetry-node/src/propagator.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,18 @@ import {
88
TraceFlags,
99
} from '@opentelemetry/api';
1010
import { isTracingSuppressed } from '@opentelemetry/core';
11-
import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils';
11+
import {
12+
baggageHeaderToDynamicSamplingContext,
13+
dynamicSamplingContextToSentryBaggageHeader,
14+
extractTraceparentData,
15+
} from '@sentry/utils';
1216

13-
import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from './constants';
17+
import {
18+
SENTRY_BAGGAGE_HEADER,
19+
SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY,
20+
SENTRY_TRACE_HEADER,
21+
SENTRY_TRACE_PARENT_CONTEXT_KEY,
22+
} from './constants';
1423
import { SENTRY_SPAN_PROCESSOR_MAP } from './spanprocessor';
1524

1625
/**
@@ -44,8 +53,31 @@ export class SentryPropagator implements TextMapPropagator {
4453
/**
4554
* @inheritDoc
4655
*/
47-
public extract(context: Context, _carrier: unknown, _getter: TextMapGetter): Context {
48-
return context;
56+
public extract(context: Context, carrier: unknown, getter: TextMapGetter): Context {
57+
let newContext = context;
58+
59+
const maybeSentryTraceHeader: string | string[] | undefined = getter.get(carrier, SENTRY_TRACE_HEADER);
60+
if (maybeSentryTraceHeader) {
61+
const header = Array.isArray(maybeSentryTraceHeader) ? maybeSentryTraceHeader[0] : maybeSentryTraceHeader;
62+
const traceparentData = extractTraceparentData(header);
63+
newContext = newContext.setValue(SENTRY_TRACE_PARENT_CONTEXT_KEY, traceparentData);
64+
if (traceparentData) {
65+
const traceFlags = traceparentData.parentSampled ? TraceFlags.SAMPLED : TraceFlags.NONE;
66+
const spanContext = {
67+
traceId: traceparentData.traceId || '',
68+
spanId: traceparentData.parentSpanId || '',
69+
isRemote: true,
70+
traceFlags,
71+
};
72+
newContext = trace.setSpanContext(newContext, spanContext);
73+
}
74+
}
75+
76+
const maybeBaggageHeader = getter.get(carrier, SENTRY_BAGGAGE_HEADER);
77+
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(maybeBaggageHeader);
78+
newContext = newContext.setValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, dynamicSamplingContext);
79+
80+
return newContext;
4981
}
5082

5183
/**

packages/opentelemetry-node/src/spanprocessor.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { Context } from '@opentelemetry/api';
22
import { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base';
33
import { getCurrentHub, withScope } from '@sentry/core';
44
import { Transaction } from '@sentry/tracing';
5-
import { Span as SentrySpan, TransactionContext } from '@sentry/types';
5+
import { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types';
66
import { logger } from '@sentry/utils';
7+
import { SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, SENTRY_TRACE_PARENT_CONTEXT_KEY } from './constants';
78

89
import { mapOtelStatus } from './utils/map-otel-status';
910
import { parseSpanDescription } from './utils/parse-otel-span-description';
@@ -21,7 +22,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
2122
/**
2223
* @inheritDoc
2324
*/
24-
public onStart(otelSpan: OtelSpan, _parentContext: Context): void {
25+
public onStart(otelSpan: OtelSpan, parentContext: Context): void {
2526
const hub = getCurrentHub();
2627
if (!hub) {
2728
__DEBUG_BUILD__ && logger.error('SentrySpanProcessor has triggered onStart before a hub has been setup.');
@@ -53,7 +54,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
5354

5455
SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, sentryChildSpan);
5556
} else {
56-
const traceCtx = getTraceData(otelSpan);
57+
const traceCtx = getTraceData(otelSpan, parentContext);
5758
const transaction = hub.startTransaction({
5859
name: otelSpan.name,
5960
...traceCtx,
@@ -109,13 +110,27 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
109110
}
110111
}
111112

112-
function getTraceData(otelSpan: OtelSpan): Partial<TransactionContext> {
113+
function getTraceData(otelSpan: OtelSpan, parentContext: Context): Partial<TransactionContext> {
113114
const spanContext = otelSpan.spanContext();
114115
const traceId = spanContext.traceId;
115116
const spanId = spanContext.spanId;
116117

117118
const parentSpanId = otelSpan.parentSpanId;
118-
return { spanId, traceId, parentSpanId };
119+
const traceparentData = parentContext.getValue(SENTRY_TRACE_PARENT_CONTEXT_KEY) as TraceparentData | undefined;
120+
const dynamicSamplingContext = parentContext.getValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY) as
121+
| Partial<DynamicSamplingContext>
122+
| undefined;
123+
124+
return {
125+
spanId,
126+
traceId,
127+
parentSpanId,
128+
metadata: {
129+
// only set dynamic sampling context if sentry-trace header was set
130+
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
131+
source: 'custom',
132+
},
133+
};
119134
}
120135

121136
function finishTransactionWithContextFromOtelData(transaction: Transaction, otelSpan: OtelSpan): void {

packages/opentelemetry-node/test/propagator.test.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import { defaultTextMapSetter, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api';
1+
import { defaultTextMapGetter, defaultTextMapSetter, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api';
22
import { suppressTracing } from '@opentelemetry/core';
33
import { Hub, makeMain } from '@sentry/core';
44
import { addExtensionMethods, Transaction } from '@sentry/tracing';
55
import { TransactionContext } from '@sentry/types';
66

7-
import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from '../src/constants';
7+
import {
8+
SENTRY_BAGGAGE_HEADER,
9+
SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY,
10+
SENTRY_TRACE_HEADER,
11+
SENTRY_TRACE_PARENT_CONTEXT_KEY,
12+
} from '../src/constants';
813
import { SentryPropagator } from '../src/propagator';
914
import { SENTRY_SPAN_PROCESSOR_MAP } from '../src/spanprocessor';
1015

@@ -204,4 +209,57 @@ describe('SentryPropagator', () => {
204209
});
205210
});
206211
});
212+
213+
describe('extract', () => {
214+
it('sets sentry span context on the context', () => {
215+
const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1';
216+
carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader;
217+
const context = propogator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
218+
expect(trace.getSpanContext(context)).toEqual({
219+
isRemote: true,
220+
spanId: '6e0c63257de34c92',
221+
traceFlags: TraceFlags.SAMPLED,
222+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
223+
});
224+
});
225+
226+
it('sets defined sentry trace header on context', () => {
227+
const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1';
228+
carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader;
229+
const context = propogator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
230+
expect(context.getValue(SENTRY_TRACE_PARENT_CONTEXT_KEY)).toEqual({
231+
parentSampled: true,
232+
parentSpanId: '6e0c63257de34c92',
233+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
234+
});
235+
});
236+
237+
it('sets undefined sentry trace header on context', () => {
238+
const sentryTraceHeader = undefined;
239+
carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader;
240+
const context = propogator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
241+
expect(context.getValue(SENTRY_TRACE_PARENT_CONTEXT_KEY)).toEqual(undefined);
242+
});
243+
244+
it('sets defined dynamic sampling context on context', () => {
245+
const baggage =
246+
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dsc-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b';
247+
carrier[SENTRY_BAGGAGE_HEADER] = baggage;
248+
const context = propogator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
249+
expect(context.getValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY)).toEqual({
250+
environment: 'production',
251+
public_key: 'abc',
252+
release: '1.0.0',
253+
trace_id: 'd4cda95b652f4a1592b449d5929fda1b',
254+
transaction: 'dsc-transaction',
255+
});
256+
});
257+
258+
it('sets undefined dynamic sampling context on context', () => {
259+
const baggage = '';
260+
carrier[SENTRY_BAGGAGE_HEADER] = baggage;
261+
const context = propogator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
262+
expect(context.getValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY)).toEqual(undefined);
263+
});
264+
});
207265
});

0 commit comments

Comments
 (0)