Skip to content

Commit 8abc247

Browse files
authored
feat: Observe transaction on scope (#4153)
1 parent 8978bc4 commit 8abc247

File tree

5 files changed

+157
-3
lines changed

5 files changed

+157
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
## Unreleased
44

5-
### Features
5+
## Features
66

7+
- 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))
78
- Added `CaptureFeedback` overload with `configureScope` parameter ([#4073](https://github.com/getsentry/sentry-dotnet/pull/4073))
89
- Custom SessionReplay masks in MAUI Android apps ([#4121](https://github.com/getsentry/sentry-dotnet/pull/4121))
910

src/Sentry/Scope.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,20 @@ public ITransactionTracer? Transaction
225225
try
226226
{
227227
_transaction.Value = value;
228+
229+
if (Options.EnableScopeSync)
230+
{
231+
if (_transaction.Value != null)
232+
{
233+
// If there is a transaction set we propagate the trace to the native layer
234+
Options.ScopeObserver?.SetTrace(_transaction.Value.TraceId, _transaction.Value.SpanId);
235+
}
236+
else
237+
{
238+
// If the transaction is being removed from the scope, reset and sync the trace as well
239+
Options.ScopeObserver?.SetTrace(PropagationContext.TraceId, PropagationContext.SpanId);
240+
}
241+
}
228242
}
229243
finally
230244
{
@@ -802,6 +816,11 @@ internal void ResetTransaction(ITransactionTracer? expectedCurrentTransaction)
802816
if (ReferenceEquals(_transaction.Value, expectedCurrentTransaction))
803817
{
804818
_transaction.Value = null;
819+
if (Options.EnableScopeSync)
820+
{
821+
// We have to restore the trace on the native layers to be in sync with the current scope
822+
Options.ScopeObserver?.SetTrace(PropagationContext.TraceId, PropagationContext.SpanId);
823+
}
805824
}
806825
}
807826
finally

src/Sentry/TransactionTracer.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,13 @@ public void Finish()
392392
EndTimestamp ??= _stopwatch.CurrentDateTimeOffset;
393393
_options?.LogDebug("Finished Transaction {0}.", SpanId);
394394

395-
// Clear the transaction from the scope
396-
_hub.ConfigureScope(scope => scope.ResetTransaction(this));
395+
// Clear the transaction from the scope and regenerate the Propagation Context
396+
// We do this so new events don't have a trace context that is "older" than the transaction that just finished
397+
_hub.ConfigureScope(scope =>
398+
{
399+
scope.ResetTransaction(this);
400+
scope.SetPropagationContext(new SentryPropagationContext());
401+
});
397402

398403
// Client decides whether to discard this transaction based on sampling
399404
_hub.CaptureTransaction(new SentryTransaction(this));

test/Sentry.Tests/Protocol/SentryTransactionTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,30 @@ public void Finish_ChildSpan_StatusSet_DoesNotOverride()
566566
span.Status.Should().Be(SpanStatus.DataLoss);
567567
}
568568

569+
[Fact]
570+
public void Finish_ResetsScopeAndSetsNewPropagationContext()
571+
{
572+
// Arrange
573+
var hub = Substitute.For<IHub>();
574+
var transaction = new TransactionTracer(hub, "test name", "test op");
575+
576+
Action<Scope> capturedAction = null;
577+
hub.ConfigureScope(Arg.Do<Action<Scope>>(action => capturedAction = action));
578+
579+
// Act
580+
transaction.Finish();
581+
582+
// Assert
583+
hub.Received(1).ConfigureScope(Arg.Any<Action<Scope>>());
584+
585+
capturedAction.Should().NotBeNull(); // Sanity Check
586+
var mockScope = Substitute.For<Scope>();
587+
capturedAction(mockScope);
588+
589+
mockScope.Received(1).ResetTransaction(transaction);
590+
mockScope.Received(1).SetPropagationContext(Arg.Any<SentryPropagationContext>());
591+
}
592+
569593
[Fact]
570594
public void ISpan_GetTransaction_FromTransaction()
571595
{

test/Sentry.Tests/ScopeTests.cs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,111 @@ public void Span_SetSpanThenCloseIt_ReturnsLatestOpen()
290290
scope.Span.Should().Be(secondSpan);
291291
}
292292

293+
[Theory]
294+
[InlineData(true)]
295+
[InlineData(false)]
296+
public void Transaction_Set_ObserverSetsTraceIfEnabled(bool enableScopeSync)
297+
{
298+
// Arrange
299+
var observer = Substitute.For<IScopeObserver>();
300+
var scope = new Scope(new SentryOptions
301+
{
302+
ScopeObserver = observer,
303+
EnableScopeSync = enableScopeSync
304+
});
305+
var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op");
306+
var expectedTraceId = transaction.TraceId;
307+
var expectedSpanId = transaction.SpanId;
308+
var expectedCount = enableScopeSync ? 1 : 0;
309+
310+
// Act
311+
scope.Transaction = transaction;
312+
313+
// Assert
314+
observer.Received(expectedCount).SetTrace(Arg.Is(expectedTraceId), Arg.Is(expectedSpanId));
315+
}
316+
317+
[Theory]
318+
[InlineData(true)]
319+
[InlineData(false)]
320+
public void Transaction_SetToNull_ObserverSetsTraceFromPropagationContextIfEnabled(bool enableScopeSync)
321+
{
322+
// Arrange
323+
var observer = Substitute.For<IScopeObserver>();
324+
var scope = new Scope(new SentryOptions
325+
{
326+
ScopeObserver = observer,
327+
EnableScopeSync = enableScopeSync
328+
});
329+
var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op");
330+
scope.Transaction = transaction;
331+
332+
var expectedTraceId = scope.PropagationContext.TraceId;
333+
var expectedSpanId = scope.PropagationContext.SpanId;
334+
var expectedCount = enableScopeSync ? 1 : 0;
335+
336+
observer.ClearReceivedCalls();
337+
338+
// Act
339+
scope.Transaction = null;
340+
341+
// Assert
342+
observer.Received(expectedCount).SetTrace(Arg.Is(expectedTraceId), Arg.Is(expectedSpanId));
343+
}
344+
345+
[Theory]
346+
[InlineData(true)]
347+
[InlineData(false)]
348+
public void ResetTransaction_MatchingTransaction_ObserverSetsTraceFromPropagationContextIfEnabled(bool enableScopeSync)
349+
{
350+
// Arrange
351+
var observer = Substitute.For<IScopeObserver>();
352+
var scope = new Scope(new SentryOptions
353+
{
354+
ScopeObserver = observer,
355+
EnableScopeSync = enableScopeSync
356+
});
357+
var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op");
358+
scope.Transaction = transaction;
359+
360+
var expectedTraceId = scope.PropagationContext.TraceId;
361+
var expectedSpanId = scope.PropagationContext.SpanId;
362+
var expectedCount = enableScopeSync ? 1 : 0;
363+
364+
observer.ClearReceivedCalls();
365+
366+
// Act
367+
scope.ResetTransaction(transaction);
368+
369+
// Assert
370+
observer.Received(expectedCount).SetTrace(Arg.Is(expectedTraceId), Arg.Is(expectedSpanId));
371+
}
372+
373+
[Theory]
374+
[InlineData(true)]
375+
[InlineData(false)]
376+
public void ResetTransaction_NonMatchingTransaction_ObserverNotCalled(bool enableScopeSync)
377+
{
378+
// Arrange
379+
var observer = Substitute.For<IScopeObserver>();
380+
var scope = new Scope(new SentryOptions
381+
{
382+
ScopeObserver = observer,
383+
EnableScopeSync = enableScopeSync
384+
});
385+
var transaction = new TransactionTracer(DisabledHub.Instance, "test-transaction", "op");
386+
var differentTransaction = new TransactionTracer(DisabledHub.Instance, "different", "op");
387+
scope.Transaction = transaction;
388+
389+
observer.ClearReceivedCalls();
390+
391+
// Act
392+
scope.ResetTransaction(differentTransaction);
393+
394+
// Assert
395+
observer.DidNotReceive().SetTrace(Arg.Any<SentryId>(), Arg.Any<SpanId>());
396+
}
397+
293398
[Fact]
294399
public void AddAttachment_AddAttachments()
295400
{

0 commit comments

Comments
 (0)