Skip to content

Commit 819c1e7

Browse files
authored
feat: Add W3C traceparent header support (#3246)
1 parent ab21773 commit 819c1e7

File tree

6 files changed

+419
-233
lines changed

6 files changed

+419
-233
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Add W3C `traceparent` header support ([#3246](https://github.com/getsentry/sentry-dart/pull/3246))
8+
- Enable the option `propagateTraceparent` to allow the propagation of the W3C Trace Context HTTP header `traceparent` on outgoing HTTP requests.
9+
510
### Enhancements
611

712
- Prefix firebase remote config feature flags with `firebase:` ([#3258](https://github.com/getsentry/sentry-dart/pull/3258))

packages/dart/lib/src/sentry_options.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,13 @@ class SentryOptions {
424424
/// array, and only attach tracing headers if a match was found.
425425
final List<String> tracePropagationTargets = ['.*'];
426426

427+
/// This option is used to enable the propagation of the
428+
/// W3C Trace Context HTTP header traceparent on outgoing HTTP requests.
429+
/// This is useful when the receiving services only support OTel/W3C propagation
430+
///
431+
/// The default is `false`.
432+
bool propagateTraceparent = false;
433+
427434
/// The idle time to wait until the transaction will be finished.
428435
/// The transaction will use the end timestamp of the last finished span as
429436
/// the endtime for the transaction.

packages/dart/lib/src/utils/tracing_utils.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@ SentryTraceHeader generateSentryTraceHeader(
1010
void addTracingHeadersToHttpHeader(Map<String, dynamic> headers, Hub hub,
1111
{ISentrySpan? span}) {
1212
if (span != null) {
13+
if (hub.options.propagateTraceparent) {
14+
addW3CHeaderFromSpan(span, headers);
15+
}
1316
addSentryTraceHeaderFromSpan(span, headers);
1417
addBaggageHeaderFromSpan(
1518
span,
1619
headers,
1720
log: hub.options.log,
1821
);
1922
} else {
23+
if (hub.options.propagateTraceparent) {
24+
addW3CHeaderFromScope(hub.scope, headers);
25+
}
2026
addSentryTraceHeaderFromScope(hub.scope, headers);
2127
addBaggageHeaderFromScope(hub.scope, headers, log: hub.options.log);
2228
}
@@ -39,6 +45,28 @@ void addSentryTraceHeader(
3945
headers[traceHeader.name] = traceHeader.value;
4046
}
4147

48+
void addW3CHeaderFromSpan(ISentrySpan span, Map<String, dynamic> headers) {
49+
final traceHeader = span.toSentryTrace();
50+
_addW3CHeaderFromSentryTrace(traceHeader, headers);
51+
}
52+
53+
void addW3CHeaderFromScope(Scope scope, Map<String, dynamic> headers) {
54+
final propagationContext = scope.propagationContext;
55+
final traceHeader = propagationContext.toSentryTrace();
56+
_addW3CHeaderFromSentryTrace(traceHeader, headers);
57+
}
58+
59+
void _addW3CHeaderFromSentryTrace(
60+
SentryTraceHeader traceHeader, Map<String, dynamic> headers) {
61+
headers['traceparent'] = formatAsW3CHeader(traceHeader);
62+
}
63+
64+
String formatAsW3CHeader(SentryTraceHeader traceHeader) {
65+
final sampled = traceHeader.sampled;
66+
final sampledBit = sampled != null && sampled ? '01' : '00';
67+
return '00-${traceHeader.traceId}-${traceHeader.spanId}-$sampledBit';
68+
}
69+
4270
void addBaggageHeaderFromScope(
4371
Scope scope,
4472
Map<String, dynamic> headers, {

packages/dart/test/http_client/tracing_client_test.dart

Lines changed: 82 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -120,57 +120,87 @@ void main() {
120120
expect(span.throwable, exception);
121121
});
122122

123-
test('should add tracing headers from span when tracing enabled', () async {
124-
final sut = fixture.getSut(
125-
client: fixture.getClient(statusCode: 200, reason: 'OK'),
126-
);
127-
final tr = fixture._hub.startTransaction(
128-
'name',
129-
'op',
130-
bindToScope: true,
131-
);
132-
133-
final response = await sut.get(requestUri);
134-
135-
await tr.finish();
136-
137-
final tracer = (tr as SentryTracer);
138-
expect(tracer.children.length, 1);
139-
final span = tracer.children.first;
140-
final baggageHeader = span.toBaggageHeader();
141-
final sentryTraceHeader = span.toSentryTrace();
142-
143-
expect(
144-
response.request!.headers[baggageHeader!.name], baggageHeader.value);
145-
expect(response.request!.headers[sentryTraceHeader.name],
146-
sentryTraceHeader.value);
147-
});
148-
149-
test(
150-
'should add tracing headers from propagation context when tracing disabled',
151-
() async {
152-
fixture._hub.options.tracesSampleRate = null;
153-
fixture._hub.options.tracesSampler = null;
154-
final sut = fixture.getSut(
155-
client: fixture.getClient(statusCode: 200, reason: 'OK'),
156-
);
157-
final propagationContext = fixture._hub.scope.propagationContext;
158-
propagationContext.baggage = SentryBaggage({'foo': 'bar'});
159-
160-
final response = await sut.get(requestUri);
161-
162-
final baggageHeader = propagationContext.toBaggageHeader();
163-
164-
expect(propagationContext.toBaggageHeader(), isNotNull);
165-
expect(
166-
response.request!.headers[baggageHeader!.name], baggageHeader.value);
123+
for (final propagate in <bool>[true, false]) {
124+
test(
125+
'should add tracing headers from span when tracing enabled (propagateTraceparent: $propagate)',
126+
() async {
127+
final sut = fixture.getSut(
128+
client: fixture.getClient(statusCode: 200, reason: 'OK'),
129+
);
130+
fixture._hub.options.propagateTraceparent = propagate;
131+
132+
final tr = fixture._hub.startTransaction(
133+
'name',
134+
'op',
135+
bindToScope: true,
136+
);
137+
138+
final response = await sut.get(requestUri);
139+
140+
await tr.finish();
141+
142+
final tracer = (tr as SentryTracer);
143+
final span = tracer.children.first;
144+
final baggageHeader = span.toBaggageHeader();
145+
final sentryTraceHeader = span.toSentryTrace();
146+
147+
expect(response.request!.headers[baggageHeader!.name],
148+
baggageHeader.value);
149+
expect(response.request!.headers[sentryTraceHeader.name],
150+
sentryTraceHeader.value);
151+
152+
final traceHeader = span.toSentryTrace();
153+
final expected =
154+
'00-${traceHeader.traceId}-${traceHeader.spanId}-${traceHeader.sampled == true ? '01' : '00'}';
155+
156+
if (propagate) {
157+
expect(response.request!.headers['traceparent'], expected);
158+
} else {
159+
expect(response.request!.headers['traceparent'], isNull);
160+
}
161+
});
162+
}
167163

168-
final traceHeader = SentryTraceHeader.fromTraceHeader(
169-
response.request!.headers['sentry-trace'] as String,
170-
);
171-
expect(traceHeader.traceId, propagationContext.traceId);
172-
// can't check span id as it is always generated new
173-
});
164+
for (final propagate in <bool>[true, false]) {
165+
test(
166+
'should add tracing headers from propagation context when tracing disabled (propagateTraceparent: $propagate)',
167+
() async {
168+
fixture._hub.options.tracesSampleRate = null;
169+
fixture._hub.options.tracesSampler = null;
170+
fixture._hub.options.propagateTraceparent = propagate;
171+
172+
final sut = fixture.getSut(
173+
client: fixture.getClient(statusCode: 200, reason: 'OK'),
174+
);
175+
final propagationContext = fixture._hub.scope.propagationContext;
176+
propagationContext.baggage = SentryBaggage({'foo': 'bar'});
177+
178+
final response = await sut.get(requestUri);
179+
180+
final baggageHeader = propagationContext.toBaggageHeader();
181+
182+
expect(propagationContext.toBaggageHeader(), isNotNull);
183+
expect(response.request!.headers[baggageHeader!.name],
184+
baggageHeader.value);
185+
186+
final traceHeader = SentryTraceHeader.fromTraceHeader(
187+
response.request!.headers['sentry-trace'] as String,
188+
);
189+
expect(traceHeader.traceId, propagationContext.traceId);
190+
191+
if (propagate) {
192+
final headerValue = response.request!.headers['traceparent']!;
193+
final parts = headerValue.split('-');
194+
expect(parts.length, 4);
195+
expect(parts[0], '00');
196+
expect(parts[1], propagationContext.traceId.toString());
197+
expect(parts[2].length, 16);
198+
expect(parts[3], '00');
199+
} else {
200+
expect(response.request!.headers['traceparent'], isNull);
201+
}
202+
});
203+
}
174204

175205
test(
176206
'tracing header from propagation context should generate new span ids for new events',
@@ -221,6 +251,7 @@ void main() {
221251

222252
expect(response.request!.headers[baggageHeader!.name], isNull);
223253
expect(response.request!.headers[sentryTraceHeader.name], isNull);
254+
expect(response.request!.headers['traceparent'], isNull);
224255
});
225256

226257
test(
@@ -242,6 +273,7 @@ void main() {
242273

243274
expect(response.request!.headers[baggageHeader!.name], isNull);
244275
expect(response.request!.headers[sentryTraceHeader.name], isNull);
276+
expect(response.request!.headers['traceparent'], isNull);
245277
});
246278

247279
test('do not throw if no span bound to the scope', () async {

packages/dart/test/utils/tracing_utils_test.dart

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,48 @@ void main() {
7474
});
7575
});
7676

77+
group('W3C traceparent header', () {
78+
final fixture = Fixture();
79+
final headerName = 'traceparent';
80+
81+
test('converts SentryTraceHeader to W3C format correctly', () {
82+
final sut = fixture.getSut();
83+
final sentryHeader = sut.toSentryTrace();
84+
85+
final w3cHeader = formatAsW3CHeader(sentryHeader);
86+
87+
expect(w3cHeader,
88+
'00-${fixture._context.traceId}-${fixture._context.spanId}-01');
89+
});
90+
91+
test('added when given a span', () {
92+
final headers = <String, dynamic>{};
93+
final sut = fixture.getSut();
94+
95+
addW3CHeaderFromSpan(sut, headers);
96+
97+
expect(headers[headerName],
98+
'00-${fixture._context.traceId}-${fixture._context.spanId}-01');
99+
});
100+
101+
test('added when given a scope', () {
102+
final headers = <String, dynamic>{};
103+
final hub = fixture._hub;
104+
final scope = hub.scope;
105+
106+
addW3CHeaderFromScope(scope, headers);
107+
108+
final headerValue = headers[headerName] as String;
109+
final parts = headerValue.split('-');
110+
111+
expect(parts.length, 4);
112+
expect(parts[0], '00');
113+
expect(parts[1], scope.propagationContext.traceId.toString());
114+
expect(parts[2], hasLength(16)); // just check length since it's random
115+
expect(parts[3], '00');
116+
});
117+
});
118+
77119
group('$addBaggageHeader', () {
78120
final fixture = Fixture();
79121

@@ -205,8 +247,69 @@ void main() {
205247
});
206248
});
207249

208-
group('$addTracingHeadersToHttpHeader', () {
209-
final fixture = Fixture();
250+
group(addTracingHeadersToHttpHeader, () {
251+
late Fixture fixture;
252+
253+
setUp(() {
254+
fixture = Fixture();
255+
});
256+
257+
test(
258+
'adds W3C traceparent header from span when propagateTraceparent is true',
259+
() {
260+
final headers = <String, dynamic>{};
261+
final hub = fixture._hub;
262+
final span = fixture.getSut();
263+
hub.options.propagateTraceparent = true;
264+
265+
addTracingHeadersToHttpHeader(headers, hub, span: span);
266+
267+
expect(headers['traceparent'],
268+
'00-${fixture._context.traceId}-${fixture._context.spanId}-01');
269+
});
270+
271+
test(
272+
'does not add W3C traceparent header from span when propagateTraceparent is false',
273+
() {
274+
final headers = <String, dynamic>{};
275+
final hub = fixture._hub;
276+
// propagateTraceparent is false by default
277+
278+
addTracingHeadersToHttpHeader(headers, hub);
279+
280+
expect(headers['traceparent'], isNull);
281+
});
282+
283+
test(
284+
'adds W3C traceparent header from scope when propagateTraceparent is true',
285+
() {
286+
final headers = <String, dynamic>{};
287+
final hub = fixture._hub;
288+
hub.options.propagateTraceparent = true;
289+
290+
addTracingHeadersToHttpHeader(headers, hub);
291+
292+
final headerValue = headers['traceparent'] as String;
293+
final parts = headerValue.split('-');
294+
295+
expect(parts.length, 4);
296+
expect(parts[0], '00');
297+
expect(parts[1], hub.scope.propagationContext.traceId.toString());
298+
expect(parts[2], hasLength(16)); // just check length since it's random
299+
expect(parts[3], '00'); // not sampled for scope context
300+
});
301+
302+
test(
303+
'does not add W3C traceparent header from scope when propagateTraceparent is false',
304+
() {
305+
final headers = <String, dynamic>{};
306+
final hub = fixture._hub;
307+
// propagateTraceparent is false by default
308+
309+
addTracingHeadersToHttpHeader(headers, hub);
310+
311+
expect(headers['traceparent'], isNull);
312+
});
210313

211314
test('adds headers from span when span is provided', () {
212315
final headers = <String, dynamic>{};

0 commit comments

Comments
 (0)