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