From ef07b7558c6443a87be13427402cb64cc2646693 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 11 Jul 2025 09:47:54 +0000 Subject: [PATCH 1/9] Add PropagationContext constructors and copy method for trace continuity Co-authored-by: giancarlo.buenaflor --- dart/lib/src/propagation_context.dart | 9 +++++++++ dart/lib/src/scope.dart | 3 +++ 2 files changed, 12 insertions(+) diff --git a/dart/lib/src/propagation_context.dart b/dart/lib/src/propagation_context.dart index 7afb1514eb..1e85cfc269 100644 --- a/dart/lib/src/propagation_context.dart +++ b/dart/lib/src/propagation_context.dart @@ -15,6 +15,15 @@ class PropagationContext { /// The dynamic sampling context. SentryBaggage? baggage; + /// Creates a new PropagationContext with random traceId and spanId + PropagationContext(); + + /// Creates a copy of an existing PropagationContext + PropagationContext.copy(PropagationContext other) + : traceId = SentryId.fromId(other.traceId.toString()), + spanId = SpanId.fromId(other.spanId.toString()), + baggage = other.baggage; + /// Baggage header to attach to http headers. SentryBaggageHeader? toBaggageHeader() => baggage != null ? SentryBaggageHeader.fromBaggage(baggage!) : null; diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index daa3882aa1..9cbd4f24fc 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -428,6 +428,9 @@ class Scope { .._enableScopeSync = false .._replayId = _replayId; + // Copy propagation context to maintain trace continuity + clone.propagationContext = PropagationContext.copy(propagationContext); + clone._setUserSync(user); final tags = List.from(_tags.keys); From 4a7df4b6df71292ac33bf2e04e400846b7d163c1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 11 Jul 2025 09:59:33 +0000 Subject: [PATCH 2/9] Remove PropagationContext constructors and share context in Scope clone Co-authored-by: giancarlo.buenaflor --- dart/lib/src/propagation_context.dart | 9 --------- dart/lib/src/scope.dart | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/dart/lib/src/propagation_context.dart b/dart/lib/src/propagation_context.dart index 1e85cfc269..7afb1514eb 100644 --- a/dart/lib/src/propagation_context.dart +++ b/dart/lib/src/propagation_context.dart @@ -15,15 +15,6 @@ class PropagationContext { /// The dynamic sampling context. SentryBaggage? baggage; - /// Creates a new PropagationContext with random traceId and spanId - PropagationContext(); - - /// Creates a copy of an existing PropagationContext - PropagationContext.copy(PropagationContext other) - : traceId = SentryId.fromId(other.traceId.toString()), - spanId = SpanId.fromId(other.spanId.toString()), - baggage = other.baggage; - /// Baggage header to attach to http headers. SentryBaggageHeader? toBaggageHeader() => baggage != null ? SentryBaggageHeader.fromBaggage(baggage!) : null; diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 9cbd4f24fc..45255d3557 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -428,8 +428,8 @@ class Scope { .._enableScopeSync = false .._replayId = _replayId; - // Copy propagation context to maintain trace continuity - clone.propagationContext = PropagationContext.copy(propagationContext); + // Share the same propagation context to maintain trace continuity + clone.propagationContext = propagationContext; clone._setUserSync(user); From 94faec8c91468db1508f5b362d5ca962b1e474d2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 11 Jul 2025 11:39:43 +0000 Subject: [PATCH 3/9] Fix trace ID consistency in scope propagation and cloning Co-authored-by: giancarlo.buenaflor --- dart/test/hub_test.dart | 44 ++++++++++++++++++++++++++ dart/test/scope_test.dart | 65 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index a6c0b3554c..2680b4f741 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -779,6 +779,50 @@ void main() { expect(calls[2].scope?.user, isNull); expect(calls[2].formatted, 'foo bar 2'); }); + + test('withScope shares propagation context to fix trace ID inconsistency', () async { + final hub = fixture.getSut(); + + // Set up baggage on the hub's scope + hub.scope.propagationContext.baggage = SentryBaggage({'service': 'test'}); + + // Get the original trace context + final originalTraceId = hub.scope.propagationContext.traceId; + final originalSpanId = hub.scope.propagationContext.spanId; + final originalTraceHeader = hub.scope.propagationContext.toSentryTrace(); + + // Capture variables from within the withScope callback + SentryId? withScopeTraceId; + SpanId? withScopeSpanId; + SentryTraceHeader? withScopeTraceHeader; + + await hub.captureException(Exception('test'), withScope: (scope) async { + withScopeTraceId = scope.propagationContext.traceId; + withScopeSpanId = scope.propagationContext.spanId; + withScopeTraceHeader = scope.propagationContext.toSentryTrace(); + + // Verify the propagation context is shared (same instance) + expect(identical(hub.scope.propagationContext, scope.propagationContext), true, + reason: 'Propagation context should be shared between hub scope and withScope'); + }); + + // Verify trace IDs are the same (fixes issue #3067) + expect(withScopeTraceId?.toString(), originalTraceId.toString(), + reason: 'Trace IDs should be identical between hub scope and withScope'); + + // Verify span IDs are the same + expect(withScopeSpanId?.toString(), originalSpanId.toString(), + reason: 'Span IDs should be identical between hub scope and withScope'); + + // Verify trace headers are the same (this is what gets sent in HTTP requests) + expect(withScopeTraceHeader?.value, originalTraceHeader.value, + reason: 'Trace headers should be identical for HTTP requests and Sentry events'); + + // Verify this solves the original issue where HTTP Request Header + // would have different trace ID than the event sent to Sentry + expect(originalTraceHeader.traceId, withScopeTraceHeader?.traceId, + reason: 'HTTP Request Header trace ID should match Sentry event trace ID'); + }); }); group('ClientReportRecorder', () { diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index 6e9732f5d4..b988b15574 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -411,6 +411,71 @@ void main() { expect(0, fixture.mockScopeObserver.numberOfSetContextsCalls); }); + test('clone shares propagation context to maintain trace continuity', () { + final sut = fixture.getSut(); + + // Get the original propagation context + final originalTraceId = sut.propagationContext.traceId; + final originalSpanId = sut.propagationContext.spanId; + + // Clone the scope + final clone = sut.clone(); + + // Verify the propagation context is shared (same instance) + expect(identical(sut.propagationContext, clone.propagationContext), true, + reason: 'Propagation context should be the same instance'); + + // Verify trace IDs are the same + expect(clone.propagationContext.traceId.toString(), + originalTraceId.toString(), + reason: 'Trace IDs should be identical'); + + // Verify span IDs are the same + expect(clone.propagationContext.spanId.toString(), + originalSpanId.toString(), + reason: 'Span IDs should be identical'); + + // Verify baggage is shared + sut.propagationContext.baggage = SentryBaggage({'test': 'value'}); + expect(clone.propagationContext.baggage?.items['test'], 'value', + reason: 'Baggage should be shared between original and clone'); + + // Verify modifications to propagation context affect both scopes + clone.propagationContext.baggage = SentryBaggage({'shared': 'data'}); + expect(sut.propagationContext.baggage?.items['shared'], 'data', + reason: 'Modifications to propagation context should affect both scopes'); + }); + + test('clone preserves trace context for withScope scenario', () { + final sut = fixture.getSut(); + + // Set up baggage on the original scope + sut.propagationContext.baggage = SentryBaggage({'service': 'test-service'}); + + // Get the original trace context + final originalTraceHeader = sut.propagationContext.toSentryTrace(); + final originalBaggageHeader = sut.propagationContext.toBaggageHeader(); + + // Clone the scope (this simulates what happens in withScope) + final clone = sut.clone(); + + // Verify the trace headers are identical + final clonedTraceHeader = clone.propagationContext.toSentryTrace(); + final clonedBaggageHeader = clone.propagationContext.toBaggageHeader(); + + expect(clonedTraceHeader.value, originalTraceHeader.value, + reason: 'Sentry trace header should be identical'); + expect(clonedBaggageHeader?.value, originalBaggageHeader?.value, + reason: 'Baggage header should be identical'); + + // Verify this fixes the issue where HTTP headers and Sentry events + // would have different trace IDs + expect(originalTraceHeader.traceId, clonedTraceHeader.traceId, + reason: 'Trace IDs in headers should match'); + expect(originalTraceHeader.spanId, clonedTraceHeader.spanId, + reason: 'Span IDs in headers should match'); + }); + group('Scope apply', () { final scopeUser = SentryUser( id: '800', From 514f1470b630723b47c5a5d19323ac8887455b37 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 11 Jul 2025 13:56:33 +0200 Subject: [PATCH 4/9] Update --- dart/lib/src/scope.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 45255d3557..fef52b5d9f 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -426,11 +426,9 @@ class Scope { .._transaction = _transaction ..span = span .._enableScopeSync = false + ..propagationContext = propagationContext .._replayId = _replayId; - // Share the same propagation context to maintain trace continuity - clone.propagationContext = propagationContext; - clone._setUserSync(user); final tags = List.from(_tags.keys); From a1ab02352aae1069fc7cdf3fd9f944f6dda76539 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 11 Jul 2025 13:58:33 +0200 Subject: [PATCH 5/9] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 468d2a8279..7c69b546da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - `ScreenshotIntegration` not being added for web ([#3055](https://github.com/getsentry/sentry-dart/pull/3055)) +- `PropagationContext` not being set when `Scope` is cloned resulting in different trace ids when using `withScope` ([#3069](https://github.com/getsentry/sentry-dart/pull/3069)) ### Enhancements From 2afe08056974e0c34701c2fe45aa24a4bbdccf1c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 11 Jul 2025 14:01:21 +0200 Subject: [PATCH 6/9] Update test --- dart/test/scope_test.dart | 50 ++++++++++----------------------------- 1 file changed, 13 insertions(+), 37 deletions(-) diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index b988b15574..84fa08089e 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -413,67 +413,43 @@ void main() { test('clone shares propagation context to maintain trace continuity', () { final sut = fixture.getSut(); - - // Get the original propagation context - final originalTraceId = sut.propagationContext.traceId; - final originalSpanId = sut.propagationContext.spanId; - + // Clone the scope final clone = sut.clone(); - + // Verify the propagation context is shared (same instance) expect(identical(sut.propagationContext, clone.propagationContext), true, reason: 'Propagation context should be the same instance'); - - // Verify trace IDs are the same - expect(clone.propagationContext.traceId.toString(), - originalTraceId.toString(), - reason: 'Trace IDs should be identical'); - - // Verify span IDs are the same - expect(clone.propagationContext.spanId.toString(), - originalSpanId.toString(), - reason: 'Span IDs should be identical'); - - // Verify baggage is shared - sut.propagationContext.baggage = SentryBaggage({'test': 'value'}); - expect(clone.propagationContext.baggage?.items['test'], 'value', - reason: 'Baggage should be shared between original and clone'); - - // Verify modifications to propagation context affect both scopes - clone.propagationContext.baggage = SentryBaggage({'shared': 'data'}); - expect(sut.propagationContext.baggage?.items['shared'], 'data', - reason: 'Modifications to propagation context should affect both scopes'); }); test('clone preserves trace context for withScope scenario', () { final sut = fixture.getSut(); - + // Set up baggage on the original scope sut.propagationContext.baggage = SentryBaggage({'service': 'test-service'}); - + // Get the original trace context final originalTraceHeader = sut.propagationContext.toSentryTrace(); final originalBaggageHeader = sut.propagationContext.toBaggageHeader(); - + // Clone the scope (this simulates what happens in withScope) final clone = sut.clone(); - + // Verify the trace headers are identical final clonedTraceHeader = clone.propagationContext.toSentryTrace(); final clonedBaggageHeader = clone.propagationContext.toBaggageHeader(); - + expect(clonedTraceHeader.value, originalTraceHeader.value, - reason: 'Sentry trace header should be identical'); + reason: 'Sentry trace header should be identical'); expect(clonedBaggageHeader?.value, originalBaggageHeader?.value, - reason: 'Baggage header should be identical'); - - // Verify this fixes the issue where HTTP headers and Sentry events + reason: 'Baggage header should be identical'); + + // Verify this fixes the issue where HTTP headers and Sentry events // would have different trace IDs expect(originalTraceHeader.traceId, clonedTraceHeader.traceId, - reason: 'Trace IDs in headers should match'); + reason: 'Trace IDs in headers should match'); expect(originalTraceHeader.spanId, clonedTraceHeader.spanId, - reason: 'Span IDs in headers should match'); + reason: 'Span IDs in headers should match'); }); group('Scope apply', () { From b87f697f752f06745ee709b4a82e3f0384032f96 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 11 Jul 2025 14:02:32 +0200 Subject: [PATCH 7/9] Update test --- dart/test/scope_test.dart | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index e09bb69964..a7824baac5 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -420,36 +420,7 @@ void main() { // Verify the propagation context is shared (same instance) expect(identical(sut.propagationContext, clone.propagationContext), true, reason: 'Propagation context should be the same instance'); - }); - - test('clone preserves trace context for withScope scenario', () { - final sut = fixture.getSut(); - - // Set up baggage on the original scope - sut.propagationContext.baggage = SentryBaggage({'service': 'test-service'}); - - // Get the original trace context - final originalTraceHeader = sut.propagationContext.toSentryTrace(); - final originalBaggageHeader = sut.propagationContext.toBaggageHeader(); - - // Clone the scope (this simulates what happens in withScope) - final clone = sut.clone(); - - // Verify the trace headers are identical - final clonedTraceHeader = clone.propagationContext.toSentryTrace(); - final clonedBaggageHeader = clone.propagationContext.toBaggageHeader(); - - expect(clonedTraceHeader.value, originalTraceHeader.value, - reason: 'Sentry trace header should be identical'); - expect(clonedBaggageHeader?.value, originalBaggageHeader?.value, - reason: 'Baggage header should be identical'); - - // Verify this fixes the issue where HTTP headers and Sentry events - // would have different trace IDs - expect(originalTraceHeader.traceId, clonedTraceHeader.traceId, - reason: 'Trace IDs in headers should match'); - expect(originalTraceHeader.spanId, clonedTraceHeader.spanId, - reason: 'Span IDs in headers should match'); + expect(clone.propagationContext.traceId, sut.propagationContext.traceId); }); group('Scope apply', () { From 474df44d40718416fafdafc626e1e87867e76aeb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 11 Jul 2025 14:03:39 +0200 Subject: [PATCH 8/9] Update test --- dart/test/hub_test.dart | 44 ----------------------------------------- 1 file changed, 44 deletions(-) diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 2680b4f741..a6c0b3554c 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -779,50 +779,6 @@ void main() { expect(calls[2].scope?.user, isNull); expect(calls[2].formatted, 'foo bar 2'); }); - - test('withScope shares propagation context to fix trace ID inconsistency', () async { - final hub = fixture.getSut(); - - // Set up baggage on the hub's scope - hub.scope.propagationContext.baggage = SentryBaggage({'service': 'test'}); - - // Get the original trace context - final originalTraceId = hub.scope.propagationContext.traceId; - final originalSpanId = hub.scope.propagationContext.spanId; - final originalTraceHeader = hub.scope.propagationContext.toSentryTrace(); - - // Capture variables from within the withScope callback - SentryId? withScopeTraceId; - SpanId? withScopeSpanId; - SentryTraceHeader? withScopeTraceHeader; - - await hub.captureException(Exception('test'), withScope: (scope) async { - withScopeTraceId = scope.propagationContext.traceId; - withScopeSpanId = scope.propagationContext.spanId; - withScopeTraceHeader = scope.propagationContext.toSentryTrace(); - - // Verify the propagation context is shared (same instance) - expect(identical(hub.scope.propagationContext, scope.propagationContext), true, - reason: 'Propagation context should be shared between hub scope and withScope'); - }); - - // Verify trace IDs are the same (fixes issue #3067) - expect(withScopeTraceId?.toString(), originalTraceId.toString(), - reason: 'Trace IDs should be identical between hub scope and withScope'); - - // Verify span IDs are the same - expect(withScopeSpanId?.toString(), originalSpanId.toString(), - reason: 'Span IDs should be identical between hub scope and withScope'); - - // Verify trace headers are the same (this is what gets sent in HTTP requests) - expect(withScopeTraceHeader?.value, originalTraceHeader.value, - reason: 'Trace headers should be identical for HTTP requests and Sentry events'); - - // Verify this solves the original issue where HTTP Request Header - // would have different trace ID than the event sent to Sentry - expect(originalTraceHeader.traceId, withScopeTraceHeader?.traceId, - reason: 'HTTP Request Header trace ID should match Sentry event trace ID'); - }); }); group('ClientReportRecorder', () { From 1fe94a821231c3fd9e9da50bc06a6beade7e40eb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 11 Jul 2025 14:05:49 +0200 Subject: [PATCH 9/9] Update test --- dart/test/hub_test.dart | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index a6c0b3554c..6a93f670f5 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -779,6 +779,26 @@ void main() { expect(calls[2].scope?.user, isNull); expect(calls[2].formatted, 'foo bar 2'); }); + + test( + 'withScope should use the same propagation context as the current scope', + () async { + final hub = fixture.getSut(); + late Scope clonedScope; + final currentScope = hub.scope; + await hub.captureEvent(SentryEvent(), withScope: (scope) async { + clonedScope = scope; + }); + + // Verify the propagation context is shared (same instance) + expect( + identical( + clonedScope.propagationContext, currentScope.propagationContext), + true, + reason: 'Propagation context should be the same instance'); + expect(clonedScope.propagationContext.traceId, + currentScope.propagationContext.traceId); + }); }); group('ClientReportRecorder', () {