Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Features

- Add W3C `traceparent` header support ([#3246](https://github.com/getsentry/sentry-dart/pull/3246))
- Enable the option `propagateTraceparent` to allow the propagation of the W3C Trace Context HTTP header `traceparent` on outgoing HTTP requests.

## 9.7.0-beta.4

### Features
Expand Down
1 change: 1 addition & 0 deletions min_version_test/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies:
sentry_logging:
dio: any # This gets constrained by `sentry_dio`
logging: any # This gets constrained by `sentry_logging`
package_info_plus: 8.3.1 # version 9 and above requires at least AGP 8.12.1

flutter: any
dev_dependencies:
Expand Down
7 changes: 7 additions & 0 deletions packages/dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,13 @@ class SentryOptions {
/// array, and only attach tracing headers if a match was found.
final List<String> tracePropagationTargets = ['.*'];

/// This option is used to enable the propagation of the
/// W3C Trace Context HTTP header traceparent on outgoing HTTP requests.
/// This is useful when the receiving services only support OTel/W3C propagation
///
/// The default is `false`.
bool propagateTraceparent = false;

/// The idle time to wait until the transaction will be finished.
/// The transaction will use the end timestamp of the last finished span as
/// the endtime for the transaction.
Expand Down
28 changes: 28 additions & 0 deletions packages/dart/lib/src/utils/tracing_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ SentryTraceHeader generateSentryTraceHeader(
void addTracingHeadersToHttpHeader(Map<String, dynamic> headers, Hub hub,
{ISentrySpan? span}) {
if (span != null) {
if (hub.options.propagateTraceparent) {
addW3CHeaderFromSpan(span, headers);
}
addSentryTraceHeaderFromSpan(span, headers);
addBaggageHeaderFromSpan(
span,
headers,
log: hub.options.log,
);
} else {
if (hub.options.propagateTraceparent) {
addW3CHeaderFromScope(hub.scope, headers);
}
addSentryTraceHeaderFromScope(hub.scope, headers);
addBaggageHeaderFromScope(hub.scope, headers, log: hub.options.log);
}
Expand All @@ -39,6 +45,28 @@ void addSentryTraceHeader(
headers[traceHeader.name] = traceHeader.value;
}

void addW3CHeaderFromSpan(ISentrySpan span, Map<String, dynamic> headers) {
final traceHeader = span.toSentryTrace();
_addW3CHeaderFromSentryTrace(traceHeader, headers);
}

void addW3CHeaderFromScope(Scope scope, Map<String, dynamic> headers) {
final propagationContext = scope.propagationContext;
final traceHeader = propagationContext.toSentryTrace();
_addW3CHeaderFromSentryTrace(traceHeader, headers);
}

void _addW3CHeaderFromSentryTrace(
SentryTraceHeader traceHeader, Map<String, dynamic> headers) {
headers['traceparent'] = formatAsW3CHeader(traceHeader);
}

String formatAsW3CHeader(SentryTraceHeader traceHeader) {
final sampled = traceHeader.sampled;
final sampledBit = sampled != null && sampled ? '01' : '00';
return '00-${traceHeader.traceId}-${traceHeader.spanId}-$sampledBit';
}

void addBaggageHeaderFromScope(
Scope scope,
Map<String, dynamic> headers, {
Expand Down
132 changes: 82 additions & 50 deletions packages/dart/test/http_client/tracing_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,57 +120,87 @@ void main() {
expect(span.throwable, exception);
});

test('should add tracing headers from span when tracing enabled', () async {
final sut = fixture.getSut(
client: fixture.getClient(statusCode: 200, reason: 'OK'),
);
final tr = fixture._hub.startTransaction(
'name',
'op',
bindToScope: true,
);

final response = await sut.get(requestUri);

await tr.finish();

final tracer = (tr as SentryTracer);
expect(tracer.children.length, 1);
final span = tracer.children.first;
final baggageHeader = span.toBaggageHeader();
final sentryTraceHeader = span.toSentryTrace();

expect(
response.request!.headers[baggageHeader!.name], baggageHeader.value);
expect(response.request!.headers[sentryTraceHeader.name],
sentryTraceHeader.value);
});

test(
'should add tracing headers from propagation context when tracing disabled',
() async {
fixture._hub.options.tracesSampleRate = null;
fixture._hub.options.tracesSampler = null;
final sut = fixture.getSut(
client: fixture.getClient(statusCode: 200, reason: 'OK'),
);
final propagationContext = fixture._hub.scope.propagationContext;
propagationContext.baggage = SentryBaggage({'foo': 'bar'});

final response = await sut.get(requestUri);

final baggageHeader = propagationContext.toBaggageHeader();

expect(propagationContext.toBaggageHeader(), isNotNull);
expect(
response.request!.headers[baggageHeader!.name], baggageHeader.value);
for (final propagate in <bool>[true, false]) {
test(
'should add tracing headers from span when tracing enabled (propagateTraceparent: $propagate)',
() async {
final sut = fixture.getSut(
client: fixture.getClient(statusCode: 200, reason: 'OK'),
);
fixture._hub.options.propagateTraceparent = propagate;

final tr = fixture._hub.startTransaction(
'name',
'op',
bindToScope: true,
);

final response = await sut.get(requestUri);

await tr.finish();

final tracer = (tr as SentryTracer);
final span = tracer.children.first;
final baggageHeader = span.toBaggageHeader();
final sentryTraceHeader = span.toSentryTrace();

expect(response.request!.headers[baggageHeader!.name],
baggageHeader.value);
expect(response.request!.headers[sentryTraceHeader.name],
sentryTraceHeader.value);

final traceHeader = span.toSentryTrace();
final expected =
'00-${traceHeader.traceId}-${traceHeader.spanId}-${traceHeader.sampled == true ? '01' : '00'}';

if (propagate) {
expect(response.request!.headers['traceparent'], expected);
} else {
expect(response.request!.headers['traceparent'], isNull);
}
});
}

final traceHeader = SentryTraceHeader.fromTraceHeader(
response.request!.headers['sentry-trace'] as String,
);
expect(traceHeader.traceId, propagationContext.traceId);
// can't check span id as it is always generated new
});
for (final propagate in <bool>[true, false]) {
test(
'should add tracing headers from propagation context when tracing disabled (propagateTraceparent: $propagate)',
() async {
fixture._hub.options.tracesSampleRate = null;
fixture._hub.options.tracesSampler = null;
fixture._hub.options.propagateTraceparent = propagate;

final sut = fixture.getSut(
client: fixture.getClient(statusCode: 200, reason: 'OK'),
);
final propagationContext = fixture._hub.scope.propagationContext;
propagationContext.baggage = SentryBaggage({'foo': 'bar'});

final response = await sut.get(requestUri);

final baggageHeader = propagationContext.toBaggageHeader();

expect(propagationContext.toBaggageHeader(), isNotNull);
expect(response.request!.headers[baggageHeader!.name],
baggageHeader.value);

final traceHeader = SentryTraceHeader.fromTraceHeader(
response.request!.headers['sentry-trace'] as String,
);
expect(traceHeader.traceId, propagationContext.traceId);

if (propagate) {
final headerValue = response.request!.headers['traceparent']!;
final parts = headerValue.split('-');
expect(parts.length, 4);
expect(parts[0], '00');
expect(parts[1], propagationContext.traceId.toString());
expect(parts[2].length, 16);
expect(parts[3], '00');
} else {
expect(response.request!.headers['traceparent'], isNull);
}
});
}

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

expect(response.request!.headers[baggageHeader!.name], isNull);
expect(response.request!.headers[sentryTraceHeader.name], isNull);
expect(response.request!.headers['traceparent'], isNull);
});

test(
Expand All @@ -242,6 +273,7 @@ void main() {

expect(response.request!.headers[baggageHeader!.name], isNull);
expect(response.request!.headers[sentryTraceHeader.name], isNull);
expect(response.request!.headers['traceparent'], isNull);
});

test('do not throw if no span bound to the scope', () async {
Expand Down
107 changes: 105 additions & 2 deletions packages/dart/test/utils/tracing_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,48 @@ void main() {
});
});

group('W3C traceparent header', () {
final fixture = Fixture();
final headerName = 'traceparent';

test('converts SentryTraceHeader to W3C format correctly', () {
final sut = fixture.getSut();
final sentryHeader = sut.toSentryTrace();

final w3cHeader = formatAsW3CHeader(sentryHeader);

expect(w3cHeader,
'00-${fixture._context.traceId}-${fixture._context.spanId}-01');
});

test('added when given a span', () {
final headers = <String, dynamic>{};
final sut = fixture.getSut();

addW3CHeaderFromSpan(sut, headers);

expect(headers[headerName],
'00-${fixture._context.traceId}-${fixture._context.spanId}-01');
});

test('added when given a scope', () {
final headers = <String, dynamic>{};
final hub = fixture._hub;
final scope = hub.scope;

addW3CHeaderFromScope(scope, headers);

final headerValue = headers[headerName] as String;
final parts = headerValue.split('-');

expect(parts.length, 4);
expect(parts[0], '00');
expect(parts[1], scope.propagationContext.traceId.toString());
expect(parts[2], hasLength(16)); // just check length since it's random
expect(parts[3], '00');
});
});

group('$addBaggageHeader', () {
final fixture = Fixture();

Expand Down Expand Up @@ -205,8 +247,69 @@ void main() {
});
});

group('$addTracingHeadersToHttpHeader', () {
final fixture = Fixture();
group(addTracingHeadersToHttpHeader, () {
late Fixture fixture;

setUp(() {
fixture = Fixture();
});

test(
'adds W3C traceparent header from span when propagateTraceparent is true',
() {
final headers = <String, dynamic>{};
final hub = fixture._hub;
final span = fixture.getSut();
hub.options.propagateTraceparent = true;

addTracingHeadersToHttpHeader(headers, hub, span: span);

expect(headers['traceparent'],
'00-${fixture._context.traceId}-${fixture._context.spanId}-01');
});

test(
'does not add W3C traceparent header from span when propagateTraceparent is false',
() {
final headers = <String, dynamic>{};
final hub = fixture._hub;
// propagateTraceparent is false by default

addTracingHeadersToHttpHeader(headers, hub);

expect(headers['traceparent'], isNull);
});

test(
'adds W3C traceparent header from scope when propagateTraceparent is true',
() {
final headers = <String, dynamic>{};
final hub = fixture._hub;
hub.options.propagateTraceparent = true;

addTracingHeadersToHttpHeader(headers, hub);

final headerValue = headers['traceparent'] as String;
final parts = headerValue.split('-');

expect(parts.length, 4);
expect(parts[0], '00');
expect(parts[1], hub.scope.propagationContext.traceId.toString());
expect(parts[2], hasLength(16)); // just check length since it's random
expect(parts[3], '00'); // not sampled for scope context
});

test(
'does not add W3C traceparent header from scope when propagateTraceparent is false',
() {
final headers = <String, dynamic>{};
final hub = fixture._hub;
// propagateTraceparent is false by default

addTracingHeadersToHttpHeader(headers, hub);

expect(headers['traceparent'], isNull);
});

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