Skip to content

Commit f36c268

Browse files
authored
feat(otel): Add inject functionality to SentryPropagator (#6114)
Inject `sentry-trace` and `baggage` headers into context so it's gets propagated on outgoing requests
1 parent e00de10 commit f36c268

File tree

4 files changed

+242
-15
lines changed

4 files changed

+242
-15
lines changed

packages/opentelemetry-node/src/propagator.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
import { Context, TextMapGetter, TextMapPropagator, TextMapSetter } from '@opentelemetry/api';
1+
import {
2+
Context,
3+
isSpanContextValid,
4+
TextMapGetter,
5+
TextMapPropagator,
6+
TextMapSetter,
7+
trace,
8+
TraceFlags,
9+
} from '@opentelemetry/api';
10+
import { isTracingSuppressed } from '@opentelemetry/core';
11+
import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils';
212

313
import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from './constants';
14+
import { SENTRY_SPAN_PROCESSOR_MAP } from './spanprocessor';
415

516
/**
617
* Injects and extracts `sentry-trace` and `baggage` headers from carriers.
@@ -9,8 +20,25 @@ export class SentryPropagator implements TextMapPropagator {
920
/**
1021
* @inheritDoc
1122
*/
12-
public inject(_context: Context, _carrier: unknown, _setter: TextMapSetter): void {
13-
// no-op
23+
public inject(context: Context, carrier: unknown, setter: TextMapSetter): void {
24+
const spanContext = trace.getSpanContext(context);
25+
if (!spanContext || !isSpanContextValid(spanContext) || isTracingSuppressed(context)) {
26+
return;
27+
}
28+
29+
// eslint-disable-next-line no-bitwise
30+
const samplingDecision = spanContext.traceFlags & TraceFlags.SAMPLED ? 1 : 0;
31+
const traceparent = `${spanContext.traceId}-${spanContext.spanId}-${samplingDecision}`;
32+
setter.set(carrier, SENTRY_TRACE_HEADER, traceparent);
33+
34+
const span = SENTRY_SPAN_PROCESSOR_MAP.get(spanContext.spanId);
35+
if (span && span.transaction) {
36+
const dynamicSamplingContext = span.transaction.getDynamicSamplingContext();
37+
const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
38+
if (sentryBaggageHeader) {
39+
setter.set(carrier, SENTRY_BAGGAGE_HEADER, sentryBaggageHeader);
40+
}
41+
}
1442
}
1543

1644
/**

packages/opentelemetry-node/src/spanprocessor.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ import { logger } from '@sentry/utils';
88
import { mapOtelStatus } from './utils/map-otel-status';
99
import { parseSpanDescription } from './utils/parse-otel-span-description';
1010

11+
export const SENTRY_SPAN_PROCESSOR_MAP: Map<SentrySpan['spanId'], SentrySpan> = new Map<
12+
SentrySpan['spanId'],
13+
SentrySpan
14+
>();
15+
1116
/**
1217
* Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via
1318
* the Sentry SDK.
1419
*/
1520
export class SentrySpanProcessor implements OtelSpanProcessor {
16-
// public only for testing
17-
public readonly _map: Map<SentrySpan['spanId'], SentrySpan> = new Map<SentrySpan['spanId'], SentrySpan>();
18-
1921
/**
2022
* @inheritDoc
2123
*/
@@ -39,7 +41,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
3941

4042
// Otel supports having multiple non-nested spans at the same time
4143
// so we cannot use hub.getSpan(), as we cannot rely on this being on the current span
42-
const sentryParentSpan = otelParentSpanId && this._map.get(otelParentSpanId);
44+
const sentryParentSpan = otelParentSpanId && SENTRY_SPAN_PROCESSOR_MAP.get(otelParentSpanId);
4345

4446
if (sentryParentSpan) {
4547
const sentryChildSpan = sentryParentSpan.startChild({
@@ -49,7 +51,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
4951
spanId: otelSpanId,
5052
});
5153

52-
this._map.set(otelSpanId, sentryChildSpan);
54+
SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, sentryChildSpan);
5355
} else {
5456
const traceCtx = getTraceData(otelSpan);
5557
const transaction = hub.startTransaction({
@@ -60,7 +62,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
6062
spanId: otelSpanId,
6163
});
6264

63-
this._map.set(otelSpanId, transaction);
65+
SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, transaction);
6466
}
6567
}
6668

