diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed7eb87c3..dba8a54740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Features +- The `IScopeObserver` now has an `SetTrace` that allows observing changes to the scope's trace context. The SDK uses this to propagate the `trace ID` to `sentry-native`. This allows Sentry to connect errors coming from all layers of your application ([#4026](https://github.com/getsentry/sentry-dotnet/pull/4026)) - Exception.HResult is now included in the mechanism data for all exceptions ([#4029](https://github.com/getsentry/sentry-dotnet/pull/4029)) ### Dependencies diff --git a/src/Sentry/IScopeObserver.cs b/src/Sentry/IScopeObserver.cs index 591188b3c8..2e8c8ec6ab 100644 --- a/src/Sentry/IScopeObserver.cs +++ b/src/Sentry/IScopeObserver.cs @@ -29,4 +29,9 @@ public interface IScopeObserver /// Sets the user information. /// public void SetUser(SentryUser? user); + + /// + /// Sets the current trace + /// + public void SetTrace(SentryId traceId, SpanId parentSpanId); } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index cd00686250..e7234b6356 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -255,7 +255,7 @@ public TransactionContext ContinueTrace( string? operation = null) { var propagationContext = SentryPropagationContext.CreateFromHeaders(_options.DiagnosticLogger, traceHeader, baggageHeader); - ConfigureScope(scope => scope.PropagationContext = propagationContext); + ConfigureScope(scope => scope.SetPropagationContext(propagationContext)); return new TransactionContext( name: name ?? string.Empty, diff --git a/src/Sentry/Internal/ScopeObserver.cs b/src/Sentry/Internal/ScopeObserver.cs index 8e99626e4e..feb1411747 100644 --- a/src/Sentry/Internal/ScopeObserver.cs +++ b/src/Sentry/Internal/ScopeObserver.cs @@ -84,4 +84,14 @@ public void SetUser(SentryUser? user) public abstract void SetUserImpl(SentryUser user); public abstract void UnsetUserImpl(); + + public void SetTrace(SentryId traceId, SpanId parentSpanId) + { + _options.DiagnosticLogger?.Log( + SentryLevel.Debug, "{0} Scope Sync - Setting Trace traceId:{1} parentSpanId:{2}", null, + _name, traceId, parentSpanId); + SetTraceImpl(traceId, parentSpanId); + } + + public abstract void SetTraceImpl(SentryId traceId, SpanId parentSpanId); } diff --git a/src/Sentry/Platforms/Android/AndroidScopeObserver.cs b/src/Sentry/Platforms/Android/AndroidScopeObserver.cs index dcedfa611e..c2a272061f 100644 --- a/src/Sentry/Platforms/Android/AndroidScopeObserver.cs +++ b/src/Sentry/Platforms/Android/AndroidScopeObserver.cs @@ -99,4 +99,9 @@ public void SetUser(SentryUser? user) _innerObserver?.SetUser(user); } } + + public void SetTrace(SentryId traceId, SpanId parentSpanId) + { + // TODO: This requires sentry-java 8.4.0 + } } diff --git a/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs b/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs index f36271f320..d4e7def7a8 100644 --- a/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs +++ b/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs @@ -107,4 +107,9 @@ public void SetUser(SentryUser? user) _innerObserver?.SetUser(user); } } + + public void SetTrace(SentryId traceId, SpanId parentSpanId) + { + // TODO: Missing corresponding functionality on the Cocoa SDK + } } diff --git a/src/Sentry/Platforms/Native/CFunctions.cs b/src/Sentry/Platforms/Native/CFunctions.cs index 05be757fa9..0b5465bd6b 100644 --- a/src/Sentry/Platforms/Native/CFunctions.cs +++ b/src/Sentry/Platforms/Native/CFunctions.cs @@ -241,6 +241,9 @@ internal static string GetCacheDirectory(SentryOptions options) [DllImport("sentry-native")] internal static extern void sentry_remove_extra(string key); + [DllImport("sentry-native")] + internal static extern void sentry_set_trace(string traceId, string parentSpanId); + internal static Dictionary LoadDebugImages(IDiagnosticLogger? logger) { // It only makes sense to load them once because they're cached on the native side anyway. We could force diff --git a/src/Sentry/Platforms/Native/NativeScopeObserver.cs b/src/Sentry/Platforms/Native/NativeScopeObserver.cs index 68b8bc6e57..b278bf1e83 100644 --- a/src/Sentry/Platforms/Native/NativeScopeObserver.cs +++ b/src/Sentry/Platforms/Native/NativeScopeObserver.cs @@ -40,6 +40,9 @@ public override void SetUserImpl(SentryUser user) public override void UnsetUserImpl() => C.sentry_remove_user(); + public override void SetTraceImpl(SentryId traceId, SpanId parentSpanId) => + C.sentry_set_trace(traceId.ToString(), parentSpanId.ToString()); + private static string GetTimestamp(DateTimeOffset timestamp) => // "o": Using ISO 8601 to make sure the timestamp makes it to the bridge correctly. // https://docs.microsoft.com/en-gb/dotnet/standard/base-types/standard-date-and-time-format-strings#Roundtrip diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index 9cd253b216..7e87eb6007 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -233,7 +233,7 @@ public ITransactionTracer? Transaction } } - internal SentryPropagationContext PropagationContext { get; set; } + internal SentryPropagationContext PropagationContext { get; private set; } internal SessionUpdate? SessionUpdate { get; set; } @@ -376,6 +376,15 @@ public void UnsetTag(string key) /// public void AddAttachment(SentryAttachment attachment) => _attachments.Add(attachment); + internal void SetPropagationContext(SentryPropagationContext propagationContext) + { + PropagationContext = propagationContext; + if (Options.EnableScopeSync) + { + Options.ScopeObserver?.SetTrace(propagationContext.TraceId, propagationContext.SpanId); + } + } + /// /// Resets all the properties and collections within the scope to their default values. /// diff --git a/src/Sentry/Sentry.csproj b/src/Sentry/Sentry.csproj index f7eb60f38a..49632a39aa 100644 --- a/src/Sentry/Sentry.csproj +++ b/src/Sentry/Sentry.csproj @@ -16,8 +16,14 @@ netstandard2.0;netstandard2.1 + + false + + + + diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 401a0fa6f0..8c5efbd967 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -209,6 +209,13 @@ internal static IDisposable UseHub(IHub hub) return new DisposeHandle(hub); } + /// + /// Allows to set the trace + /// + internal static void SetTrace(SentryId traceId, SpanId parentSpanId) => + CurrentHub.ConfigureScope(scope => + scope.SetPropagationContext(new SentryPropagationContext(traceId, parentSpanId))); + /// /// Flushes the queue of captured events until the timeout set in /// is reached. diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 67795954b7..93d72d716c 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -228,6 +228,7 @@ namespace Sentry void AddBreadcrumb(Sentry.Breadcrumb breadcrumb); void SetExtra(string key, object? value); void SetTag(string key, string value); + void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId); void SetUser(Sentry.SentryUser? user); void UnsetTag(string key); } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 67795954b7..93d72d716c 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -228,6 +228,7 @@ namespace Sentry void AddBreadcrumb(Sentry.Breadcrumb breadcrumb); void SetExtra(string key, object? value); void SetTag(string key, string value); + void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId); void SetUser(Sentry.SentryUser? user); void UnsetTag(string key); } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 0c06282d34..75fa5ad89d 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -216,6 +216,7 @@ namespace Sentry void AddBreadcrumb(Sentry.Breadcrumb breadcrumb); void SetExtra(string key, object? value); void SetTag(string key, string value); + void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId); void SetUser(Sentry.SentryUser? user); void UnsetTag(string key); } diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index d22326b119..f0c141617e 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1013,7 +1013,7 @@ public void GetTraceHeader_NoSpanActive_ReturnsHeaderFromPropagationContext() var propagationContext = new SentryPropagationContext( SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"), SpanId.Parse("2000000000000000")); - hub.ConfigureScope(scope => scope.PropagationContext = propagationContext); + hub.ConfigureScope(scope => scope.SetPropagationContext(propagationContext)); // Act var header = hub.GetTraceHeader(); @@ -1052,7 +1052,7 @@ public void GetBaggage_NoSpanActive_ReturnsBaggageFromPropagationContext() var hub = _fixture.GetSut(); var propagationContext = new SentryPropagationContext( SentryId.Parse("43365712692146d08ee11a729dfbcaca"), SpanId.Parse("1000000000000000")); - hub.ConfigureScope(scope => scope.PropagationContext = propagationContext); + hub.ConfigureScope(scope => scope.SetPropagationContext(propagationContext)); // Act var baggage = hub.GetBaggage(); @@ -1069,7 +1069,7 @@ public void ContinueTrace_SetsPropagationContextAndReturnsTransactionContext() var hub = _fixture.GetSut(); var propagationContext = new SentryPropagationContext( SentryId.Parse("43365712692146d08ee11a729dfbcaca"), SpanId.Parse("1000000000000000")); - hub.ConfigureScope(scope => scope.PropagationContext = propagationContext); + hub.ConfigureScope(scope => scope.SetPropagationContext(propagationContext)); var traceHeader = new SentryTraceHeader(SentryId.Parse("5bd5f6d346b442dd9177dce9302fd737"), SpanId.Parse("2000000000000000"), null); @@ -1104,7 +1104,7 @@ public void ContinueTrace_ReceivesHeadersAsStrings_SetsPropagationContextAndRetu var hub = _fixture.GetSut(); var propagationContext = new SentryPropagationContext( SentryId.Parse("43365712692146d08ee11a729dfbcaca"), SpanId.Parse("1000000000000000")); - hub.ConfigureScope(scope => scope.PropagationContext = propagationContext); + hub.ConfigureScope(scope => scope.SetPropagationContext(propagationContext)); var traceHeader = "5bd5f6d346b442dd9177dce9302fd737-2000000000000000"; var baggageHeader = "sentry-trace_id=5bd5f6d346b442dd9177dce9302fd737, sentry-public_key=49d0f7386ad645858ae85020e393bef3, sentry-sample_rate=1.0"; diff --git a/test/Sentry.Tests/ScopeTests.cs b/test/Sentry.Tests/ScopeTests.cs index ece48d5170..8040666b10 100644 --- a/test/Sentry.Tests/ScopeTests.cs +++ b/test/Sentry.Tests/ScopeTests.cs @@ -633,6 +633,31 @@ public void SetTag_NullValue_DoesNotThrowArgumentNullException() Assert.Null(exception); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SetPropagationContext_ObserverExist_ObserverSetsTraceIfEnabled(bool enableScopeSync) + { + // Arrange + var observer = Substitute.For(); + var scope = new Scope(new SentryOptions + { + ScopeObserver = observer, + EnableScopeSync = enableScopeSync + }); + var propagationContext = new SentryPropagationContext(); + var expectedTraceId = propagationContext.TraceId; + var expectedSpanId = propagationContext.SpanId; + var expectedCount = enableScopeSync ? 1 : 0; + + // Act + scope.SetPropagationContext(propagationContext); + + // Assert + scope.PropagationContext.Should().Be(propagationContext); + observer.Received(expectedCount).SetTrace(Arg.Is(expectedTraceId), Arg.Is(expectedSpanId)); + } } public static class ScopeTestExtensions