From dbbf35d65c8f34e8004d7240c5ea0de52a80148b Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 29 Apr 2025 14:45:26 +0200 Subject: [PATCH 1/5] initial commit --- src/Sentry/Scope.cs | 19 ++++++ test/Sentry.Tests/ScopeTests.cs | 109 ++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index 7e87eb6007..838c961011 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -225,6 +225,20 @@ public ITransactionTracer? Transaction try { _transaction.Value = value; + + if (Options.EnableScopeSync) + { + if (_transaction.Value != null) + { + // If there is a transaction set we propagate the trace to the native layer + Options.ScopeObserver?.SetTrace(_transaction.Value.TraceId, _transaction.Value.SpanId); + } + else + { + // If the transaction is being removed from the scope, reset and sync the trace as well + Options.ScopeObserver?.SetTrace(PropagationContext.TraceId, PropagationContext.SpanId); + } + } } finally { @@ -802,6 +816,11 @@ internal void ResetTransaction(ITransactionTracer? expectedCurrentTransaction) if (ReferenceEquals(_transaction.Value, expectedCurrentTransaction)) { _transaction.Value = null; + if (Options.EnableScopeSync) + { + // We have to restore the trace on the native layers to be in sync with the current scope + Options.ScopeObserver?.SetTrace(PropagationContext.TraceId, PropagationContext.SpanId); + } } } finally diff --git a/test/Sentry.Tests/ScopeTests.cs b/test/Sentry.Tests/ScopeTests.cs index 8040666b10..548edfcb56 100644 --- a/test/Sentry.Tests/ScopeTests.cs +++ b/test/Sentry.Tests/ScopeTests.cs @@ -290,6 +290,115 @@ public void Span_SetSpanThenCloseIt_ReturnsLatestOpen() scope.Span.Should().Be(secondSpan); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Transaction_Set_ObserverSetsTraceIfEnabled(bool enableScopeSync) + { + // Arrange + var observer = Substitute.For(); + var scope = new Scope(new SentryOptions + { + ScopeObserver = observer, + EnableScopeSync = enableScopeSync + }); + var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op"); + var expectedTraceId = transaction.TraceId; + var expectedSpanId = transaction.SpanId; + var expectedCount = enableScopeSync ? 1 : 0; + + // Act + scope.Transaction = transaction; + + // Assert + observer.Received(expectedCount).SetTrace(Arg.Is(expectedTraceId), Arg.Is(expectedSpanId)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Transaction_SetToNull_ObserverSetsTraceFromPropagationContextIfEnabled(bool enableScopeSync) + { + // Arrange + var observer = Substitute.For(); + var scope = new Scope(new SentryOptions + { + ScopeObserver = observer, + EnableScopeSync = enableScopeSync + }); + // First set a transaction + var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op"); + scope.Transaction = transaction; + + // Clear received calls to observer + observer.ClearReceivedCalls(); + + var expectedTraceId = scope.PropagationContext.TraceId; + var expectedSpanId = scope.PropagationContext.SpanId; + var expectedCount = enableScopeSync ? 1 : 0; + + // Act + scope.Transaction = null; + + // Assert + observer.Received(expectedCount).SetTrace(Arg.Is(expectedTraceId), Arg.Is(expectedSpanId)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResetTransaction_MatchingTransaction_ObserverSetsTraceFromPropagationContextIfEnabled(bool enableScopeSync) + { + // Arrange + var observer = Substitute.For(); + var scope = new Scope(new SentryOptions + { + ScopeObserver = observer, + EnableScopeSync = enableScopeSync + }); + var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op"); + scope.Transaction = transaction; + + // Clear received calls to observer + observer.ClearReceivedCalls(); + + var expectedTraceId = scope.PropagationContext.TraceId; + var expectedSpanId = scope.PropagationContext.SpanId; + var expectedCount = enableScopeSync ? 1 : 0; + + // Act + scope.ResetTransaction(transaction); + + // Assert + observer.Received(expectedCount).SetTrace(Arg.Is(expectedTraceId), Arg.Is(expectedSpanId)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ResetTransaction_NonMatchingTransaction_ObserverNotCalled(bool enableScopeSync) + { + // Arrange + var observer = Substitute.For(); + var scope = new Scope(new SentryOptions + { + ScopeObserver = observer, + EnableScopeSync = enableScopeSync + }); + var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op"); + var differentTransaction = new TransactionTracer(DisabledHub.Instance, "different", "op"); + scope.Transaction = transaction; + + // Clear received calls to observer + observer.ClearReceivedCalls(); + + // Act + scope.ResetTransaction(differentTransaction); + + // Assert + observer.DidNotReceive().SetTrace(Arg.Any(), Arg.Any()); + } + [Fact] public void AddAttachment_AddAttachments() { From f4cf166a7183d0351d2e376648218e3728968ed4 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 29 Apr 2025 15:28:15 +0200 Subject: [PATCH 2/5] tests --- test/Sentry.Tests/ScopeTests.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/Sentry.Tests/ScopeTests.cs b/test/Sentry.Tests/ScopeTests.cs index 548edfcb56..4b46d62c3f 100644 --- a/test/Sentry.Tests/ScopeTests.cs +++ b/test/Sentry.Tests/ScopeTests.cs @@ -326,17 +326,15 @@ public void Transaction_SetToNull_ObserverSetsTraceFromPropagationContextIfEnabl ScopeObserver = observer, EnableScopeSync = enableScopeSync }); - // First set a transaction var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op"); scope.Transaction = transaction; - // Clear received calls to observer - observer.ClearReceivedCalls(); - var expectedTraceId = scope.PropagationContext.TraceId; var expectedSpanId = scope.PropagationContext.SpanId; var expectedCount = enableScopeSync ? 1 : 0; + observer.ClearReceivedCalls(); + // Act scope.Transaction = null; @@ -359,13 +357,12 @@ public void ResetTransaction_MatchingTransaction_ObserverSetsTraceFromPropagatio var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op"); scope.Transaction = transaction; - // Clear received calls to observer - observer.ClearReceivedCalls(); - var expectedTraceId = scope.PropagationContext.TraceId; var expectedSpanId = scope.PropagationContext.SpanId; var expectedCount = enableScopeSync ? 1 : 0; + observer.ClearReceivedCalls(); + // Act scope.ResetTransaction(transaction); @@ -389,7 +386,6 @@ public void ResetTransaction_NonMatchingTransaction_ObserverNotCalled(bool enabl var differentTransaction = new TransactionTracer(DisabledHub.Instance, "different", "op"); scope.Transaction = transaction; - // Clear received calls to observer observer.ClearReceivedCalls(); // Act From 6179c8d01b6a4763f8d0989985817cfb6f86b15b Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 29 Apr 2025 15:49:58 +0200 Subject: [PATCH 3/5] Updated CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9973d686d7..c5bf85cb0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +## Features + +- When setting a transaction on the scope, the SDK will attempt to sync the transaction's trace with the SDK on the native layer ([#4153](https://github.com/getsentry/sentry-dotnet/pull/4153)) + ## 5.6.0 ### Features From 26c5319ca9acc3a5e76b6f3db95fc1a5e78e0fc0 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 2 May 2025 11:29:46 +0200 Subject: [PATCH 4/5] create new propagation context when transaction finishes --- src/Sentry/TransactionTracer.cs | 9 +++++-- .../Protocol/SentryTransactionTests.cs | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index c4da3d5933..23c54a5fa2 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -392,8 +392,13 @@ public void Finish() EndTimestamp ??= _stopwatch.CurrentDateTimeOffset; _options?.LogDebug("Finished Transaction {0}.", SpanId); - // Clear the transaction from the scope - _hub.ConfigureScope(scope => scope.ResetTransaction(this)); + // Clear the transaction from the scope and regenerate the Propagation Context + // We do this so new events don't have a trace context that is "older" than the transaction that just finished + _hub.ConfigureScope(scope => + { + scope.ResetTransaction(this); + scope.SetPropagationContext(new SentryPropagationContext()); + }); // Client decides whether to discard this transaction based on sampling _hub.CaptureTransaction(new SentryTransaction(this)); diff --git a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs index 4e0872ada1..c4f075dd00 100644 --- a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs +++ b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs @@ -566,6 +566,30 @@ public void Finish_ChildSpan_StatusSet_DoesNotOverride() span.Status.Should().Be(SpanStatus.DataLoss); } + [Fact] + public void Finish_ResetsScopeAndSetsNewPropagationContext() + { + // Arrange + var hub = Substitute.For(); + var transaction = new TransactionTracer(hub, "test name", "test op"); + + Action capturedAction = null; + hub.ConfigureScope(Arg.Do>(action => capturedAction = action)); + + // Act + transaction.Finish(); + + // Assert + hub.Received(1).ConfigureScope(Arg.Any>()); + + capturedAction.Should().NotBeNull(); // Sanity Check + var mockScope = Substitute.For(); + capturedAction(mockScope); + + mockScope.Received(1).ResetTransaction(transaction); + mockScope.Received(1).SetPropagationContext(Arg.Any()); + } + [Fact] public void ISpan_GetTransaction_FromTransaction() { From 7365c5a8bf88c14460b278a2d03bffeb878f9d31 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 2 May 2025 11:43:01 +0200 Subject: [PATCH 5/5] Updated CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 986ee224a1..e86fe20b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ## Features -- When setting a transaction on the scope, the SDK will attempt to sync the transaction's trace with the SDK on the native layer ([#4153](https://github.com/getsentry/sentry-dotnet/pull/4153)) +- When setting a transaction on the scope, the SDK will attempt to sync the transaction's trace context with the SDK on the native layer. Finishing a transaction will now also start a new trace ([#4153](https://github.com/getsentry/sentry-dotnet/pull/4153)) - Added `CaptureFeedback` overload with `configureScope` parameter ([#4073](https://github.com/getsentry/sentry-dotnet/pull/4073)) - Custom SessionReplay masks in MAUI Android apps ([#4121](https://github.com/getsentry/sentry-dotnet/pull/4121))