diff --git a/CHANGELOG.md b/CHANGELOG.md index dd68f3d14a..41447b7328 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Tracing without performance ([#1621](https://github.com/getsentry/sentry-dart/pull/1621)) + ### Fixes - Normalize data properties of `SentryUser` and `Breadcrumb` before sending over method channel ([#1591](https://github.com/getsentry/sentry-dart/pull/1591)) diff --git a/dart/lib/src/http_client/tracing_client.dart b/dart/lib/src/http_client/tracing_client.dart index ae7a81e973..4a469d9d3c 100644 --- a/dart/lib/src/http_client/tracing_client.dart +++ b/dart/lib/src/http_client/tracing_client.dart @@ -46,15 +46,28 @@ class TracingClient extends BaseClient { StreamedResponse? response; try { - if (span != null) { - if (containsTargetOrMatchesRegExp( - _hub.options.tracePropagationTargets, request.url.toString())) { - addSentryTraceHeader(span, request.headers); - addBaggageHeader( + if (containsTargetOrMatchesRegExp( + _hub.options.tracePropagationTargets, request.url.toString())) { + if (span != null) { + addSentryTraceHeaderFromSpan(span, request.headers); + addBaggageHeaderFromSpan( span, request.headers, logger: _hub.options.logger, ); + } else { + final scope = _hub.scope; + final propagationContext = scope.propagationContext; + + final traceHeader = propagationContext.toSentryTrace(); + addSentryTraceHeader(traceHeader, request.headers); + + final baggage = propagationContext.baggage; + if (baggage != null) { + final baggageHeader = SentryBaggageHeader.fromBaggage(baggage); + addBaggageHeader(baggageHeader, request.headers, + logger: _hub.options.logger); + } } } diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index e1ffe802dd..855f2b9438 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:collection'; import 'package:meta/meta.dart'; +import 'propagation_context.dart'; import 'transport/data_category.dart'; import '../sentry.dart'; @@ -65,6 +66,9 @@ class Hub { /// Last event id recorded by the Hub SentryId get lastEventId => _lastEventId; + @internal + Scope get scope => _peek().scope; + /// Captures the event. Future captureEvent( SentryEvent event, { @@ -426,10 +430,8 @@ class Hub { "Instance is disabled and this 'startTransaction' call is a no-op.", ); } else if (!_options.isTracingEnabled()) { - _options.logger( - SentryLevel.info, - "Tracing is disabled and this 'startTransaction' returns a no-op.", - ); + final item = _peek(); + item.scope.propagationContext = PropagationContext(); } else { final item = _peek(); diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 8c1a176df8..0775042e04 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -5,6 +5,7 @@ import 'hint.dart'; import 'hub.dart'; import 'protocol.dart'; +import 'scope.dart'; import 'sentry.dart'; import 'sentry_client.dart'; import 'sentry_user_feedback.dart'; @@ -166,4 +167,7 @@ class HubAdapter implements Hub { String transaction, ) => Sentry.currentHub.setSpanContext(throwable, span, transaction); + + @override + Scope get scope => Sentry.currentHub.scope; } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index ea005184b6..28a56de249 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import 'hint.dart'; import 'hub.dart'; import 'protocol.dart'; +import 'scope.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; import 'sentry_user_feedback.dart'; @@ -118,4 +119,7 @@ class NoOpHub implements Hub { @override void setSpanContext(throwable, ISentrySpan span, String transaction) {} + + @override + Scope get scope => Scope(_options); } diff --git a/dart/lib/src/propagation_context.dart b/dart/lib/src/propagation_context.dart new file mode 100644 index 0000000000..1a0f32355a --- /dev/null +++ b/dart/lib/src/propagation_context.dart @@ -0,0 +1,12 @@ +import 'package:meta/meta.dart'; +import 'protocol.dart'; +import 'sentry_baggage.dart'; + +@internal +class PropagationContext { + late SentryId traceId = SentryId.newId(); + late SpanId spanId = SpanId.newId(); + SentryBaggage? baggage; + + SentryTraceHeader toSentryTrace() => SentryTraceHeader(traceId, spanId); +} diff --git a/dart/lib/src/protocol/sentry_trace_context.dart b/dart/lib/src/protocol/sentry_trace_context.dart index fab3d1444d..25c4ca7ad8 100644 --- a/dart/lib/src/protocol/sentry_trace_context.dart +++ b/dart/lib/src/protocol/sentry_trace_context.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +import '../../sentry.dart'; +import '../propagation_context.dart'; import '../protocol.dart'; @immutable @@ -87,4 +89,14 @@ class SentryTraceContext { this.origin, }) : traceId = traceId ?? SentryId.newId(), spanId = spanId ?? SpanId.newId(); + + @internal + factory SentryTraceContext.fromPropagationContext( + PropagationContext propagationContext) { + return SentryTraceContext( + traceId: propagationContext.traceId, + spanId: propagationContext.spanId, + operation: 'default', + ); + } } diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index f9cb3796aa..aa37d67993 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -1,8 +1,11 @@ import 'dart:async'; import 'dart:collection'; +import 'package:meta/meta.dart'; + import 'event_processor.dart'; import 'hint.dart'; +import 'propagation_context.dart'; import 'protocol.dart'; import 'scope_observer.dart'; import 'sentry_attachment/sentry_attachment.dart'; @@ -39,6 +42,9 @@ class Scope { /// Returns active transaction or null if there is no active transaction. ISentrySpan? span; + @internal + PropagationContext propagationContext = PropagationContext(); + SentryUser? _user; /// Get the current user. @@ -311,10 +317,15 @@ class Scope { }); final newSpan = span; - if (event.contexts.trace == null && newSpan != null) { - event.contexts.trace = newSpan.context.toTraceContext( - sampled: newSpan.samplingDecision?.sampled, - ); + if (event.contexts.trace == null) { + if (newSpan != null) { + event.contexts.trace = newSpan.context.toTraceContext( + sampled: newSpan.samplingDecision?.sampled, + ); + } else { + event.contexts.trace = + SentryTraceContext.fromPropagationContext(propagationContext); + } } SentryEvent? processedEvent = event; diff --git a/dart/lib/src/sentry_baggage.dart b/dart/lib/src/sentry_baggage.dart index 50547622d5..60ac395648 100644 --- a/dart/lib/src/sentry_baggage.dart +++ b/dart/lib/src/sentry_baggage.dart @@ -1,4 +1,7 @@ -import 'protocol/sentry_level.dart'; +import 'package:meta/meta.dart'; +import 'scope.dart'; +import 'protocol.dart'; + import 'sentry_options.dart'; class SentryBaggage { @@ -87,6 +90,27 @@ class SentryBaggage { return SentryBaggage(keyValues, logger: logger); } + @internal + setValuesFromScope(Scope scope, SentryOptions options) { + final propagationContext = scope.propagationContext; + setTraceId(propagationContext.traceId.toString()); + if (options.dsn != null) { + setPublicKey(Dsn.parse(options.dsn!).publicKey); + } + if (options.release != null) { + setRelease(options.release!); + } + if (options.environment != null) { + setEnvironment(options.environment!); + } + if (scope.user?.id != null) { + setUserId(scope.user!.id!); + } + if (scope.user?.segment != null) { + setUserSegment(scope.user!.segment!); + } + } + static Map _extractKeyValuesFromBaggageString( String headerValue, { SentryLogger? logger, diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 09f98e4d02..a2af9c47b2 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; import 'package:meta/meta.dart'; +import 'sentry_baggage.dart'; import 'sentry_attachment/sentry_attachment.dart'; import 'event_processor.dart'; @@ -122,11 +123,24 @@ class SentryClient { attachments.add(viewHierarchy); } + var traceContext = scope?.span?.traceContext(); + if (traceContext == null) { + if (scope?.propagationContext.baggage == null) { + scope?.propagationContext.baggage = + SentryBaggage({}, logger: _options.logger); + scope?.propagationContext.baggage?.setValuesFromScope(scope, _options); + } + if (scope != null) { + traceContext = SentryTraceContextHeader.fromBaggage( + scope.propagationContext.baggage!); + } + } + final envelope = SentryEnvelope.fromEvent( preparedEvent, _options.sdk, dsn: _options.dsn, - traceContext: scope?.span?.traceContext(), + traceContext: traceContext, attachments: attachments.isNotEmpty ? attachments : null, ); diff --git a/dart/lib/src/sentry_trace_context_header.dart b/dart/lib/src/sentry_trace_context_header.dart index 181752b359..68208a7096 100644 --- a/dart/lib/src/sentry_trace_context_header.dart +++ b/dart/lib/src/sentry_trace_context_header.dart @@ -79,4 +79,13 @@ class SentryTraceContextHeader { return baggage; } + + factory SentryTraceContextHeader.fromBaggage(SentryBaggage baggage) { + return SentryTraceContextHeader( + SentryId.fromId(baggage.get('sentry-trace_id').toString()), + baggage.get('sentry-public_key').toString(), + release: baggage.get('sentry-release'), + environment: baggage.get('sentry-environment'), + ); + } } diff --git a/dart/lib/src/utils/tracing_utils.dart b/dart/lib/src/utils/tracing_utils.dart index a60497509f..6198062ddc 100644 --- a/dart/lib/src/utils/tracing_utils.dart +++ b/dart/lib/src/utils/tracing_utils.dart @@ -1,42 +1,55 @@ import '../../sentry.dart'; -void addSentryTraceHeader(ISentrySpan span, Map headers) { +void addSentryTraceHeaderFromSpan( + ISentrySpan span, Map headers) { final traceHeader = span.toSentryTrace(); headers[traceHeader.name] = traceHeader.value; } -void addBaggageHeader( +void addSentryTraceHeader( + SentryTraceHeader traceHeader, Map headers) { + headers[traceHeader.name] = traceHeader.value; +} + +void addBaggageHeaderFromSpan( ISentrySpan span, Map headers, { SentryLogger? logger, }) { final baggage = span.toBaggageHeader(); if (baggage != null) { - final currentValue = headers[baggage.name] as String? ?? ''; + addBaggageHeader(baggage, headers, logger: logger); + } +} - final currentBaggage = SentryBaggage.fromHeader( - currentValue, - logger: logger, - ); - final sentryBaggage = SentryBaggage.fromHeader( - baggage.value, - logger: logger, - ); +void addBaggageHeader( + SentryBaggageHeader baggage, + Map headers, { + SentryLogger? logger, +}) { + final currentValue = headers[baggage.name] as String? ?? ''; - // overwrite sentry's keys https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#baggage - final filteredBaggageHeader = Map.from(currentBaggage.keyValues); - filteredBaggageHeader - .removeWhere((key, value) => key.startsWith('sentry-')); + final currentBaggage = SentryBaggage.fromHeader( + currentValue, + logger: logger, + ); + final sentryBaggage = SentryBaggage.fromHeader( + baggage.value, + logger: logger, + ); - final mergedBaggage = { - ...filteredBaggageHeader, - ...sentryBaggage.keyValues, - }; + // overwrite sentry's keys https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#baggage + final filteredBaggageHeader = Map.from(currentBaggage.keyValues); + filteredBaggageHeader.removeWhere((key, value) => key.startsWith('sentry-')); - final newBaggage = SentryBaggage(mergedBaggage, logger: logger); + final mergedBaggage = { + ...filteredBaggageHeader, + ...sentryBaggage.keyValues, + }; - headers[baggage.name] = newBaggage.toHeaderString(); - } + final newBaggage = SentryBaggage(mergedBaggage, logger: logger); + + headers[baggage.name] = newBaggage.toHeaderString(); } bool containsTargetOrMatchesRegExp( diff --git a/dart/test/http_client/tracing_client_test.dart b/dart/test/http_client/tracing_client_test.dart index dce3b3ef31..0e34f7da90 100644 --- a/dart/test/http_client/tracing_client_test.dart +++ b/dart/test/http_client/tracing_client_test.dart @@ -141,20 +141,6 @@ void main() { expect(response.request!.headers[sentryTrace.name], sentryTrace.value); }); - test('captured span do not add headers if NoOp', () async { - final sut = fixture.getSut( - client: fixture.getClient(statusCode: 200, reason: 'OK'), - ); - - await fixture._hub - .configureScope((scope) => scope.span = NoOpSentrySpan()); - - final response = await sut.get(requestUri); - - expect(response.request!.headers['baggage'], null); - expect(response.request!.headers['sentry-trace'], null); - }); - test('captured span do not add headers if origins not set', () async { final sut = fixture.getSut( client: fixture.getClient( @@ -188,6 +174,38 @@ void main() { await sut.get(requestUri); }); + + test('set headers from propagationContext when tracing is disabled', + () async { + fixture._options.enableTracing = false; + 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); + + expect(response.request!.headers['sentry-trace'], + propagationContext.toSentryTrace().value); + expect(response.request!.headers['baggage'], 'foo=bar'); + }); + + test('set headers from propagationContext when no transaction', () async { + 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); + + expect(response.request!.headers['sentry-trace'], + propagationContext.toSentryTrace().value); + expect(response.request!.headers['baggage'], 'foo=bar'); + }); }); } diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 32e0f1021c..8cc4e99502 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -374,6 +374,14 @@ void main() { expect( fixture.client.captureTransactionCalls.first.traceContext, context); }); + + test('returns scope', () async { + final hub = fixture.getSut(); + + final scope = hub.scope; + + expect(scope, isNotNull); + }); }); group('Hub scope', () { diff --git a/dart/test/mocks/mock_hub.dart b/dart/test/mocks/mock_hub.dart index d8b4d7384b..351ad70672 100644 --- a/dart/test/mocks/mock_hub.dart +++ b/dart/test/mocks/mock_hub.dart @@ -131,6 +131,9 @@ class MockHub with NoSuchMethodProvider implements Hub { void setSpanContext(throwable, ISentrySpan span, String transaction) { spanContextCals++; } + + @override + Scope get scope => Scope(_options); } class CaptureEventCall { diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 484dd009a5..cdbbd73950 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -542,6 +542,137 @@ void main() { fixture = Fixture(); }); + test( + 'when scope does not have an active transaction, trace state is set on the envelope from scope', + () async { + final client = fixture.getSut(); + final scope = Scope(fixture.options); + await client.captureEvent(SentryEvent(), scope: scope); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final capturedTraceContext = capturedEnvelope.header.traceContext; + final capturedTraceId = capturedTraceContext?.traceId; + final propagationContextTraceId = scope.propagationContext.traceId; + + expect(capturedTraceContext, isNotNull); + expect(capturedTraceId, propagationContextTraceId); + }); + + test('attaches trace context from span if none present yet', () async { + final client = fixture.getSut(); + final spanContext = SentrySpanContext( + traceId: SentryId.newId(), + spanId: SpanId.newId(), + operation: 'op.load', + ); + final scope = Scope(fixture.options); + scope.span = SentrySpan(fixture.tracer, spanContext, MockHub()); + + final sentryEvent = SentryEvent(); + await client.captureEvent(sentryEvent, scope: scope); + + expect(fixture.transport.envelopes.length, 1); + expect(spanContext.spanId, sentryEvent.contexts.trace!.spanId); + expect(spanContext.traceId, sentryEvent.contexts.trace!.traceId); + }); + + test( + 'attaches trace context from scope if none present yet and no span on scope', + () async { + final client = fixture.getSut(); + + final scope = Scope(fixture.options); + final scopePropagationContext = scope.propagationContext; + + final sentryEvent = SentryEvent(); + await client.captureEvent(sentryEvent, scope: scope); + + expect(fixture.transport.envelopes.length, 1); + expect( + scopePropagationContext.traceId, sentryEvent.contexts.trace!.traceId); + expect( + scopePropagationContext.spanId, sentryEvent.contexts.trace!.spanId); + }); + + test('keeps existing trace context if already present', () async { + final client = fixture.getSut(); + + final spanContext = SentrySpanContext( + traceId: SentryId.newId(), + spanId: SpanId.newId(), + operation: 'op.load', + ); + final scope = Scope(fixture.options); + scope.span = SentrySpan(fixture.tracer, spanContext, MockHub()); + + final propagationContext = scope.propagationContext; + final preExistingSpanContext = SentryTraceContext( + traceId: SentryId.newId(), + spanId: SpanId.newId(), + operation: 'op.load'); + + final sentryEvent = SentryEvent(); + sentryEvent.contexts.trace = preExistingSpanContext; + await client.captureEvent(sentryEvent, scope: scope); + + expect(fixture.transport.envelopes.length, 1); + expect( + preExistingSpanContext.traceId, sentryEvent.contexts.trace!.traceId); + expect(preExistingSpanContext.spanId, sentryEvent.contexts.trace!.spanId); + expect(spanContext.traceId, isNot(sentryEvent.contexts.trace!.traceId)); + expect(spanContext.spanId, isNot(sentryEvent.contexts.trace!.spanId)); + expect(propagationContext.traceId, + isNot(sentryEvent.contexts.trace!.traceId)); + expect( + propagationContext.spanId, isNot(sentryEvent.contexts.trace!.spanId)); + }); + + test( + 'uses propagation context on scope for trace header if no transaction is on scope', + () async { + final client = fixture.getSut(); + + final scope = Scope(fixture.options); + final scopePropagationContext = scope.propagationContext; + + final sentryEvent = SentryEvent(); + await client.captureEvent(sentryEvent, scope: scope); + + final capturedEnvelope = fixture.transport.envelopes.first; + final capturedTraceContext = capturedEnvelope.header.traceContext; + + expect(fixture.transport.envelopes.length, 1); + expect(scope.span, isNull); + expect(capturedTraceContext, isNotNull); + expect(scopePropagationContext.traceId, capturedTraceContext!.traceId); + }); + + test( + 'uses trace context on transaction for trace header if a transaction is on scope', + () async { + final client = fixture.getSut(); + + final spanContext = SentrySpanContext( + traceId: SentryId.newId(), + spanId: SpanId.newId(), + operation: 'op.load', + ); + final scope = Scope(fixture.options); + scope.span = SentrySpan(fixture.tracer, spanContext, MockHub()); + + final sentryEvent = SentryEvent(); + await client.captureEvent(sentryEvent, scope: scope); + + final capturedEnvelope = fixture.transport.envelopes.first; + final capturedTraceContext = capturedEnvelope.header.traceContext; + + expect(fixture.transport.envelopes.length, 1); + expect(scope.span, isNotNull); + expect(capturedTraceContext, isNotNull); + expect( + scope.span!.traceContext()!.traceId, capturedTraceContext!.traceId); + }); + test('should contain a transaction in the envelope', () async { try { throw StateError('Error'); diff --git a/dart/test/utils/tracing_utils_test.dart b/dart/test/utils/tracing_utils_test.dart index 39d6da6349..3dd47c3186 100644 --- a/dart/test/utils/tracing_utils_test.dart +++ b/dart/test/utils/tracing_utils_test.dart @@ -50,15 +50,25 @@ void main() { }); }); - group('$addSentryTraceHeader', () { + group('$addSentryTraceHeaderFromSpan', () { final fixture = Fixture(); + test('adds sentry trace header from span', () { + final headers = {}; + final sut = fixture.getSut(); + final sentryHeader = sut.toSentryTrace(); + + addSentryTraceHeaderFromSpan(sut, headers); + + expect(headers[sentryHeader.name], sentryHeader.value); + }); + test('adds sentry trace header', () { final headers = {}; final sut = fixture.getSut(); final sentryHeader = sut.toSentryTrace(); - addSentryTraceHeader(sut, headers); + addSentryTraceHeader(sentryHeader, headers); expect(headers[sentryHeader.name], sentryHeader.value); }); @@ -72,12 +82,22 @@ void main() { final sut = fixture.getSut(); final baggage = sut.toBaggageHeader(); - addBaggageHeader(sut, headers); + addBaggageHeader(sut.toBaggageHeader()!, headers); + + expect(headers[baggage!.name], baggage.value); + }); + + test('adds baggage header from span', () { + final headers = {}; + final sut = fixture.getSut(); + final baggage = sut.toBaggageHeader(); + + addBaggageHeaderFromSpan(sut, headers); expect(headers[baggage!.name], baggage.value); }); - test('appends baggage header', () { + test('appends baggage header from span', () { final headers = {}; final oldValue = 'other-vendor-value-1=foo'; headers['baggage'] = oldValue; @@ -87,7 +107,7 @@ void main() { final newValue = '$oldValue,${baggage!.value}'; - addBaggageHeader(sut, headers); + addBaggageHeaderFromSpan(sut, headers); expect(headers[baggage.name], newValue); }); @@ -102,7 +122,7 @@ void main() { final sut = fixture.getSut(); final baggage = sut.toBaggageHeader(); - addBaggageHeader(sut, headers); + addBaggageHeaderFromSpan(sut, headers); expect(headers[baggage!.name], 'other-vendor-value=foo,sentry-trace_id=${sut.context.traceId},sentry-public_key=abc,sentry-release=release,sentry-environment=environment,sentry-user_segment=segment,sentry-transaction=name,sentry-sample_rate=1'); diff --git a/dio/lib/src/tracing_client_adapter.dart b/dio/lib/src/tracing_client_adapter.dart index 506339e7d4..ab06c5b685 100644 --- a/dio/lib/src/tracing_client_adapter.dart +++ b/dio/lib/src/tracing_client_adapter.dart @@ -57,8 +57,8 @@ class TracingClientAdapter implements HttpClientAdapter { _hub.options.tracePropagationTargets, options.uri.toString(), )) { - addSentryTraceHeader(span, options.headers); - addBaggageHeader( + addSentryTraceHeaderFromSpan(span, options.headers); + addBaggageHeaderFromSpan( span, options.headers, // ignore: invalid_use_of_internal_member