Skip to content

Commit d884c91

Browse files
committed
feat(otel): Add extract functionality to SentryPropagator
1 parent 0d695fe commit d884c91

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,9 +2,10 @@ 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';
77

8+
import { SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, SENTRY_TRACE_PARENT_CONTEXT_KEY } from './constants';
89
import { isSentryRequestSpan } from './utils/is-sentry-request';
910
import { mapOtelStatus } from './utils/map-otel-status';
1011
import { parseSpanDescription } from './utils/parse-otel-span-description';
@@ -22,7 +23,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
2223
/**
2324
* @inheritDoc
2425
*/
25-
public onStart(otelSpan: OtelSpan, _parentContext: Context): void {
26+
public onStart(otelSpan: OtelSpan, parentContext: Context): void {
2627
const hub = getCurrentHub();
2728
if (!hub) {
2829
__DEBUG_BUILD__ && logger.error('SentrySpanProcessor has triggered onStart before a hub has been setup.');
@@ -51,7 +52,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
5152

5253
SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, sentryChildSpan);
5354
} else {
54-
const traceCtx = getTraceData(otelSpan);
55+
const traceCtx = getTraceData(otelSpan, parentContext);
5556
const transaction = hub.startTransaction({
5657
name: otelSpan.name,
5758
...traceCtx,
@@ -117,13 +118,27 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
117118
}
118119
}
119120

120-
function getTraceData(otelSpan: OtelSpan): Partial<TransactionContext> {
121+
function getTraceData(otelSpan: OtelSpan, parentContext: Context): Partial<TransactionContext> {
121122
const spanContext = otelSpan.spanContext();
122123
const traceId = spanContext.traceId;
123124
const spanId = spanContext.spanId;
124125

125126
const parentSpanId = otelSpan.parentSpanId;
126-
return { spanId, traceId, parentSpanId };
127+
const traceparentData = parentContext.getValue(SENTRY_TRACE_PARENT_CONTEXT_KEY) as TraceparentData | undefined;
128+
const dynamicSamplingContext = parentContext.getValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY) as
129+
| Partial<DynamicSamplingContext>
130+
| undefined;
131+
132+
return {
133+
spanId,
134+
traceId,
135+
parentSpanId,
136+
metadata: {
137+
// only set dynamic sampling context if sentry-trace header was set
138+
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
139+
source: 'custom',
140+
},
141+
};
127142
}
128143

129144
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 = propagator.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 = propagator.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 = propagator.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 = propagator.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 = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
262+
expect(context.getValue(SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY)).toEqual(undefined);
263+
});
264+
});
207265
});

0 commit comments

Comments
 (0)