@@ -69,7 +71,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
6971
*/
7072
public onEnd(otelSpan: OtelSpan): void {
7173
const otelSpanId = otelSpan.spanContext().spanId;
72-
const sentrySpan = this._map.get(otelSpanId);
74+
const sentrySpan = SENTRY_SPAN_PROCESSOR_MAP.get(otelSpanId);
7375

7476
if (!sentrySpan) {
7577
__DEBUG_BUILD__ &&
@@ -85,7 +87,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
8587
sentrySpan.finish(convertOtelTimeToSeconds(otelSpan.endTime));
8688
}
8789

88-
this._map.delete(otelSpanId);
90+
SENTRY_SPAN_PROCESSOR_MAP.delete(otelSpanId);
8991
}
9092

9193
/**
Lines changed: 199 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,207 @@
1+
import { defaultTextMapSetter, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api';
2+
import { suppressTracing } from '@opentelemetry/core';
3+
import { Hub, makeMain } from '@sentry/core';
4+
import { addExtensionMethods, Transaction } from '@sentry/tracing';
5+
import { TransactionContext } from '@sentry/types';
6+
17
import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from '../src/constants';
28
import { SentryPropagator } from '../src/propagator';
9+
import { SENTRY_SPAN_PROCESSOR_MAP } from '../src/spanprocessor';
10+
11+
beforeAll(() => {
12+
addExtensionMethods();
13+
});
314

415
describe('SentryPropagator', () => {
5-
const propogator = new SentryPropagator();
16+
const propagator = new SentryPropagator();
17+
let carrier: { [key: string]: unknown };
18+
19+
beforeEach(() => {
20+
carrier = {};
21+
});
622

723
it('returns fields set', () => {
8-
expect(propogator.fields()).toEqual([SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER]);
24+
expect(propagator.fields()).toEqual([SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER]);
25+
});
26+
27+
describe('inject', () => {
28+
describe('sentry-trace', () => {
29+
it.each([
30+
[
31+
'should set sentry-trace header when sampled',
32+
{
33+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
34+
spanId: '6e0c63257de34c92',
35+
traceFlags: TraceFlags.SAMPLED,
36+
},
37+
'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1',
38+
],
39+
[
40+
'should set sentry-trace header when not sampled',
41+
{
42+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
43+
spanId: '6e0c63257de34c92',
44+
traceFlags: TraceFlags.NONE,
45+
},
46+
'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0',
47+
],
48+
[
49+
'should NOT set sentry-trace header when traceId is empty',
50+
{
51+
traceId: '',
52+
spanId: '6e0c63257de34c92',
53+
traceFlags: TraceFlags.SAMPLED,
54+
},
55+
undefined,
56+
],
57+
[
58+
'should NOT set sentry-trace header when spanId is empty',
59+
{
60+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
61+
spanId: '',
62+
traceFlags: TraceFlags.NONE,
63+
},
64+
undefined,
65+
],
66+
])('%s', (_name, spanContext, expected) => {
67+
const context = trace.setSpanContext(ROOT_CONTEXT, spanContext);
68+
propagator.inject(context, carrier, defaultTextMapSetter);
69+
expect(carrier[SENTRY_TRACE_HEADER]).toBe(expected);
70+
});
71+
72+
it('should NOT set sentry-trace header if instrumentation is supressed', () => {
73+
const spanContext = {
74+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
75+
spanId: '6e0c63257de34c92',
76+
traceFlags: TraceFlags.SAMPLED,
77+
};
78+
const context = suppressTracing(trace.setSpanContext(ROOT_CONTEXT, spanContext));
79+
propagator.inject(context, carrier, defaultTextMapSetter);
80+
expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined);
81+
});
82+
});
83+
84+
describe('baggage', () => {
85+
const client = {
86+
getOptions: () => ({
87+
environment: 'production',
88+
release: '1.0.0',
89+
}),
90+
getDsn: () => ({
91+
publicKey: 'abc',
92+
}),
93+
};
94+
// @ts-ignore Use mock client for unit tests
95+
const hub: Hub = new Hub(client);
96+
makeMain(hub);
97+
98+
afterEach(() => {
99+
SENTRY_SPAN_PROCESSOR_MAP.clear();
100+
});
101+
102+
enum PerfType {
103+
Transaction = 'transaction',
104+
Span = 'span',
105+
}
106+
107+
function createTransactionAndMaybeSpan(type: PerfType, transactionContext: TransactionContext) {
108+
const transaction = new Transaction(transactionContext, hub);
109+
SENTRY_SPAN_PROCESSOR_MAP.set(transaction.spanId, transaction);
110+
if (type === PerfType.Span) {
111+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
112+
const { spanId, ...ctx } = transactionContext;
113+
const span = transaction.startChild({ ...ctx, description: transaction.name });
114+
SENTRY_SPAN_PROCESSOR_MAP.set(span.spanId, span);
115+
}
116+
}
117+
118+
describe.each([PerfType.Transaction, PerfType.Span])('with active %s', type => {
119+
it.each([
120+
[
121+
'should set baggage header when sampled',
122+
{
123+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
124+
spanId: '6e0c63257de34c92',
125+
traceFlags: TraceFlags.SAMPLED,
126+
},
127+
{
128+
name: 'sampled-transaction',
129+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
130+
spanId: '6e0c63257de34c92',
131+
sampled: true,
132+
},
133+
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b',
134+
],
135+
[
136+
'should NOT set baggage header when not sampled',
137+
{
138+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
139+
spanId: '6e0c63257de34c92',
140+
traceFlags: TraceFlags.NONE,
141+
},
142+
{
143+
name: 'not-sampled-transaction',
144+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
145+
spanId: '6e0c63257de34c92',
146+
sampled: false,
147+
},
148+
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=not-sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b',
149+
],
150+
[
151+
'should NOT set baggage header when traceId is empty',
152+
{
153+
traceId: '',
154+
spanId: '6e0c63257de34c92',
155+
traceFlags: TraceFlags.SAMPLED,
156+
},
157+
{
158+
name: 'empty-traceId-transaction',
159+
traceId: '',
160+
spanId: '6e0c63257de34c92',
161+
sampled: true,
162+
},
163+
undefined,
164+
],
165+
[
166+
'should NOT set baggage header when spanId is empty',
167+
{
168+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
169+
spanId: '',
170+
traceFlags: TraceFlags.SAMPLED,
171+
},
172+
{
173+
name: 'empty-spanId-transaction',
174+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
175+
spanId: '',
176+
sampled: true,
177+
},
178+
undefined,
179+
],
180+
])('%s', (_name, spanContext, transactionContext, expected) => {
181+
createTransactionAndMaybeSpan(type, transactionContext);
182+
const context = trace.setSpanContext(ROOT_CONTEXT, spanContext);
183+
propagator.inject(context, carrier, defaultTextMapSetter);
184+
expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe(expected);
185+
});
186+
187+
it('should NOT set sentry-trace header if instrumentation is supressed', () => {
188+
const spanContext = {
189+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
190+
spanId: '6e0c63257de34c92',
191+
traceFlags: TraceFlags.SAMPLED,
192+
};
193+
const transactionContext = {
194+
name: 'sampled-transaction',
195+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
196+
spanId: '6e0c63257de34c92',
197+
sampled: true,
198+
};
199+
createTransactionAndMaybeSpan(type, transactionContext);
200+
const context = suppressTracing(trace.setSpanContext(ROOT_CONTEXT, spanContext));
201+
propagator.inject(context, carrier, defaultTextMapSetter);
202+
expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined);
203+
});
204+
});
205+
});
9206
});
10207
});

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Hub, makeMain } from '@sentry/core';
88
import { addExtensionMethods, Span as SentrySpan, SpanStatusType, Transaction } from '@sentry/tracing';
99
import { Contexts, Scope } from '@sentry/types';
1010

11-
import { SentrySpanProcessor } from '../src/spanprocessor';
11+
import { SENTRY_SPAN_PROCESSOR_MAP, SentrySpanProcessor } from '../src/spanprocessor';
1212

1313
// Integration Test of SentrySpanProcessor
1414

@@ -41,7 +41,7 @@ describe('SentrySpanProcessor', () => {
4141
});
4242

4343
function getSpanForOtelSpan(otelSpan: OtelSpan | OpenTelemetry.Span) {
44-
return spanProcessor._map.get(otelSpan.spanContext().spanId);
44+
return SENTRY_SPAN_PROCESSOR_MAP.get(otelSpan.spanContext().spanId);
4545
}
4646

4747
function getContext(transaction: Transaction) {

0 commit comments

Comments
 (0)