diff --git a/CHANGELOG.md b/CHANGELOG.md index a0382cbb90..e86fe20b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ ## Unreleased -### Features +## Features +- 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)) 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/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() { diff --git a/test/Sentry.Tests/ScopeTests.cs b/test/Sentry.Tests/ScopeTests.cs index 8040666b10..4b46d62c3f 100644 --- a/test/Sentry.Tests/ScopeTests.cs +++ b/test/Sentry.Tests/ScopeTests.cs @@ -290,6 +290,111 @@ 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 + }); + var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op"); + scope.Transaction = transaction; + + var expectedTraceId = scope.PropagationContext.TraceId; + var expectedSpanId = scope.PropagationContext.SpanId; + var expectedCount = enableScopeSync ? 1 : 0; + + observer.ClearReceivedCalls(); + + // 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; + + var expectedTraceId = scope.PropagationContext.TraceId; + var expectedSpanId = scope.PropagationContext.SpanId; + var expectedCount = enableScopeSync ? 1 : 0; + + observer.ClearReceivedCalls(); + + // 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; + + observer.ClearReceivedCalls(); + + // Act + scope.ResetTransaction(differentTransaction); + + // Assert + observer.DidNotReceive().SetTrace(Arg.Any(), Arg.Any()); + } + [Fact] public void AddAttachment_AddAttachments() {