From 3ef1422520b6e8f062d08e898fe7cc3977a91e85 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 23 Apr 2025 11:14:20 +1200 Subject: [PATCH 01/28] Associate replays with errors and traces on Android Part of #2136 Associate Errors and Traces with the active Session Replay (if one exists) on Android. --- .../Android/AndroidEventProcessor.cs | 10 +++ src/Sentry/Protocol/Replay.cs | 88 +++++++++++++++++++ src/Sentry/SentryContexts.cs | 9 ++ 3 files changed, 107 insertions(+) create mode 100644 src/Sentry/Protocol/Replay.cs diff --git a/src/Sentry/Platforms/Android/AndroidEventProcessor.cs b/src/Sentry/Platforms/Android/AndroidEventProcessor.cs index cba15fd78e..dc483fa8ec 100644 --- a/src/Sentry/Platforms/Android/AndroidEventProcessor.cs +++ b/src/Sentry/Platforms/Android/AndroidEventProcessor.cs @@ -1,3 +1,5 @@ +using Java.Lang; +using Kotlin; using Sentry.Android.Extensions; using Sentry.Extensibility; using Sentry.JavaSdk.Android.Core; @@ -33,6 +35,14 @@ public SentryEvent Process(SentryEvent @event) // Copy more information from the Android SDK if (_androidProcessor is { } androidProcessor) { + // TODO: We should really do this when creating the DSC, so that it gets propagated correctly + // Check to see if a Replay ID is available + var activeReplayId = Sentry.JavaSdk.ScopesAdapter.Instance?.Options?.ReplayController?.ReplayId?.ToSentryId(); + if (activeReplayId is {} replayId && replayId != SentryId.Empty) + { + @event.Contexts.Replay.ReplayId = replayId; + } + // TODO: Can we gather more data directly and remove this? // Run a fake event through the Android processor, so we can get context info from the Android SDK. diff --git a/src/Sentry/Protocol/Replay.cs b/src/Sentry/Protocol/Replay.cs new file mode 100644 index 0000000000..5a74789c7b --- /dev/null +++ b/src/Sentry/Protocol/Replay.cs @@ -0,0 +1,88 @@ +using Sentry.Extensibility; +using Sentry.Internal; +using Sentry.Internal.Extensions; + +namespace Sentry.Protocol; + +/// +/// Sentry Replay context interface. +/// +/// +/// { +/// "contexts": { +/// "replay": { +/// "replay_id": "12312012123120121231201212312012" +/// } +/// } +/// } +/// +/// +public sealed class Replay : ISentryJsonSerializable, ICloneable, IUpdatable +{ + /// + /// Tells Sentry which type of context this is. + /// + public const string Type = "replay"; + + /// + /// The name of the runtime. + /// + public SentryId? ReplayId { get; set; } + + /// + /// Clones this instance. + /// + public Replay Clone() + { + var response = new Replay(); + + response.UpdateFrom(this); + + return response; + } + + /// + /// Updates this instance with data from the properties in the , + /// unless there is already a value in the existing property. + /// + public void UpdateFrom(Replay source) + { + ReplayId ??= source.ReplayId; + } + + /// + /// Updates this instance with data from the properties in the , + /// unless there is already a value in the existing property. + /// + public void UpdateFrom(object source) + { + if (source is Replay response) + { + UpdateFrom(response); + } + } + + /// + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteStartObject(); + + writer.WriteString("type", Type); + writer.WriteSerializableIfNotNull("replay_id", ReplayId, logger); + + writer.WriteEndObject(); + } + + /// + /// Parses from JSON. + /// + public static Replay FromJson(JsonElement json) + { + var replayId = json.GetPropertyOrNull("replay_id")?.Pipe(SentryId.FromJson); + + return new Replay + { + ReplayId = replayId + }; + } +} diff --git a/src/Sentry/SentryContexts.cs b/src/Sentry/SentryContexts.cs index b0abb14227..74aa51b8d4 100644 --- a/src/Sentry/SentryContexts.cs +++ b/src/Sentry/SentryContexts.cs @@ -55,6 +55,11 @@ public SentryFeedback? Feedback /// public OperatingSystem OperatingSystem => _innerDictionary.GetOrCreate(OperatingSystem.Type); + /// + /// Replay interface that contains information about the Session Replay (if any) related to the event. + /// + public Replay Replay => _innerDictionary.GetOrCreate(Replay.Type); + /// /// Response interface that contains information on any HTTP response related to the event. /// @@ -172,6 +177,10 @@ public static SentryContexts FromJson(JsonElement json) { result[name] = OperatingSystem.FromJson(value); } + else if (string.Equals(type, Replay.Type, StringComparison.OrdinalIgnoreCase)) + { + result[name] = Replay.FromJson(value); + } else if (string.Equals(type, Response.Type, StringComparison.OrdinalIgnoreCase)) { result[name] = Response.FromJson(value); From 98723760893233ed0367241097e09dbff855e5d4 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 23 Apr 2025 02:01:09 +0000 Subject: [PATCH 02/28] Format code --- src/Sentry/Platforms/Android/AndroidEventProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry/Platforms/Android/AndroidEventProcessor.cs b/src/Sentry/Platforms/Android/AndroidEventProcessor.cs index dc483fa8ec..04f194e3e4 100644 --- a/src/Sentry/Platforms/Android/AndroidEventProcessor.cs +++ b/src/Sentry/Platforms/Android/AndroidEventProcessor.cs @@ -38,7 +38,7 @@ public SentryEvent Process(SentryEvent @event) // TODO: We should really do this when creating the DSC, so that it gets propagated correctly // Check to see if a Replay ID is available var activeReplayId = Sentry.JavaSdk.ScopesAdapter.Instance?.Options?.ReplayController?.ReplayId?.ToSentryId(); - if (activeReplayId is {} replayId && replayId != SentryId.Empty) + if (activeReplayId is { } replayId && replayId != SentryId.Empty) { @event.Contexts.Replay.ReplayId = replayId; } From b8d945242cc7c4e4670fba0965194d7f2d65b6f4 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 23 Apr 2025 17:59:34 +1200 Subject: [PATCH 03/28] Set replay_id on DSC rather than directly on the event Context --- src/Sentry/DynamicSamplingContext.cs | 17 ++++++++++++++--- src/Sentry/Internal/Hub.cs | 2 ++ src/Sentry/Internal/ReplayHelper.cs | 19 +++++++++++++++++++ .../Android/AndroidEventProcessor.cs | 8 -------- ...ionTests.Versioning.DotNet8_0.verified.txt | 1 + ...ionTests.Versioning.DotNet9_0.verified.txt | 1 + ...piApprovalTests.Run.DotNet8_0.verified.txt | 12 ++++++++++++ ...piApprovalTests.Run.DotNet9_0.verified.txt | 12 ++++++++++++ .../ApiApprovalTests.Run.Net4_8.verified.txt | 12 ++++++++++++ ...ctionEndedAsCrashed.DotNet8_0.verified.txt | 1 + ...ctionEndedAsCrashed.DotNet9_0.verified.txt | 1 + ...nsactionProcessorTests.Simple.verified.txt | 1 + 12 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 src/Sentry/Internal/ReplayHelper.cs diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs index e85ec88b84..e18fda1df1 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -28,7 +28,9 @@ private DynamicSamplingContext( double? sampleRand = null, string? release = null, string? environment = null, - string? transactionName = null) + string? transactionName = null, + SentryId? replayId = null + ) { // Validate and set required values if (traceId == SentryId.Empty) @@ -88,6 +90,11 @@ private DynamicSamplingContext( items.Add("transaction", transactionName); } + if (replayId is not null && replayId.Value != SentryId.Empty) + { + items.Add("replay_id", replayId.Value.ToString()); + } + Items = items; } @@ -156,6 +163,7 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra var sampleRate = transaction.SampleRate!.Value; var sampleRand = transaction.SampleRand; var transactionName = transaction.NameSource.IsHighQuality() ? transaction.Name : null; + var replayId = transaction.Contexts.Replay.ReplayId; // These two may not have been set yet on the transaction, but we can get them directly. var release = options.SettingLocator.GetRelease(); @@ -169,7 +177,8 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra sampleRand, release, environment, - transactionName); + transactionName, + replayId); } public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options) @@ -178,13 +187,15 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat var publicKey = options.ParsedDsn.PublicKey; var release = options.SettingLocator.GetRelease(); var environment = options.SettingLocator.GetEnvironment(); + var replayId = ReplayHelper.GetReplayId(); return new DynamicSamplingContext( traceId, publicKey, null, release: release, - environment: environment); + environment: environment, + replayId: replayId); } } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index e7234b6356..373ee54525 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -134,6 +134,8 @@ internal ITransactionTracer StartTransaction( : SampleRandHelper.GenerateSampleRand(context.TraceId.ToString()) }; + transaction.Contexts.Replay.ReplayId = ReplayHelper.GetReplayId(); + // If the hub is disabled, we will always sample out. In other words, starting a transaction // after disposing the hub will result in that transaction not being sent to Sentry. // Additionally, we will always sample out if tracing is explicitly disabled. diff --git a/src/Sentry/Internal/ReplayHelper.cs b/src/Sentry/Internal/ReplayHelper.cs new file mode 100644 index 0000000000..edb317071a --- /dev/null +++ b/src/Sentry/Internal/ReplayHelper.cs @@ -0,0 +1,19 @@ +#if __ANDROID__ +using Sentry.Android.Extensions; +#endif + +namespace Sentry.Internal; + +internal static class ReplayHelper +{ + internal static SentryId? GetReplayId() + { +#if __ANDROID__ + // Check to see if a Replay ID is available + var replayId = JavaSdk.ScopesAdapter.Instance?.Options?.ReplayController?.ReplayId?.ToSentryId(); + return (replayId is { } id && id != SentryId.Empty) ? id : null; +#else + return null; +#endif + } +} diff --git a/src/Sentry/Platforms/Android/AndroidEventProcessor.cs b/src/Sentry/Platforms/Android/AndroidEventProcessor.cs index 04f194e3e4..9850b7fe35 100644 --- a/src/Sentry/Platforms/Android/AndroidEventProcessor.cs +++ b/src/Sentry/Platforms/Android/AndroidEventProcessor.cs @@ -35,14 +35,6 @@ public SentryEvent Process(SentryEvent @event) // Copy more information from the Android SDK if (_androidProcessor is { } androidProcessor) { - // TODO: We should really do this when creating the DSC, so that it gets propagated correctly - // Check to see if a Replay ID is available - var activeReplayId = Sentry.JavaSdk.ScopesAdapter.Instance?.Options?.ReplayController?.ReplayId?.ToSentryId(); - if (activeReplayId is { } replayId && replayId != SentryId.Empty) - { - @event.Contexts.Replay.ReplayId = replayId; - } - // TODO: Can we gather more data directly and remove this? // Run a fake event through the Android processor, so we can get context info from the Android SDK. diff --git a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet8_0.verified.txt b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet8_0.verified.txt index 019cc1fb2d..0e7e2ed474 100644 --- a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet8_0.verified.txt +++ b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet8_0.verified.txt @@ -17,6 +17,7 @@ QueryString: }, Contexts: { + replay: {}, trace: { Operation: http.server, Origin: auto.http.aspnetcore, diff --git a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet9_0.verified.txt b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet9_0.verified.txt index 019cc1fb2d..0e7e2ed474 100644 --- a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet9_0.verified.txt +++ b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet9_0.verified.txt @@ -17,6 +17,7 @@ QueryString: }, Contexts: { + replay: {}, trace: { Operation: http.server, Origin: auto.http.aspnetcore, diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 93d72d716c..8b26269a3c 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -504,6 +504,7 @@ namespace Sentry public object this[string key] { get; set; } public System.Collections.Generic.ICollection Keys { get; } public Sentry.Protocol.OperatingSystem OperatingSystem { get; } + public Sentry.Protocol.Replay Replay { get; } public Sentry.Protocol.Response Response { get; } public Sentry.Protocol.Runtime Runtime { get; } public Sentry.Protocol.Trace Trace { get; } @@ -1754,6 +1755,17 @@ namespace Sentry.Protocol public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } public static Sentry.Protocol.OperatingSystem FromJson(System.Text.Json.JsonElement json) { } } + public sealed class Replay : Sentry.ISentryJsonSerializable + { + public const string Type = "replay"; + public Replay() { } + public Sentry.SentryId? ReplayId { get; set; } + public Sentry.Protocol.Replay Clone() { } + public void UpdateFrom(Sentry.Protocol.Replay source) { } + public void UpdateFrom(object source) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.Replay FromJson(System.Text.Json.JsonElement json) { } + } public sealed class Response : Sentry.ISentryJsonSerializable { public const string Type = "response"; diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 93d72d716c..8b26269a3c 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -504,6 +504,7 @@ namespace Sentry public object this[string key] { get; set; } public System.Collections.Generic.ICollection Keys { get; } public Sentry.Protocol.OperatingSystem OperatingSystem { get; } + public Sentry.Protocol.Replay Replay { get; } public Sentry.Protocol.Response Response { get; } public Sentry.Protocol.Runtime Runtime { get; } public Sentry.Protocol.Trace Trace { get; } @@ -1754,6 +1755,17 @@ namespace Sentry.Protocol public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } public static Sentry.Protocol.OperatingSystem FromJson(System.Text.Json.JsonElement json) { } } + public sealed class Replay : Sentry.ISentryJsonSerializable + { + public const string Type = "replay"; + public Replay() { } + public Sentry.SentryId? ReplayId { get; set; } + public Sentry.Protocol.Replay Clone() { } + public void UpdateFrom(Sentry.Protocol.Replay source) { } + public void UpdateFrom(object source) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.Replay FromJson(System.Text.Json.JsonElement json) { } + } public sealed class Response : Sentry.ISentryJsonSerializable { public const string Type = "response"; diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 75fa5ad89d..17f2eceff1 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -492,6 +492,7 @@ namespace Sentry public object this[string key] { get; set; } public System.Collections.Generic.ICollection Keys { get; } public Sentry.Protocol.OperatingSystem OperatingSystem { get; } + public Sentry.Protocol.Replay Replay { get; } public Sentry.Protocol.Response Response { get; } public Sentry.Protocol.Runtime Runtime { get; } public Sentry.Protocol.Trace Trace { get; } @@ -1736,6 +1737,17 @@ namespace Sentry.Protocol public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } public static Sentry.Protocol.OperatingSystem FromJson(System.Text.Json.JsonElement json) { } } + public sealed class Replay : Sentry.ISentryJsonSerializable + { + public const string Type = "replay"; + public Replay() { } + public Sentry.SentryId? ReplayId { get; set; } + public Sentry.Protocol.Replay Clone() { } + public void UpdateFrom(Sentry.Protocol.Replay source) { } + public void UpdateFrom(object source) { } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.Replay FromJson(System.Text.Json.JsonElement json) { } + } public sealed class Response : Sentry.ISentryJsonSerializable { public const string Type = "response"; diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt index c510d50d43..b8aeb9bc7a 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt @@ -170,6 +170,7 @@ SampleRate: 1.0, Request: {}, Contexts: { + replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt index c510d50d43..b8aeb9bc7a 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt @@ -170,6 +170,7 @@ SampleRate: 1.0, Request: {}, Contexts: { + replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt b/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt index 0e9cc2a73b..10dbc6deba 100644 --- a/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt +++ b/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt @@ -78,6 +78,7 @@ Request: {}, Contexts: { key: value, + replay: {}, trace: { Operation: my operation, Description: , From ae6157e643e19cf27a2c7d0ecbd4deda0c6092eb Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 23 Apr 2025 18:00:34 +1200 Subject: [PATCH 04/28] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 198f555c62..a95c571b3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Option to disable the SentryNative integration ([#4107](https://github.com/getsentry/sentry-dotnet/pull/4107)) - Reintroduced experimental support for Session Replay on Android ([#4097](https://github.com/getsentry/sentry-dotnet/pull/4097)) +- Associate replays with errors and traces on Android ([#4133](https://github.com/getsentry/sentry-dotnet/pull/4133)) ### Fixes From 6f848a3de8504f377009a6d99fea8f12f9255374 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 28 Apr 2025 11:52:32 +1200 Subject: [PATCH 05/28] Verify NetFX --- .../SqlListenerTests.LoggingAsync.DotNet8_0.verified.txt | 1 + .../SqlListenerTests.LoggingAsync.DotNet9_0.verified.txt | 1 + .../SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt | 1 + .../SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt | 1 + .../SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt | 1 + .../SqlListenerTests.RecordsSqlAsync.verified.txt | 1 + .../IntegrationTests.Simple.verified.txt | 1 + ...handledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt | 1 + 8 files changed, 8 insertions(+) diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet8_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet8_0.verified.txt index 348b185b6f..fe2cd0f50b 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet8_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet8_0.verified.txt @@ -10,6 +10,7 @@ SampleRate: 1.0, Request: {}, Contexts: { + replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet9_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet9_0.verified.txt index 348b185b6f..fe2cd0f50b 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet9_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet9_0.verified.txt @@ -10,6 +10,7 @@ SampleRate: 1.0, Request: {}, Contexts: { + replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt index 05803effbe..22b9a3b1d1 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt @@ -45,6 +45,7 @@ SampleRate: 1.0, Request: {}, Contexts: { + replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt index 05803effbe..22b9a3b1d1 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt @@ -45,6 +45,7 @@ SampleRate: 1.0, Request: {}, Contexts: { + replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt index 75d6b3e321..53cb9a722d 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt @@ -45,6 +45,7 @@ SampleRate: 1.0, Request: {}, Contexts: { + replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt index e317514b27..f86bcfdbe2 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt @@ -45,6 +45,7 @@ SampleRate: 1.0, Request: {}, Contexts: { + replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt b/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt index 826fb58e95..7fdd0e0858 100644 --- a/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt +++ b/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt @@ -46,6 +46,7 @@ SampleRate: 1.0, Request: {}, Contexts: { + replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt index f41bcaa626..5d6c13b6b5 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt @@ -170,6 +170,7 @@ SampleRate: 1.0, Request: {}, Contexts: { + replay: {}, trace: { Operation: my operation, Description: , From cf534b9b3b9fc4840141b9256626a8f450eef1e0 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 28 Apr 2025 13:54:25 +1200 Subject: [PATCH 06/28] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc35a3f3f2..51f5465af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Custom SessionReplay masks in MAUI Android apps ([#4121](https://github.com/getsentry/sentry-dotnet/pull/4121)) +- Associate replays with errors and traces on Android ([#4133](https://github.com/getsentry/sentry-dotnet/pull/4133)) ### Fixes @@ -17,7 +18,6 @@ - Option to disable the SentryNative integration ([#4107](https://github.com/getsentry/sentry-dotnet/pull/4107), [#4134](https://github.com/getsentry/sentry-dotnet/pull/4134)) - To disable it, add this msbuild property: `false` - Reintroduced experimental support for Session Replay on Android ([#4097](https://github.com/getsentry/sentry-dotnet/pull/4097)) -- Associate replays with errors and traces on Android ([#4133](https://github.com/getsentry/sentry-dotnet/pull/4133)) ### Fixes From ff8019bbe8fe71ec404797e243beadc00275b5a6 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 28 Apr 2025 14:53:09 +1200 Subject: [PATCH 07/28] DSC tests --- src/Sentry/Internal/ReplayHelper.cs | 16 ++++++++++++++++ test/Sentry.Tests/DynamicSamplingContextTests.cs | 8 +++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Sentry/Internal/ReplayHelper.cs b/src/Sentry/Internal/ReplayHelper.cs index edb317071a..11bd90c2aa 100644 --- a/src/Sentry/Internal/ReplayHelper.cs +++ b/src/Sentry/Internal/ReplayHelper.cs @@ -6,7 +6,23 @@ namespace Sentry.Internal; internal static class ReplayHelper { + /// + /// Function that resolves the replay ID - for use in tests only. + /// + internal static Func? TestReplayIdResolver; + internal static Lazy TestReplayId = new(() => SentryId.Create()); + internal static SentryId? GetReplayId() + { + if (TestReplayIdResolver is {} resolver) + { + // This is a test, so we need to return a test ID + return resolver(); + } + return ConcreteReplayIdResolver(); + } + + private static SentryId? ConcreteReplayIdResolver() { #if __ANDROID__ // Check to see if a Replay ID is available diff --git a/test/Sentry.Tests/DynamicSamplingContextTests.cs b/test/Sentry.Tests/DynamicSamplingContextTests.cs index 374be0c371..8a0412e3aa 100644 --- a/test/Sentry.Tests/DynamicSamplingContextTests.cs +++ b/test/Sentry.Tests/DynamicSamplingContextTests.cs @@ -361,11 +361,13 @@ public void CreateFromTransaction(bool? isSampled) { }, }; + var replayId = SentryId.Create(); + transaction.Contexts.Replay.ReplayId = replayId; var dsc = transaction.CreateDynamicSamplingContext(options); Assert.NotNull(dsc); - Assert.Equal(isSampled.HasValue ? 8 : 7, dsc.Items.Count); + Assert.Equal(isSampled.HasValue ? 9 : 8, dsc.Items.Count); Assert.Equal(traceId.ToString(), Assert.Contains("trace_id", dsc.Items)); Assert.Equal("d4d82fc1c2c4032a83f3a29aa3a3aff", Assert.Contains("public_key", dsc.Items)); if (transaction.IsSampled is { } sampled) @@ -381,11 +383,14 @@ public void CreateFromTransaction(bool? isSampled) Assert.Equal("foo@2.4.5", Assert.Contains("release", dsc.Items)); Assert.Equal("staging", Assert.Contains("environment", dsc.Items)); Assert.Equal("GET /person/{id}", Assert.Contains("transaction", dsc.Items)); + Assert.Equal(replayId.ToString(), Assert.Contains("replay_id", dsc.Items)); } [Fact] public void CreateFromPropagationContext_Valid_Complete() { + var replayId = ReplayHelper.TestReplayId.Value; + ReplayHelper.TestReplayIdResolver = () => replayId; var options = new SentryOptions { Dsn = "https://a@sentry.io/1", Release = "test-release", Environment = "test-environment" }; var propagationContext = new SentryPropagationContext( SentryId.Parse("43365712692146d08ee11a729dfbcaca"), SpanId.Parse("1234")); @@ -397,5 +402,6 @@ public void CreateFromPropagationContext_Valid_Complete() Assert.Equal("a", Assert.Contains("public_key", dsc.Items)); Assert.Equal("test-release", Assert.Contains("release", dsc.Items)); Assert.Equal("test-environment", Assert.Contains("environment", dsc.Items)); + Assert.Equal(replayId.ToString(), Assert.Contains("replay_id", dsc.Items)); } } From 0989ecaa9f3691f8b25135db0c3716666e6cc517 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 28 Apr 2025 15:23:08 +1200 Subject: [PATCH 08/28] Hub Tests --- src/Sentry/Internal/Hub.cs | 4 ++- test/Sentry.Tests/HubTests.cs | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 373ee54525..5485c8224f 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -134,7 +134,9 @@ internal ITransactionTracer StartTransaction( : SampleRandHelper.GenerateSampleRand(context.TraceId.ToString()) }; - transaction.Contexts.Replay.ReplayId = ReplayHelper.GetReplayId(); + transaction.Contexts.Replay.ReplayId = dynamicSamplingContext?.Items.TryGetValue("replay_id", out var replayId) == true + ? SentryId.Parse(replayId) + : ReplayHelper.GetReplayId(); // If the hub is disabled, we will always sample out. In other words, starting a transaction // after disposing the hub will result in that transaction not being sent to Sentry. diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index f0c141617e..396eea4cca 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -680,6 +680,56 @@ public void StartTransaction_SameInstrumenter_SampledIn() transaction.IsSampled.Should().BeTrue(); } + [Fact] + public void StartTransaction_DynamicSamplingContextWithReplayId_InheritsReplayId() + { + // Arrange + var transactionContext = new TransactionContext("name", "operation"); + var customContext = new Dictionary(); + var dsc = BaggageHeader.Create(new List> + { + {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, + {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, + {"sentry-sampled", "true"}, + {"sentry-sample_rate", "0.5"}, // Required in the baggage header, but ignored by sampling logic + {"sentry-replay_id","bfd31b89a59d41c99d96dc2baf840ecd"} + }).CreateDynamicSamplingContext(); + + _fixture.Options.TracesSampleRate = 1.0; + var hub = _fixture.GetSut(); + + // Act + var transaction = hub.StartTransaction(transactionContext, customContext, dsc); + + // Assert + var transactionTracer = ((TransactionTracer)transaction); + transactionTracer.IsSampled.Should().Be(true); + transactionTracer.DynamicSamplingContext.Should().Be(dsc); + transactionTracer.Contexts.Replay.ReplayId.Should().Be(SentryId.Parse("bfd31b89a59d41c99d96dc2baf840ecd")); + } + + [Fact] + public void StartTransaction_NoDynamicSamplingContext_GeneratesReplayId() + { + // Arrange + var replayId = ReplayHelper.TestReplayId.Value; + ReplayHelper.TestReplayIdResolver = () => replayId; + var transactionContext = new TransactionContext("name", "operation"); + var customContext = new Dictionary(); + + var hub = _fixture.GetSut(); + + // Act + var transaction = hub.StartTransaction(transactionContext, customContext); + + // Assert + var transactionTracer = ((TransactionTracer)transaction); + transactionTracer.SampleRand.Should().NotBeNull(); + transactionTracer.DynamicSamplingContext.Should().NotBeNull(); + transactionTracer.DynamicSamplingContext!.Items.Should().ContainKey("replay_id"); + transactionTracer.DynamicSamplingContext.Items["replay_id"].Should().Be(replayId!.Value.ToString()); + } + [Fact] public void StartTransaction_NoDynamicSamplingContext_GeneratesSampleRand() { From b43f1bfc3fb790b12b8771961cc5affa8931b880 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 28 Apr 2025 15:24:01 +1200 Subject: [PATCH 09/28] Update AndroidEventProcessor.cs --- src/Sentry/Platforms/Android/AndroidEventProcessor.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Sentry/Platforms/Android/AndroidEventProcessor.cs b/src/Sentry/Platforms/Android/AndroidEventProcessor.cs index 9850b7fe35..cba15fd78e 100644 --- a/src/Sentry/Platforms/Android/AndroidEventProcessor.cs +++ b/src/Sentry/Platforms/Android/AndroidEventProcessor.cs @@ -1,5 +1,3 @@ -using Java.Lang; -using Kotlin; using Sentry.Android.Extensions; using Sentry.Extensibility; using Sentry.JavaSdk.Android.Core; From f6a8242f6a59033cb19b6ac6686310aee77e41fa Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 28 Apr 2025 15:37:24 +1200 Subject: [PATCH 10/28] Protocol tests --- .../Protocol/Context/ReplayTests.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 test/Sentry.Tests/Protocol/Context/ReplayTests.cs diff --git a/test/Sentry.Tests/Protocol/Context/ReplayTests.cs b/test/Sentry.Tests/Protocol/Context/ReplayTests.cs new file mode 100644 index 0000000000..c0de75983c --- /dev/null +++ b/test/Sentry.Tests/Protocol/Context/ReplayTests.cs @@ -0,0 +1,63 @@ +namespace Sentry.Tests.Protocol.Context; + +public class ReplayTests +{ + private readonly IDiagnosticLogger _testOutputLogger; + + public ReplayTests(ITestOutputHelper output) + { + _testOutputLogger = new TestOutputDiagnosticLogger(output); + } + + [Fact] + public void Ctor_NoPropertyFilled_SerializesEmptyObject() + { + // Arrange + var replay = new Replay(); + + // Act + var actual = replay.ToJsonString(_testOutputLogger); + + // Assert + Assert.Equal("""{"type":"replay"}""", actual); + } + + [Fact] + public void SerializeObject_AllPropertiesSetToNonDefault_SerializesValidObject() + { + // Arrange + var replay = new Replay + { + ReplayId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8") + }; + + // Act + var actual = replay.ToJsonString(_testOutputLogger, indented: true); + + // Assert + Assert.Equal( + """ + { + "type": "replay", + "replay_id": "75302ac48a024bde9a3b3734a82e36c8" + } + """, + actual); + } + + [Fact] + public void Clone_CopyValues() + { + // Arrange + var replay = new Replay + { + ReplayId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8") + }; + + // Act + var clone = replay.Clone(); + + // Assert + Assert.Equal(replay.ReplayId, clone.ReplayId); + } +} From 6386234e02a932592a1df5757d6e1f2f3bc044c7 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 28 Apr 2025 16:19:11 +1200 Subject: [PATCH 11/28] Fix verify tests --- src/Sentry/Internal/ReplayHelper.cs | 22 +++++++++++-------- .../DynamicSamplingContextTests.cs | 4 +++- .../EventProcessorTests.Simple.verified.txt | 5 +++-- ...rocessorTests.WithTransaction.verified.txt | 5 +++-- .../EventProcessorTests.verify.cs | 3 +++ ...ctionEndedAsCrashed.DotNet8_0.verified.txt | 8 ++++--- ...ctionEndedAsCrashed.DotNet9_0.verified.txt | 8 ++++--- test/Sentry.Tests/HubTests.cs | 3 ++- test/Sentry.Tests/ReplayFixture.cs | 19 ++++++++++++++++ ...sactionProcessorTests.Discard.verified.txt | 5 +++-- ...nsactionProcessorTests.Simple.verified.txt | 12 +++++----- .../TransactionProcessorTests.verify.cs | 3 +++ 12 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 test/Sentry.Tests/ReplayFixture.cs diff --git a/src/Sentry/Internal/ReplayHelper.cs b/src/Sentry/Internal/ReplayHelper.cs index 11bd90c2aa..fb4001ad81 100644 --- a/src/Sentry/Internal/ReplayHelper.cs +++ b/src/Sentry/Internal/ReplayHelper.cs @@ -6,20 +6,24 @@ namespace Sentry.Internal; internal static class ReplayHelper { + internal static Lazy TestReplayId { get; } = new(() => SentryId.Create()); + + private static Func? TestReplayIdResolver; + /// - /// Function that resolves the replay ID - for use in tests only. + /// Initialises the test replay id resolver so that unit tests return a test id (rather than trying to resovle an + /// ID from static platform libraries). /// - internal static Func? TestReplayIdResolver; - internal static Lazy TestReplayId = new(() => SentryId.Create()); + internal static void InitTestReplayId() + { + TestReplayIdResolver = () => TestReplayId.Value; + } internal static SentryId? GetReplayId() { - if (TestReplayIdResolver is {} resolver) - { - // This is a test, so we need to return a test ID - return resolver(); - } - return ConcreteReplayIdResolver(); + return TestReplayIdResolver is { } testIdResolver + ? testIdResolver() + : ConcreteReplayIdResolver(); } private static SentryId? ConcreteReplayIdResolver() diff --git a/test/Sentry.Tests/DynamicSamplingContextTests.cs b/test/Sentry.Tests/DynamicSamplingContextTests.cs index 8a0412e3aa..62f0a1372b 100644 --- a/test/Sentry.Tests/DynamicSamplingContextTests.cs +++ b/test/Sentry.Tests/DynamicSamplingContextTests.cs @@ -1,5 +1,8 @@ +using Sentry.Tests.Internals; + namespace Sentry.Tests; +[Collection("Replay collection")] public class DynamicSamplingContextTests { [Fact] @@ -390,7 +393,6 @@ public void CreateFromTransaction(bool? isSampled) public void CreateFromPropagationContext_Valid_Complete() { var replayId = ReplayHelper.TestReplayId.Value; - ReplayHelper.TestReplayIdResolver = () => replayId; var options = new SentryOptions { Dsn = "https://a@sentry.io/1", Release = "test-release", Environment = "test-environment" }; var propagationContext = new SentryPropagationContext( SentryId.Parse("43365712692146d08ee11a729dfbcaca"), SpanId.Parse("1234")); diff --git a/test/Sentry.Tests/EventProcessorTests.Simple.verified.txt b/test/Sentry.Tests/EventProcessorTests.Simple.verified.txt index b63e99dd53..4b02ea304c 100644 --- a/test/Sentry.Tests/EventProcessorTests.Simple.verified.txt +++ b/test/Sentry.Tests/EventProcessorTests.Simple.verified.txt @@ -9,7 +9,8 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - trace_id: Guid_2 + replay_id: Guid_2, + trace_id: Guid_3 } }, Items: [ @@ -32,7 +33,7 @@ } }, User: { - Id: Guid_3 + Id: Guid_4 }, Environment: production } diff --git a/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt b/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt index a39b4497ea..c7fffc8fe3 100644 --- a/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt +++ b/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt @@ -9,10 +9,11 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + replay_id: Guid_2, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_2, + trace_id: Guid_3, transaction: my transaction } }, @@ -37,7 +38,7 @@ } }, User: { - Id: Guid_3 + Id: Guid_4 }, Environment: production } diff --git a/test/Sentry.Tests/EventProcessorTests.verify.cs b/test/Sentry.Tests/EventProcessorTests.verify.cs index ea324e8283..3ff7217b3a 100644 --- a/test/Sentry.Tests/EventProcessorTests.verify.cs +++ b/test/Sentry.Tests/EventProcessorTests.verify.cs @@ -1,5 +1,8 @@ +using Sentry.Tests.Internals; + namespace Sentry.Tests; +[Collection("Replay collection")] public class EventProcessorTests { private readonly TestOutputDiagnosticLogger _logger; diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt index b8aeb9bc7a..5c08ab79ff 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt @@ -31,10 +31,11 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_3, + trace_id: Guid_4, transaction: my transaction } }, @@ -139,7 +140,7 @@ }, { Header: { - event_id: Guid_4, + event_id: Guid_5, sdk: { name: sentry.dotnet }, @@ -147,10 +148,11 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_3, + trace_id: Guid_4, transaction: my transaction } }, diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt index b8aeb9bc7a..5c08ab79ff 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt @@ -31,10 +31,11 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_3, + trace_id: Guid_4, transaction: my transaction } }, @@ -139,7 +140,7 @@ }, { Header: { - event_id: Guid_4, + event_id: Guid_5, sdk: { name: sentry.dotnet }, @@ -147,10 +148,11 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_3, + trace_id: Guid_4, transaction: my transaction } }, diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index 396eea4cca..995f775f69 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1,8 +1,10 @@ using System.IO.Abstractions.TestingHelpers; using Sentry.Internal.Http; +using Sentry.Tests.Internals; namespace Sentry.Tests; +[Collection("Replay collection")] public partial class HubTests { private readonly ITestOutputHelper _output; @@ -713,7 +715,6 @@ public void StartTransaction_NoDynamicSamplingContext_GeneratesReplayId() { // Arrange var replayId = ReplayHelper.TestReplayId.Value; - ReplayHelper.TestReplayIdResolver = () => replayId; var transactionContext = new TransactionContext("name", "operation"); var customContext = new Dictionary(); diff --git a/test/Sentry.Tests/ReplayFixture.cs b/test/Sentry.Tests/ReplayFixture.cs new file mode 100644 index 0000000000..7994b2dbc7 --- /dev/null +++ b/test/Sentry.Tests/ReplayFixture.cs @@ -0,0 +1,19 @@ +namespace Sentry.Tests; + +public class ReplayFixture +{ + public ReplayFixture() + { + ReplayHelper.InitTestReplayId(); + } +} + +/// +/// This class has no code, and is never created. Its purpose is simply to be the place to apply [CollectionDefinition] +/// and the interface. +/// +[CollectionDefinition("Replay collection")] +public class ReplayCollection : ICollectionFixture +{ + // TODO: When we upgrade to Xcode 3, it would be cleaner to use an AssemblyFixture +} diff --git a/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt b/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt index 1c801c9b72..d60ed49633 100644 --- a/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt +++ b/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt @@ -9,10 +9,11 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + replay_id: Guid_2, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_2, + trace_id: Guid_3, transaction: my transaction } }, @@ -36,7 +37,7 @@ } }, User: { - Id: Guid_3 + Id: Guid_4 }, Environment: production } diff --git a/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt b/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt index 10dbc6deba..4214e4298f 100644 --- a/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt +++ b/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt @@ -9,10 +9,11 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + replay_id: Guid_2, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_2, + trace_id: Guid_3, transaction: my transaction } }, @@ -36,7 +37,7 @@ } }, User: { - Id: Guid_3 + Id: Guid_4 }, Environment: production } @@ -46,7 +47,7 @@ }, { Header: { - event_id: Guid_4, + event_id: Guid_5, sdk: { name: sentry.dotnet }, @@ -54,10 +55,11 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + replay_id: Guid_2, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_2, + trace_id: Guid_3, transaction: my transaction } }, @@ -87,7 +89,7 @@ } }, User: { - Id: Guid_3 + Id: Guid_4 }, Environment: production, IsFinished: true diff --git a/test/Sentry.Tests/TransactionProcessorTests.verify.cs b/test/Sentry.Tests/TransactionProcessorTests.verify.cs index 7d6425405b..8edf51e9de 100644 --- a/test/Sentry.Tests/TransactionProcessorTests.verify.cs +++ b/test/Sentry.Tests/TransactionProcessorTests.verify.cs @@ -1,5 +1,8 @@ +using Sentry.Tests.Internals; + namespace Sentry.Tests; +[Collection("Replay collection")] public partial class TransactionProcessorTests { [Fact] From 41ba57d9fa6e5f8ee5842ef8806ba68c942121b3 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 28 Apr 2025 16:55:19 +1200 Subject: [PATCH 12/28] Update DynamicSamplingContextTests.cs --- test/Sentry.Tests/DynamicSamplingContextTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/Sentry.Tests/DynamicSamplingContextTests.cs b/test/Sentry.Tests/DynamicSamplingContextTests.cs index 62f0a1372b..902f52b8c7 100644 --- a/test/Sentry.Tests/DynamicSamplingContextTests.cs +++ b/test/Sentry.Tests/DynamicSamplingContextTests.cs @@ -293,7 +293,8 @@ public void CreateFromBaggage_Valid_Complete() {"sentry-release", "test@1.0.0+abc"}, {"sentry-environment", "production"}, {"sentry-user_segment", "Group B"}, - {"sentry-transaction", "GET /person/{id}"} + {"sentry-transaction", "GET /person/{id}"}, + {"sentry-replay_id","bfd31b89a59d41c99d96dc2baf840ecd"} }); var dsc = baggage.CreateDynamicSamplingContext(); @@ -309,6 +310,7 @@ public void CreateFromBaggage_Valid_Complete() Assert.Equal("production", Assert.Contains("environment", dsc.Items)); Assert.Equal("Group B", Assert.Contains("user_segment", dsc.Items)); Assert.Equal("GET /person/{id}", Assert.Contains("transaction", dsc.Items)); + Assert.Equal("bfd31b89a59d41c99d96dc2baf840ecd", Assert.Contains("replay_id", dsc.Items)); } [Fact] From 75997c645eea5914d913d537727855d450adda89 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 28 Apr 2025 17:08:35 +1200 Subject: [PATCH 13/28] Windows verify tests --- ...ExceptionTransactionEndedAsCrashed.Net4_8.verified.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt index 5d6c13b6b5..4516665826 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt @@ -31,10 +31,11 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_3, + trace_id: Guid_4, transaction: my transaction } }, @@ -139,7 +140,7 @@ }, { Header: { - event_id: Guid_4, + event_id: Guid_5, sdk: { name: sentry.dotnet }, @@ -147,10 +148,11 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_3, + trace_id: Guid_4, transaction: my transaction } }, From 32ba50bcf2017f4dafdfc94f45917a73a611ab47 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 28 Apr 2025 05:20:50 +0000 Subject: [PATCH 14/28] Format code --- src/Sentry/Internal/ReplayHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry/Internal/ReplayHelper.cs b/src/Sentry/Internal/ReplayHelper.cs index fb4001ad81..3b31d236a9 100644 --- a/src/Sentry/Internal/ReplayHelper.cs +++ b/src/Sentry/Internal/ReplayHelper.cs @@ -6,7 +6,7 @@ namespace Sentry.Internal; internal static class ReplayHelper { - internal static Lazy TestReplayId { get; } = new(() => SentryId.Create()); + internal static Lazy TestReplayId { get; } = new(() => SentryId.Create()); private static Func? TestReplayIdResolver; From 6d770cbc106928a19cfd2367c47f0396da33f973 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 29 Apr 2025 22:35:18 +1200 Subject: [PATCH 15/28] Ensure any replay_id in the PropagationContext is used when creating new transactions --- src/Sentry/SentryPropagationContext.cs | 8 ++++++ src/Sentry/TransactionTracer.cs | 2 ++ .../SentrySpanProcessorTests.cs | 1 - .../Protocol/SentryTransactionTests.cs | 27 +++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Sentry/SentryPropagationContext.cs b/src/Sentry/SentryPropagationContext.cs index 19e9bd30d3..3a697ffb60 100644 --- a/src/Sentry/SentryPropagationContext.cs +++ b/src/Sentry/SentryPropagationContext.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal; namespace Sentry; @@ -10,6 +11,13 @@ internal class SentryPropagationContext internal DynamicSamplingContext? _dynamicSamplingContext; + internal SentryId? GetReplayId() + { + return _dynamicSamplingContext?.Items.TryGetValue("replay_id", out var value) == true + ? SentryId.Parse(value) + : ReplayHelper.GetReplayId(); + } + public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options) { if (_dynamicSamplingContext is null) diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index c4da3d5933..ebaec3103f 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -252,6 +252,8 @@ internal TransactionTracer(IHub hub, ITransactionContext context, TimeSpan? idle _instrumenter = transactionContext.Instrumenter; Origin = transactionContext.Origin; } + // If there is a ReplayId on the PropagationContext, set this in the Contexts + hub.ConfigureScope(s => Contexts.Replay.ReplayId = s.PropagationContext.GetReplayId()); // Set idle timer only if an idle timeout has been provided directly if (idleTimeout.HasValue) diff --git a/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs b/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs index 1b199c9fc4..f7d74b1b88 100644 --- a/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs @@ -287,7 +287,6 @@ public void OnStart_WithoutParentSpanId_StartsNewTransaction() transaction.Description.Should().Be(data.DisplayName); transaction.Status.Should().BeNull(); transaction.StartTimestamp.Should().Be(data.StartTimeUtc); - _fixture.ScopeManager.Received(1).ConfigureScope(Arg.Any>()); } } diff --git a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs index 4e0872ada1..41e711def6 100644 --- a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs +++ b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs @@ -55,6 +55,33 @@ public async Task NewTransactionTracer_IdleTimeoutProvided_AutomaticallyFinishes transaction.IsFinished.Should().BeTrue(); } + [Fact] + public void NewTransactionTracer_PropagationContextHasReplayId_SetsReplayContext() + { + // Arrange + var hub = Substitute.For(); + var traceHeader = new SentryTraceHeader(SentryId.Create(), SpanId.Create(), null); + var baggageHeader = BaggageHeader.Create(new List> + { + { "sentry-sample_rate", "1.0" }, + { "sentry-sample_rand", "0.1234" }, + { "sentry-trace_id", "75302ac48a024bde9a3b3734a82e36c8" }, + { "sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff" }, + { "sentry-replay_id", "bfd31b89a59d41c99d96dc2baf840ecd" } + }); + var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, baggageHeader); + var scope = new Scope(hub.GetSentryOptions(), propagationContext); + hub.ConfigureScope(Arg.Do>(callback => callback(scope))); + var transactionContext = new TransactionContext("test-name", "test-operation"); + + // Act + var actualTransaction = new TransactionTracer(hub, transactionContext); + + // Assert + actualTransaction.Contexts.Replay.ReplayId.Should().Be(SentryId.Parse("bfd31b89a59d41c99d96dc2baf840ecd")); + Assert.NotEqual(DateTimeOffset.MinValue, actualTransaction.StartTimestamp); + } + [Fact] public void Redact_Redacts_Urls() { From 37b40c56c38ecedaf34a8039aeec6be5e401422e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 1 May 2025 22:44:34 +1200 Subject: [PATCH 16/28] Prefer our session replay id over the propagated ones --- src/Sentry/DynamicSamplingContext.cs | 25 +++++++++++-------- src/Sentry/Internal/Hub.cs | 4 +-- .../{ReplayHelper.cs => ReplaySession.cs} | 11 +++----- src/Sentry/SentryPropagationContext.cs | 2 +- src/Sentry/SentryTransaction.cs | 6 +++++ .../DynamicSamplingContextTests.cs | 19 ++++++++------ .../EventProcessorTests.verify.cs | 2 +- test/Sentry.Tests/HubTests.cs | 20 +++++++-------- .../Protocol/SentryTransactionTests.cs | 5 ++-- test/Sentry.Tests/ReplayFixture.cs | 3 ++- .../TransactionProcessorTests.verify.cs | 2 +- 11 files changed, 53 insertions(+), 46 deletions(-) rename src/Sentry/Internal/{ReplayHelper.cs => ReplaySession.cs} (75%) diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs index e18fda1df1..c491beca4f 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -28,8 +28,7 @@ private DynamicSamplingContext( double? sampleRand = null, string? release = null, string? environment = null, - string? transactionName = null, - SentryId? replayId = null + string? transactionName = null ) { // Validate and set required values @@ -53,7 +52,7 @@ private DynamicSamplingContext( throw new ArgumentOutOfRangeException(nameof(sampleRand), "Arg invalid if < 0.0 or >= 1.0"); } - var items = new Dictionary(capacity: 8) + var items = new Dictionary(capacity: 9) { ["trace_id"] = traceId.ToString(), ["public_key"] = publicKey, @@ -90,9 +89,9 @@ private DynamicSamplingContext( items.Add("transaction", transactionName); } - if (replayId is not null && replayId.Value != SentryId.Empty) + if (ReplaySession.GetReplayId() is { } replayId && replayId != SentryId.Empty) { - items.Add("replay_id", replayId.Value.ToString()); + items.Add("replay_id", replayId.ToString()); } Items = items; @@ -151,6 +150,14 @@ private DynamicSamplingContext( } items.Add("sample_rand", rand.ToString("N4", CultureInfo.InvariantCulture)); } + + if (ReplaySession.GetReplayId() is { } replayId) + { + // Overwrite any existing value - the DSC is simply used as a transport mechanism so that SDKs can + // communicate the replayId to Sentry Relay (SDKs don't need to propagate the replayId to each other). + items["replay_id"] = replayId.ToString(); + } + return new DynamicSamplingContext(items); } @@ -163,7 +170,6 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra var sampleRate = transaction.SampleRate!.Value; var sampleRand = transaction.SampleRand; var transactionName = transaction.NameSource.IsHighQuality() ? transaction.Name : null; - var replayId = transaction.Contexts.Replay.ReplayId; // These two may not have been set yet on the transaction, but we can get them directly. var release = options.SettingLocator.GetRelease(); @@ -177,8 +183,7 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra sampleRand, release, environment, - transactionName, - replayId); + transactionName); } public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options) @@ -187,15 +192,13 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat var publicKey = options.ParsedDsn.PublicKey; var release = options.SettingLocator.GetRelease(); var environment = options.SettingLocator.GetEnvironment(); - var replayId = ReplayHelper.GetReplayId(); return new DynamicSamplingContext( traceId, publicKey, null, release: release, - environment: environment, - replayId: replayId); + environment: environment); } } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 5485c8224f..8b9455770a 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -134,9 +134,7 @@ internal ITransactionTracer StartTransaction( : SampleRandHelper.GenerateSampleRand(context.TraceId.ToString()) }; - transaction.Contexts.Replay.ReplayId = dynamicSamplingContext?.Items.TryGetValue("replay_id", out var replayId) == true - ? SentryId.Parse(replayId) - : ReplayHelper.GetReplayId(); + transaction.Contexts.Replay.ReplayId = ReplaySession.GetReplayId(); // If the hub is disabled, we will always sample out. In other words, starting a transaction // after disposing the hub will result in that transaction not being sent to Sentry. diff --git a/src/Sentry/Internal/ReplayHelper.cs b/src/Sentry/Internal/ReplaySession.cs similarity index 75% rename from src/Sentry/Internal/ReplayHelper.cs rename to src/Sentry/Internal/ReplaySession.cs index 3b31d236a9..708a5b3015 100644 --- a/src/Sentry/Internal/ReplayHelper.cs +++ b/src/Sentry/Internal/ReplaySession.cs @@ -4,7 +4,7 @@ namespace Sentry.Internal; -internal static class ReplayHelper +internal static class ReplaySession { internal static Lazy TestReplayId { get; } = new(() => SentryId.Create()); @@ -19,14 +19,9 @@ internal static void InitTestReplayId() TestReplayIdResolver = () => TestReplayId.Value; } - internal static SentryId? GetReplayId() - { - return TestReplayIdResolver is { } testIdResolver - ? testIdResolver() - : ConcreteReplayIdResolver(); - } + internal static SentryId? GetReplayId() => (TestReplayIdResolver ?? ReplayIdResolver)(); - private static SentryId? ConcreteReplayIdResolver() + private static SentryId? ReplayIdResolver() { #if __ANDROID__ // Check to see if a Replay ID is available diff --git a/src/Sentry/SentryPropagationContext.cs b/src/Sentry/SentryPropagationContext.cs index 3a697ffb60..cce69ac58e 100644 --- a/src/Sentry/SentryPropagationContext.cs +++ b/src/Sentry/SentryPropagationContext.cs @@ -15,7 +15,7 @@ internal class SentryPropagationContext { return _dynamicSamplingContext?.Items.TryGetValue("replay_id", out var value) == true ? SentryId.Parse(value) - : ReplayHelper.GetReplayId(); + : ReplaySession.GetReplayId(); } public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options) diff --git a/src/Sentry/SentryTransaction.cs b/src/Sentry/SentryTransaction.cs index 386891ec01..e70ccb3a64 100644 --- a/src/Sentry/SentryTransaction.cs +++ b/src/Sentry/SentryTransaction.cs @@ -274,6 +274,12 @@ public SentryTransaction(ITransactionTracer tracer) { SampleRate = transactionTracer.SampleRate; DynamicSamplingContext = transactionTracer.DynamicSamplingContext; + if (DynamicSamplingContext?.Items.TryGetValue("replay_id", out var replayId) == true) + { + // Sentry Relay should populate this from the DSC automatically, but just for good measure + tracer.Contexts.Replay.ReplayId = SentryId.Parse(replayId); + } + TransactionProfiler = transactionTracer.TransactionProfiler; if (transactionTracer.HasMetrics) { diff --git a/test/Sentry.Tests/DynamicSamplingContextTests.cs b/test/Sentry.Tests/DynamicSamplingContextTests.cs index 902f52b8c7..c2efa189d2 100644 --- a/test/Sentry.Tests/DynamicSamplingContextTests.cs +++ b/test/Sentry.Tests/DynamicSamplingContextTests.cs @@ -2,7 +2,7 @@ namespace Sentry.Tests; -[Collection("Replay collection")] +[Collection(ReplayCollection.Name)] public class DynamicSamplingContextTests { [Fact] @@ -273,11 +273,13 @@ public void CreateFromBaggage_Valid_Minimum() var dsc = baggage.CreateDynamicSamplingContext(); Assert.NotNull(dsc); - Assert.Equal(4, dsc.Items.Count); + Assert.Equal(5, dsc.Items.Count); Assert.Equal("43365712692146d08ee11a729dfbcaca", Assert.Contains("trace_id", dsc.Items)); Assert.Equal("d4d82fc1c2c4032a83f3a29aa3a3aff", Assert.Contains("public_key", dsc.Items)); Assert.Equal("1.0", Assert.Contains("sample_rate", dsc.Items)); Assert.Contains("sample_rand", dsc.Items); + // We add the replay_id automatically when we have an active replay session + Assert.Equal(ReplaySession.TestReplayId.Value!.ToString(), Assert.Contains("replay_id", dsc.Items)); } [Fact] @@ -310,7 +312,8 @@ public void CreateFromBaggage_Valid_Complete() Assert.Equal("production", Assert.Contains("environment", dsc.Items)); Assert.Equal("Group B", Assert.Contains("user_segment", dsc.Items)); Assert.Equal("GET /person/{id}", Assert.Contains("transaction", dsc.Items)); - Assert.Equal("bfd31b89a59d41c99d96dc2baf840ecd", Assert.Contains("replay_id", dsc.Items)); + // The replay_id doesn't get propagated when we have an active replay session of our own + Assert.Equal(ReplaySession.TestReplayId.Value!.ToString(), Assert.Contains("replay_id", dsc.Items)); } [Fact] @@ -325,7 +328,8 @@ public void ToBaggageHeader() {"sentry-release", "test@1.0.0+abc"}, {"sentry-environment", "production"}, {"sentry-user_segment", "Group B"}, - {"sentry-transaction", "GET /person/{id}"} + {"sentry-transaction", "GET /person/{id}"}, + {"sentry-replay_id", ReplaySession.TestReplayId.Value!.ToString()} }); var dsc = original.CreateDynamicSamplingContext(); @@ -366,8 +370,6 @@ public void CreateFromTransaction(bool? isSampled) { }, }; - var replayId = SentryId.Create(); - transaction.Contexts.Replay.ReplayId = replayId; var dsc = transaction.CreateDynamicSamplingContext(options); @@ -388,13 +390,14 @@ public void CreateFromTransaction(bool? isSampled) Assert.Equal("foo@2.4.5", Assert.Contains("release", dsc.Items)); Assert.Equal("staging", Assert.Contains("environment", dsc.Items)); Assert.Equal("GET /person/{id}", Assert.Contains("transaction", dsc.Items)); - Assert.Equal(replayId.ToString(), Assert.Contains("replay_id", dsc.Items)); + // We add the replay_id automatically when we have an active replay session + Assert.Equal(ReplaySession.TestReplayId.Value!.ToString(), Assert.Contains("replay_id", dsc.Items)); } [Fact] public void CreateFromPropagationContext_Valid_Complete() { - var replayId = ReplayHelper.TestReplayId.Value; + var replayId = ReplaySession.TestReplayId.Value; var options = new SentryOptions { Dsn = "https://a@sentry.io/1", Release = "test-release", Environment = "test-environment" }; var propagationContext = new SentryPropagationContext( SentryId.Parse("43365712692146d08ee11a729dfbcaca"), SpanId.Parse("1234")); diff --git a/test/Sentry.Tests/EventProcessorTests.verify.cs b/test/Sentry.Tests/EventProcessorTests.verify.cs index 3ff7217b3a..57a8293363 100644 --- a/test/Sentry.Tests/EventProcessorTests.verify.cs +++ b/test/Sentry.Tests/EventProcessorTests.verify.cs @@ -2,7 +2,7 @@ namespace Sentry.Tests; -[Collection("Replay collection")] +[Collection(ReplayCollection.Name)] public class EventProcessorTests { private readonly TestOutputDiagnosticLogger _logger; diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index 995f775f69..fea0f46f42 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -4,7 +4,7 @@ namespace Sentry.Tests; -[Collection("Replay collection")] +[Collection(ReplayCollection.Name)] public partial class HubTests { private readonly ITestOutputHelper _output; @@ -683,11 +683,10 @@ public void StartTransaction_SameInstrumenter_SampledIn() } [Fact] - public void StartTransaction_DynamicSamplingContextWithReplayId_InheritsReplayId() + public void StartTransaction_DynamicSamplingContextWithReplayId_UsesActiveReplaySessionId() { // Arrange var transactionContext = new TransactionContext("name", "operation"); - var customContext = new Dictionary(); var dsc = BaggageHeader.Create(new List> { {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, @@ -701,34 +700,35 @@ public void StartTransaction_DynamicSamplingContextWithReplayId_InheritsReplayId var hub = _fixture.GetSut(); // Act - var transaction = hub.StartTransaction(transactionContext, customContext, dsc); + var transaction = hub.StartTransaction(transactionContext, new Dictionary(), dsc); // Assert var transactionTracer = ((TransactionTracer)transaction); transactionTracer.IsSampled.Should().Be(true); transactionTracer.DynamicSamplingContext.Should().Be(dsc); - transactionTracer.Contexts.Replay.ReplayId.Should().Be(SentryId.Parse("bfd31b89a59d41c99d96dc2baf840ecd")); + transactionTracer.DynamicSamplingContext.Should().NotBeNull(); + transactionTracer.DynamicSamplingContext!.Items.Should().ContainKey("replay_id"); + // The replay_id doesn't get propagated when we have an active replay session of our own + transactionTracer.DynamicSamplingContext.Items["replay_id"].Should().Be(ReplaySession.TestReplayId.Value!.ToString()); } [Fact] - public void StartTransaction_NoDynamicSamplingContext_GeneratesReplayId() + public void StartTransaction_NoDynamicSamplingContext_UsesActiveReplaySessionId() { // Arrange - var replayId = ReplayHelper.TestReplayId.Value; var transactionContext = new TransactionContext("name", "operation"); - var customContext = new Dictionary(); var hub = _fixture.GetSut(); // Act - var transaction = hub.StartTransaction(transactionContext, customContext); + var transaction = hub.StartTransaction(transactionContext, new Dictionary()); // Assert var transactionTracer = ((TransactionTracer)transaction); transactionTracer.SampleRand.Should().NotBeNull(); transactionTracer.DynamicSamplingContext.Should().NotBeNull(); transactionTracer.DynamicSamplingContext!.Items.Should().ContainKey("replay_id"); - transactionTracer.DynamicSamplingContext.Items["replay_id"].Should().Be(replayId!.Value.ToString()); + transactionTracer.DynamicSamplingContext.Items["replay_id"].Should().Be(ReplaySession.TestReplayId.Value!.ToString()); } [Fact] diff --git a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs index 41e711def6..57d62a57c0 100644 --- a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs +++ b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs @@ -1,5 +1,6 @@ namespace Sentry.Tests.Protocol; +[Collection(ReplayCollection.Name)] public class SentryTransactionTests { private readonly IDiagnosticLogger _testOutputLogger; @@ -56,7 +57,7 @@ public async Task NewTransactionTracer_IdleTimeoutProvided_AutomaticallyFinishes } [Fact] - public void NewTransactionTracer_PropagationContextHasReplayId_SetsReplayContext() + public void NewTransactionTracer_PropagationContextHasReplayId_UsesActiveSessionReplayIdInstead() { // Arrange var hub = Substitute.For(); @@ -78,7 +79,7 @@ public void NewTransactionTracer_PropagationContextHasReplayId_SetsReplayContext var actualTransaction = new TransactionTracer(hub, transactionContext); // Assert - actualTransaction.Contexts.Replay.ReplayId.Should().Be(SentryId.Parse("bfd31b89a59d41c99d96dc2baf840ecd")); + actualTransaction.Contexts.Replay.ReplayId.Should().Be(ReplaySession.TestReplayId.Value); Assert.NotEqual(DateTimeOffset.MinValue, actualTransaction.StartTimestamp); } diff --git a/test/Sentry.Tests/ReplayFixture.cs b/test/Sentry.Tests/ReplayFixture.cs index 7994b2dbc7..d9f7828855 100644 --- a/test/Sentry.Tests/ReplayFixture.cs +++ b/test/Sentry.Tests/ReplayFixture.cs @@ -4,7 +4,7 @@ public class ReplayFixture { public ReplayFixture() { - ReplayHelper.InitTestReplayId(); + ReplaySession.InitTestReplayId(); } } @@ -15,5 +15,6 @@ public ReplayFixture() [CollectionDefinition("Replay collection")] public class ReplayCollection : ICollectionFixture { + public const string Name = "Replay collection"; // TODO: When we upgrade to Xcode 3, it would be cleaner to use an AssemblyFixture } diff --git a/test/Sentry.Tests/TransactionProcessorTests.verify.cs b/test/Sentry.Tests/TransactionProcessorTests.verify.cs index 8edf51e9de..4a0687307b 100644 --- a/test/Sentry.Tests/TransactionProcessorTests.verify.cs +++ b/test/Sentry.Tests/TransactionProcessorTests.verify.cs @@ -2,7 +2,7 @@ namespace Sentry.Tests; -[Collection("Replay collection")] +[Collection(ReplayCollection.Name)] public partial class TransactionProcessorTests { [Fact] From 356d39d745effd668c28a2a3c7a8e428c67a0d1e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 2 May 2025 16:50:48 +1200 Subject: [PATCH 17/28] Converted static ReplayHelper to a Singleton (for cleaner testing) --- src/Sentry.AspNet/HttpContextExtensions.cs | 6 +- .../SentryTracingMiddleware.cs | 4 +- .../SentrySpanProcessor.cs | 8 +- src/Sentry/DynamicSamplingContext.cs | 37 ++++-- src/Sentry/Internal/Hub.cs | 20 +-- src/Sentry/Internal/ReplaySession.cs | 43 ++++--- src/Sentry/SentryPropagationContext.cs | 15 +-- src/Sentry/TransactionTracer.cs | 2 - ...ionTests.Versioning.DotNet8_0.verified.txt | 1 - ...ionTests.Versioning.DotNet9_0.verified.txt | 1 - .../SentrySpanProcessorTests.cs | 6 +- .../DynamicSamplingContextTests.cs | 117 ++++++++++++------ .../EventProcessorTests.Simple.verified.txt | 5 +- ...rocessorTests.WithTransaction.verified.txt | 5 +- .../EventProcessorTests.verify.cs | 1 - ...ctionEndedAsCrashed.DotNet8_0.verified.txt | 9 +- ...ctionEndedAsCrashed.DotNet9_0.verified.txt | 9 +- test/Sentry.Tests/HubTests.cs | 77 ++++++++---- .../Protocol/SentryTransactionTests.cs | 5 +- test/Sentry.Tests/ReplayFixture.cs | 20 --- .../SentryPropagationContextTests.cs | 36 ++++-- ...sactionProcessorTests.Discard.verified.txt | 5 +- ...nsactionProcessorTests.Simple.verified.txt | 13 +- .../TransactionProcessorTests.verify.cs | 1 - 24 files changed, 261 insertions(+), 185 deletions(-) delete mode 100644 test/Sentry.Tests/ReplayFixture.cs diff --git a/src/Sentry.AspNet/HttpContextExtensions.cs b/src/Sentry.AspNet/HttpContextExtensions.cs index 220413140f..42c0cf2bd2 100644 --- a/src/Sentry.AspNet/HttpContextExtensions.cs +++ b/src/Sentry.AspNet/HttpContextExtensions.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal; using Sentry.Protocol; namespace Sentry.AspNet; @@ -125,8 +126,9 @@ public static ITransactionTracer StartSentryTransaction(this HttpContext httpCon ["__HttpContext"] = httpContext, }; - // Set the Dynamic Sampling Context from the baggage header, if it exists. - var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(); + // Set the Dynamic Sampling Context from the baggage header, if it exists + // Note: We don't record Session Replays in ASP.NET + var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(ReplaySession.DisabledInstance); if (traceHeader is not null && baggageHeader is null) { diff --git a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs index 60c43b2587..e8b2a64518 100644 --- a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Options; using Sentry.AspNetCore.Extensions; using Sentry.Extensibility; +using Sentry.Internal; using Sentry.Internal.OpenTelemetry; namespace Sentry.AspNetCore; @@ -65,7 +66,8 @@ public SentryTracingMiddleware( var baggageHeader = context.Items.TryGetValue(SentryMiddleware.BaggageHeaderItemKey, out var baggageHeaderObject) ? baggageHeaderObject as BaggageHeader : null; - var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(); + // Note: We don't record Session Replays in ASP.NET core + var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(ReplaySession.DisabledInstance); if (traceHeader is not null && baggageHeader is null) { diff --git a/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs b/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs index 39230143a8..b7942bb21b 100644 --- a/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs +++ b/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs @@ -13,6 +13,7 @@ public class SentrySpanProcessor : BaseProcessor { private readonly IHub _hub; internal readonly IEnumerable _enrichers; + private readonly IReplaySession _replaySession; internal const string OpenTelemetryOrigin = "auto.otel"; // ReSharper disable once MemberCanBePrivate.Global - Used by tests @@ -38,7 +39,7 @@ public SentrySpanProcessor(IHub hub) : this(hub, null) { } - internal SentrySpanProcessor(IHub hub, IEnumerable? enrichers) + internal SentrySpanProcessor(IHub hub, IEnumerable? enrichers, IReplaySession? replaySession = null) { _hub = hub; _realHub = new Lazy(() => @@ -57,7 +58,8 @@ internal SentrySpanProcessor(IHub hub, IEnumerable? enri "You should use the TracerProviderBuilderExtensions to configure Sentry with OpenTelemetry"); } - _enrichers = enrichers ?? Enumerable.Empty(); + _enrichers = enrichers ?? []; + _replaySession = replaySession ?? ReplaySession.Instance; _options = hub.GetSentryOptions(); if (_options is null) @@ -158,7 +160,7 @@ private void CreateRootSpan(Activity data) }; var baggageHeader = data.Baggage.AsBaggageHeader(); - var dynamicSamplingContext = baggageHeader.CreateDynamicSamplingContext(); + var dynamicSamplingContext = baggageHeader.CreateDynamicSamplingContext(_replaySession); var transaction = (TransactionTracer)_hub.StartTransaction( transactionContext, new Dictionary(), dynamicSamplingContext ); diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs index c491beca4f..af5950c4c4 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -21,6 +21,7 @@ internal class DynamicSamplingContext public static readonly DynamicSamplingContext Empty = new(new Dictionary().AsReadOnly()); private DynamicSamplingContext( + IReplaySession replaySession, SentryId traceId, string publicKey, bool? sampled, @@ -89,7 +90,7 @@ private DynamicSamplingContext( items.Add("transaction", transactionName); } - if (ReplaySession.GetReplayId() is { } replayId && replayId != SentryId.Empty) + if (replaySession.ActiveReplayId is { } replayId && replayId != SentryId.Empty) { items.Add("replay_id", replayId.ToString()); } @@ -99,7 +100,19 @@ private DynamicSamplingContext( public BaggageHeader ToBaggageHeader() => BaggageHeader.Create(Items, useSentryPrefix: true); - public static DynamicSamplingContext? CreateFromBaggageHeader(BaggageHeader baggage) + public DynamicSamplingContext WithReplayId(IReplaySession replaySession) + { + if (replaySession.ActiveReplayId is not { } replayId || replayId == SentryId.Empty) + { + return this; + } + + var items = Items.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + items["replay_id"] = replayId.ToString(); + return new DynamicSamplingContext(items); + } + + public static DynamicSamplingContext? CreateFromBaggageHeader(BaggageHeader baggage, IReplaySession replaySession) { var items = baggage.GetSentryMembers(); @@ -151,7 +164,7 @@ private DynamicSamplingContext( items.Add("sample_rand", rand.ToString("N4", CultureInfo.InvariantCulture)); } - if (ReplaySession.GetReplayId() is { } replayId) + if (replaySession.ActiveReplayId is { } replayId) { // Overwrite any existing value - the DSC is simply used as a transport mechanism so that SDKs can // communicate the replayId to Sentry Relay (SDKs don't need to propagate the replayId to each other). @@ -161,7 +174,7 @@ private DynamicSamplingContext( return new DynamicSamplingContext(items); } - public static DynamicSamplingContext CreateFromTransaction(TransactionTracer transaction, SentryOptions options) + public static DynamicSamplingContext CreateFromTransaction(TransactionTracer transaction, SentryOptions options, IReplaySession replaySession) { // These should already be set on the transaction. var publicKey = options.ParsedDsn.PublicKey; @@ -176,6 +189,7 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra var environment = options.SettingLocator.GetEnvironment(); return new DynamicSamplingContext( + replaySession, traceId, publicKey, sampled, @@ -186,7 +200,7 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra transactionName); } - public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options) + public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession replaySession) { var traceId = propagationContext.TraceId; var publicKey = options.ParsedDsn.PublicKey; @@ -194,6 +208,7 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat var environment = options.SettingLocator.GetEnvironment(); return new DynamicSamplingContext( + replaySession, traceId, publicKey, null, @@ -204,12 +219,12 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat internal static class DynamicSamplingContextExtensions { - public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage) - => DynamicSamplingContext.CreateFromBaggageHeader(baggage); + public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage, IReplaySession replaySession) + => DynamicSamplingContext.CreateFromBaggageHeader(baggage, replaySession); - public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options) - => DynamicSamplingContext.CreateFromTransaction(transaction, options); + public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options, IReplaySession replaySession) + => DynamicSamplingContext.CreateFromTransaction(transaction, options, replaySession); - public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options) - => DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options); + public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options, IReplaySession replaySession) + => DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options, replaySession); } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 8b9455770a..11895e1093 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -13,6 +13,7 @@ internal class Hub : IHub, IDisposable private readonly ISessionManager _sessionManager; private readonly SentryOptions _options; private readonly RandomValuesFactory _randomValuesFactory; + private readonly IReplaySession _replaySession; #if MEMORY_DUMP_SUPPORTED private readonly MemoryMonitor? _memoryMonitor; @@ -39,7 +40,8 @@ internal Hub( ISessionManager? sessionManager = null, ISystemClock? clock = null, IInternalScopeManager? scopeManager = null, - RandomValuesFactory? randomValuesFactory = null) + RandomValuesFactory? randomValuesFactory = null, + IReplaySession? replaySession = null) { if (string.IsNullOrWhiteSpace(options.Dsn)) { @@ -55,7 +57,7 @@ internal Hub( _sessionManager = sessionManager ?? new GlobalSessionManager(options); _clock = clock ?? SystemClock.Clock; client ??= new SentryClient(options, randomValuesFactory: _randomValuesFactory, sessionManager: _sessionManager); - + _replaySession = replaySession ?? ReplaySession.Instance; ScopeManager = scopeManager ?? new SentryScopeManager(options, client); if (!options.IsGlobalModeEnabled) @@ -134,8 +136,6 @@ internal ITransactionTracer StartTransaction( : SampleRandHelper.GenerateSampleRand(context.TraceId.ToString()) }; - transaction.Contexts.Replay.ReplayId = ReplaySession.GetReplayId(); - // If the hub is disabled, we will always sample out. In other words, starting a transaction // after disposing the hub will result in that transaction not being sent to Sentry. // Additionally, we will always sample out if tracing is explicitly disabled. @@ -180,10 +180,10 @@ _options.TransactionProfilerFactory is { } profilerFactory && } } - // Use the provided DSC, or create one based on this transaction. + // Use the provided DSC (adding the active replayId if necessary), or create one based on this transaction. // DSC creation must be done AFTER the sampling decision has been made. - transaction.DynamicSamplingContext = - dynamicSamplingContext ?? transaction.CreateDynamicSamplingContext(_options); + transaction.DynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession) + ?? transaction.CreateDynamicSamplingContext(_options, _replaySession); // A sampled out transaction still appears fully functional to the user // but will be dropped by the client and won't reach Sentry's servers. @@ -226,7 +226,7 @@ public BaggageHeader GetBaggage() } var propagationContext = CurrentScope.PropagationContext; - return propagationContext.GetOrCreateDynamicSamplingContext(_options).ToBaggageHeader(); + return propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession).ToBaggageHeader(); } public TransactionContext ContinueTrace( @@ -256,7 +256,7 @@ public TransactionContext ContinueTrace( string? name = null, string? operation = null) { - var propagationContext = SentryPropagationContext.CreateFromHeaders(_options.DiagnosticLogger, traceHeader, baggageHeader); + var propagationContext = SentryPropagationContext.CreateFromHeaders(_options.DiagnosticLogger, traceHeader, baggageHeader, _replaySession); ConfigureScope(scope => scope.SetPropagationContext(propagationContext)); return new TransactionContext( @@ -384,7 +384,7 @@ private void ApplyTraceContextToEvent(SentryEvent evt, SentryPropagationContext evt.Contexts.Trace.TraceId = propagationContext.TraceId; evt.Contexts.Trace.SpanId = propagationContext.SpanId; evt.Contexts.Trace.ParentSpanId = propagationContext.ParentSpanId; - evt.DynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(_options); + evt.DynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession); } public bool CaptureEnvelope(Envelope envelope) => CurrentClient.CaptureEnvelope(envelope); diff --git a/src/Sentry/Internal/ReplaySession.cs b/src/Sentry/Internal/ReplaySession.cs index 708a5b3015..e42d8d8f3f 100644 --- a/src/Sentry/Internal/ReplaySession.cs +++ b/src/Sentry/Internal/ReplaySession.cs @@ -4,31 +4,42 @@ namespace Sentry.Internal; -internal static class ReplaySession +// TODO: This static class is pretty ugly... let's refactor it into an IReplaySession interface so that we can +// inject a mock in unit tests. If no IReplaySession is provided to the various classes that need it then we can +// fall back to a singleton instance of this class. +// +// We should be able to remove the ReplayFixture then as well (which is ugly - it forces us to initialise the test +// replay id for all tests in a Test class... which makes it difficult to test alternate scenarios). +internal interface IReplaySession { - internal static Lazy TestReplayId { get; } = new(() => SentryId.Create()); + SentryId? ActiveReplayId { get; } +} + +internal class ReplaySession : IReplaySession +{ + public static readonly IReplaySession Instance = new ReplaySession(); - private static Func? TestReplayIdResolver; + internal static readonly IReplaySession DisabledInstance = new DisabledReplaySession(); - /// - /// Initialises the test replay id resolver so that unit tests return a test id (rather than trying to resovle an - /// ID from static platform libraries). - /// - internal static void InitTestReplayId() + private ReplaySession() { - TestReplayIdResolver = () => TestReplayId.Value; } - internal static SentryId? GetReplayId() => (TestReplayIdResolver ?? ReplayIdResolver)(); - - private static SentryId? ReplayIdResolver() + public SentryId? ActiveReplayId { + get { #if __ANDROID__ - // Check to see if a Replay ID is available - var replayId = JavaSdk.ScopesAdapter.Instance?.Options?.ReplayController?.ReplayId?.ToSentryId(); - return (replayId is { } id && id != SentryId.Empty) ? id : null; + // Check to see if a Replay ID is available + var replayId = JavaSdk.ScopesAdapter.Instance?.Options?.ReplayController?.ReplayId?.ToSentryId(); + return (replayId is { } id && id != SentryId.Empty) ? id : null; #else - return null; + return null; #endif + } + } + + private class DisabledReplaySession : IReplaySession + { + public SentryId? ActiveReplayId => null; } } diff --git a/src/Sentry/SentryPropagationContext.cs b/src/Sentry/SentryPropagationContext.cs index cce69ac58e..24f9550638 100644 --- a/src/Sentry/SentryPropagationContext.cs +++ b/src/Sentry/SentryPropagationContext.cs @@ -11,19 +11,12 @@ internal class SentryPropagationContext internal DynamicSamplingContext? _dynamicSamplingContext; - internal SentryId? GetReplayId() - { - return _dynamicSamplingContext?.Items.TryGetValue("replay_id", out var value) == true - ? SentryId.Parse(value) - : ReplaySession.GetReplayId(); - } - - public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options) + public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options, IReplaySession replaySession) { if (_dynamicSamplingContext is null) { options.LogDebug("Creating the Dynamic Sampling Context from the Propagation Context"); - _dynamicSamplingContext = this.CreateDynamicSamplingContext(options); + _dynamicSamplingContext = this.CreateDynamicSamplingContext(options, replaySession); } return _dynamicSamplingContext; @@ -55,7 +48,7 @@ public SentryPropagationContext(SentryPropagationContext? other) _dynamicSamplingContext = other?._dynamicSamplingContext; } - public static SentryPropagationContext CreateFromHeaders(IDiagnosticLogger? logger, SentryTraceHeader? traceHeader, BaggageHeader? baggageHeader) + public static SentryPropagationContext CreateFromHeaders(IDiagnosticLogger? logger, SentryTraceHeader? traceHeader, BaggageHeader? baggageHeader, IReplaySession replaySession) { logger?.LogDebug("Creating a propagation context from headers."); @@ -65,7 +58,7 @@ public static SentryPropagationContext CreateFromHeaders(IDiagnosticLogger? logg return new SentryPropagationContext(); } - var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(); + var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(replaySession); return new SentryPropagationContext(traceHeader.TraceId, traceHeader.SpanId, dynamicSamplingContext); } } diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index ebaec3103f..c4da3d5933 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -252,8 +252,6 @@ internal TransactionTracer(IHub hub, ITransactionContext context, TimeSpan? idle _instrumenter = transactionContext.Instrumenter; Origin = transactionContext.Origin; } - // If there is a ReplayId on the PropagationContext, set this in the Contexts - hub.ConfigureScope(s => Contexts.Replay.ReplayId = s.PropagationContext.GetReplayId()); // Set idle timer only if an idle timeout has been provided directly if (idleTimeout.HasValue) diff --git a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet8_0.verified.txt b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet8_0.verified.txt index 0e7e2ed474..019cc1fb2d 100644 --- a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet8_0.verified.txt +++ b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet8_0.verified.txt @@ -17,7 +17,6 @@ QueryString: }, Contexts: { - replay: {}, trace: { Operation: http.server, Origin: auto.http.aspnetcore, diff --git a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet9_0.verified.txt b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet9_0.verified.txt index 0e7e2ed474..019cc1fb2d 100644 --- a/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet9_0.verified.txt +++ b/test/Sentry.AspNetCore.Tests/WebIntegrationTests.Versioning.DotNet9_0.verified.txt @@ -17,7 +17,6 @@ QueryString: }, Contexts: { - replay: {}, trace: { Operation: http.server, Origin: auto.http.aspnetcore, diff --git a/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs b/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs index f7d74b1b88..67a13d7da0 100644 --- a/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs @@ -18,6 +18,8 @@ private class Fixture public List Enrichers { get; set; } = new(); + private IReplaySession ReplaySession { get; } = Substitute.For(); + public Fixture() { Options = new SentryOptions @@ -32,11 +34,11 @@ public Fixture() public Hub Hub { get; private set; } - public Hub GetHub() => Hub ??= new Hub(Options, Client, SessionManager, Clock, ScopeManager); + private Hub GetHub() => Hub ??= new Hub(Options, Client, SessionManager, Clock, ScopeManager, replaySession: ReplaySession); public SentrySpanProcessor GetSut(IHub hub = null) { - return new SentrySpanProcessor(hub ?? GetHub(), Enrichers); + return new SentrySpanProcessor(hub ?? GetHub(), Enrichers, ReplaySession); } } diff --git a/test/Sentry.Tests/DynamicSamplingContextTests.cs b/test/Sentry.Tests/DynamicSamplingContextTests.cs index c2efa189d2..ebdf99c51c 100644 --- a/test/Sentry.Tests/DynamicSamplingContextTests.cs +++ b/test/Sentry.Tests/DynamicSamplingContextTests.cs @@ -2,9 +2,26 @@ namespace Sentry.Tests; -[Collection(ReplayCollection.Name)] public class DynamicSamplingContextTests { + class Fixture + { + public SentryId ActiveReplayId { get; } = SentryId.Create(); + public IReplaySession InactiveReplaySession { get; } + public IReplaySession ActiveReplaySession { get; } + + public Fixture() + { + ActiveReplaySession = Substitute.For(); + ActiveReplaySession.ActiveReplayId.Returns(ActiveReplayId); + + InactiveReplaySession = Substitute.For(); + InactiveReplaySession.ActiveReplayId.Returns((SentryId?)null); + } + } + + Fixture _fixture = new(); + [Fact] public void EmptyContext() { @@ -22,7 +39,7 @@ public void CreateFromBaggage_TraceId_Missing() {"sentry-sample_rate", "1.0"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -37,7 +54,7 @@ public void CreateFromBaggage_TraceId_EmptyGuid() {"sentry-sample_rate", "1.0"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -52,7 +69,7 @@ public void CreateFromBaggage_TraceId_Invalid() {"sentry-sample_rate", "1.0"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -66,7 +83,7 @@ public void CreateFromBaggage_PublicKey_Missing() {"sentry-sample_rate", "1.0"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -81,7 +98,7 @@ public void CreateFromBaggage_PublicKey_Blank() {"sentry-sample_rate", "1.0"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -95,7 +112,7 @@ public void CreateFromBaggage_SampleRate_Missing() {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -110,7 +127,7 @@ public void CreateFromBaggage_SampleRate_Invalid() {"sentry-sample_rate", "not-a-number"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -125,7 +142,7 @@ public void CreateFromBaggage_SampleRate_TooLow() {"sentry-sample_rate", "-0.1"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -140,7 +157,7 @@ public void CreateFromBaggage_SampleRate_TooHigh() {"sentry-sample_rate", "1.1"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -156,7 +173,7 @@ public void CreateFromBaggage_SampleRand_Invalid() {"sentry-sample_rand", "not-a-number"}, }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -172,7 +189,7 @@ public void CreateFromBaggage_SampleRand_TooLow() {"sentry-sample_rand", "-0.1"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -188,7 +205,7 @@ public void CreateFromBaggage_SampleRand_TooHigh() {"sentry-sample_rand", "1.0"} // Must be less than 1 }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } @@ -203,7 +220,7 @@ public void CreateFromBaggage_NotSampledNoSampleRand_GeneratesSampleRand() {"sentry-sample_rate", "0.5"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); using var scope = new AssertionScope(); Assert.NotNull(dsc); @@ -226,7 +243,7 @@ public void CreateFromBaggage_SampledNoSampleRand_GeneratesConsistentSampleRand( {"sentry-sampled", sampled}, }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); using var scope = new AssertionScope(); Assert.NotNull(dsc); @@ -255,13 +272,15 @@ public void CreateFromBaggage_Sampled_MalFormed() {"sentry-sampled", "foo"}, }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(_fixture.InactiveReplaySession); Assert.Null(dsc); } - [Fact] - public void CreateFromBaggage_Valid_Minimum() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CreateFromBaggage_Valid_Minimum(bool replaySessionIsActive) { var baggage = BaggageHeader.Create(new List> { @@ -270,20 +289,29 @@ public void CreateFromBaggage_Valid_Minimum() {"sentry-sample_rate", "1.0"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(replaySessionIsActive ? _fixture.ActiveReplaySession : _fixture.InactiveReplaySession); Assert.NotNull(dsc); - Assert.Equal(5, dsc.Items.Count); + Assert.Equal(replaySessionIsActive ? 5 : 4, dsc.Items.Count); Assert.Equal("43365712692146d08ee11a729dfbcaca", Assert.Contains("trace_id", dsc.Items)); Assert.Equal("d4d82fc1c2c4032a83f3a29aa3a3aff", Assert.Contains("public_key", dsc.Items)); Assert.Equal("1.0", Assert.Contains("sample_rate", dsc.Items)); Assert.Contains("sample_rand", dsc.Items); - // We add the replay_id automatically when we have an active replay session - Assert.Equal(ReplaySession.TestReplayId.Value!.ToString(), Assert.Contains("replay_id", dsc.Items)); + if (replaySessionIsActive) + { + // We add the replay_id automatically when we have an active replay session + Assert.Equal(_fixture.ActiveReplayId.ToString(), Assert.Contains("replay_id", dsc.Items)); + } + else + { + Assert.DoesNotContain("replay_id", dsc.Items); + } } - [Fact] - public void CreateFromBaggage_Valid_Complete() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CreateFromBaggage_Valid_Complete(bool replaySessionIsActive) { var baggage = BaggageHeader.Create(new List> { @@ -299,7 +327,7 @@ public void CreateFromBaggage_Valid_Complete() {"sentry-replay_id","bfd31b89a59d41c99d96dc2baf840ecd"} }); - var dsc = baggage.CreateDynamicSamplingContext(); + var dsc = baggage.CreateDynamicSamplingContext(replaySessionIsActive ? _fixture.ActiveReplaySession : _fixture.InactiveReplaySession); Assert.NotNull(dsc); Assert.Equal(baggage.Members.Count, dsc.Items.Count); @@ -312,8 +340,16 @@ public void CreateFromBaggage_Valid_Complete() Assert.Equal("production", Assert.Contains("environment", dsc.Items)); Assert.Equal("Group B", Assert.Contains("user_segment", dsc.Items)); Assert.Equal("GET /person/{id}", Assert.Contains("transaction", dsc.Items)); - // The replay_id doesn't get propagated when we have an active replay session of our own - Assert.Equal(ReplaySession.TestReplayId.Value!.ToString(), Assert.Contains("replay_id", dsc.Items)); + if (replaySessionIsActive) + { + // We overwrite the replay_id when we have an active replay session + Assert.Equal(_fixture.ActiveReplayId.ToString(), Assert.Contains("replay_id", dsc.Items)); + } + else + { + // If we don't have any active replay session of our own then we propagate whatever was in the baggage header + Assert.Equal("bfd31b89a59d41c99d96dc2baf840ecd", Assert.Contains("replay_id", dsc.Items)); + } } [Fact] @@ -329,10 +365,10 @@ public void ToBaggageHeader() {"sentry-environment", "production"}, {"sentry-user_segment", "Group B"}, {"sentry-transaction", "GET /person/{id}"}, - {"sentry-replay_id", ReplaySession.TestReplayId.Value!.ToString()} + {"sentry-replay_id", _fixture.ActiveReplayId.ToString()} }); - var dsc = original.CreateDynamicSamplingContext(); + var dsc = original.CreateDynamicSamplingContext(_fixture.ActiveReplaySession); var result = dsc?.ToBaggageHeader(); @@ -371,7 +407,7 @@ public void CreateFromTransaction(bool? isSampled) }, }; - var dsc = transaction.CreateDynamicSamplingContext(options); + var dsc = transaction.CreateDynamicSamplingContext(options, _fixture.ActiveReplaySession); Assert.NotNull(dsc); Assert.Equal(isSampled.HasValue ? 9 : 8, dsc.Items.Count); @@ -391,24 +427,33 @@ public void CreateFromTransaction(bool? isSampled) Assert.Equal("staging", Assert.Contains("environment", dsc.Items)); Assert.Equal("GET /person/{id}", Assert.Contains("transaction", dsc.Items)); // We add the replay_id automatically when we have an active replay session - Assert.Equal(ReplaySession.TestReplayId.Value!.ToString(), Assert.Contains("replay_id", dsc.Items)); + Assert.Equal(_fixture.ActiveReplayId.ToString(), Assert.Contains("replay_id", dsc.Items)); } - [Fact] - public void CreateFromPropagationContext_Valid_Complete() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CreateFromPropagationContext_Valid_Complete(bool replaySessionIsActive) { - var replayId = ReplaySession.TestReplayId.Value; var options = new SentryOptions { Dsn = "https://a@sentry.io/1", Release = "test-release", Environment = "test-environment" }; var propagationContext = new SentryPropagationContext( SentryId.Parse("43365712692146d08ee11a729dfbcaca"), SpanId.Parse("1234")); - var dsc = propagationContext.CreateDynamicSamplingContext(options); + var dsc = propagationContext.CreateDynamicSamplingContext(options, replaySessionIsActive ? _fixture.ActiveReplaySession : _fixture.InactiveReplaySession); Assert.NotNull(dsc); Assert.Equal("43365712692146d08ee11a729dfbcaca", Assert.Contains("trace_id", dsc.Items)); Assert.Equal("a", Assert.Contains("public_key", dsc.Items)); Assert.Equal("test-release", Assert.Contains("release", dsc.Items)); Assert.Equal("test-environment", Assert.Contains("environment", dsc.Items)); - Assert.Equal(replayId.ToString(), Assert.Contains("replay_id", dsc.Items)); + if (replaySessionIsActive) + { + // We add the replay_id automatically when we have an active replay session + Assert.Equal(_fixture.ActiveReplayId.ToString(), Assert.Contains("replay_id", dsc.Items)); + } + else + { + Assert.DoesNotContain("replay_id", dsc.Items); + } } } diff --git a/test/Sentry.Tests/EventProcessorTests.Simple.verified.txt b/test/Sentry.Tests/EventProcessorTests.Simple.verified.txt index 4b02ea304c..b63e99dd53 100644 --- a/test/Sentry.Tests/EventProcessorTests.Simple.verified.txt +++ b/test/Sentry.Tests/EventProcessorTests.Simple.verified.txt @@ -9,8 +9,7 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - replay_id: Guid_2, - trace_id: Guid_3 + trace_id: Guid_2 } }, Items: [ @@ -33,7 +32,7 @@ } }, User: { - Id: Guid_4 + Id: Guid_3 }, Environment: production } diff --git a/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt b/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt index c7fffc8fe3..a39b4497ea 100644 --- a/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt +++ b/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt @@ -9,11 +9,10 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - replay_id: Guid_2, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_3, + trace_id: Guid_2, transaction: my transaction } }, @@ -38,7 +37,7 @@ } }, User: { - Id: Guid_4 + Id: Guid_3 }, Environment: production } diff --git a/test/Sentry.Tests/EventProcessorTests.verify.cs b/test/Sentry.Tests/EventProcessorTests.verify.cs index 57a8293363..6dd6394fd5 100644 --- a/test/Sentry.Tests/EventProcessorTests.verify.cs +++ b/test/Sentry.Tests/EventProcessorTests.verify.cs @@ -2,7 +2,6 @@ namespace Sentry.Tests; -[Collection(ReplayCollection.Name)] public class EventProcessorTests { private readonly TestOutputDiagnosticLogger _logger; diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt index 5c08ab79ff..c510d50d43 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt @@ -31,11 +31,10 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_4, + trace_id: Guid_3, transaction: my transaction } }, @@ -140,7 +139,7 @@ }, { Header: { - event_id: Guid_5, + event_id: Guid_4, sdk: { name: sentry.dotnet }, @@ -148,11 +147,10 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_4, + trace_id: Guid_3, transaction: my transaction } }, @@ -172,7 +170,6 @@ SampleRate: 1.0, Request: {}, Contexts: { - replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt index 5c08ab79ff..c510d50d43 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt @@ -31,11 +31,10 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_4, + trace_id: Guid_3, transaction: my transaction } }, @@ -140,7 +139,7 @@ }, { Header: { - event_id: Guid_5, + event_id: Guid_4, sdk: { name: sentry.dotnet }, @@ -148,11 +147,10 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_4, + trace_id: Guid_3, transaction: my transaction } }, @@ -172,7 +170,6 @@ SampleRate: 1.0, Request: {}, Contexts: { - replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index fea0f46f42..bd7c48041a 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -4,7 +4,6 @@ namespace Sentry.Tests; -[Collection(ReplayCollection.Name)] public partial class HubTests { private readonly ITestOutputHelper _output; @@ -12,14 +11,11 @@ public partial class HubTests private class Fixture { public SentryOptions Options { get; } - public ISentryClient Client { get; set; } - public ISessionManager SessionManager { get; set; } - public IInternalScopeManager ScopeManager { get; set; } - public ISystemClock Clock { get; set; } + public IReplaySession ReplaySession { get; } public Fixture() { @@ -31,9 +27,11 @@ public Fixture() }; Client = Substitute.For(); + + ReplaySession = Substitute.For(); } - public Hub GetSut() => new(Options, Client, SessionManager, Clock, ScopeManager); + public Hub GetSut() => new(Options, Client, SessionManager, Clock, ScopeManager, replaySession: ReplaySession); } private readonly Fixture _fixture = new(); @@ -175,7 +173,7 @@ public void CaptureException_TransactionFinished_Gets_DSC_From_LinkedSpan() {"sentry-trace_id", "75302ac48a024bde9a3b3734a82e36c8"}, {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, {"sentry-replay_id","bfd31b89a59d41c99d96dc2baf840ecd"} - }).CreateDynamicSamplingContext(); + }).CreateDynamicSamplingContext(_fixture.ReplaySession); var transaction = hub.StartTransaction( transactionContext, @@ -682,11 +680,16 @@ public void StartTransaction_SameInstrumenter_SampledIn() transaction.IsSampled.Should().BeTrue(); } - [Fact] - public void StartTransaction_DynamicSamplingContextWithReplayId_UsesActiveReplaySessionId() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void StartTransaction_DynamicSamplingContextWithReplayId_UsesActiveReplaySessionId(bool replaySessionIsActive) { // Arrange var transactionContext = new TransactionContext("name", "operation"); + + var dummyReplaySession = Substitute.For(); + dummyReplaySession.ActiveReplayId.Returns((SentryId?)null); // So the replay id in the baggage header is used var dsc = BaggageHeader.Create(new List> { {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, @@ -694,9 +697,10 @@ public void StartTransaction_DynamicSamplingContextWithReplayId_UsesActiveReplay {"sentry-sampled", "true"}, {"sentry-sample_rate", "0.5"}, // Required in the baggage header, but ignored by sampling logic {"sentry-replay_id","bfd31b89a59d41c99d96dc2baf840ecd"} - }).CreateDynamicSamplingContext(); + }).CreateDynamicSamplingContext(dummyReplaySession); _fixture.Options.TracesSampleRate = 1.0; + _fixture.ReplaySession.ActiveReplayId.Returns(replaySessionIsActive ? SentryId.Create() : null); // This one gets used by the SUT var hub = _fixture.GetSut(); // Act @@ -705,19 +709,33 @@ public void StartTransaction_DynamicSamplingContextWithReplayId_UsesActiveReplay // Assert var transactionTracer = ((TransactionTracer)transaction); transactionTracer.IsSampled.Should().Be(true); - transactionTracer.DynamicSamplingContext.Should().Be(dsc); transactionTracer.DynamicSamplingContext.Should().NotBeNull(); - transactionTracer.DynamicSamplingContext!.Items.Should().ContainKey("replay_id"); - // The replay_id doesn't get propagated when we have an active replay session of our own - transactionTracer.DynamicSamplingContext.Items["replay_id"].Should().Be(ReplaySession.TestReplayId.Value!.ToString()); + foreach (var dscItem in dsc!.Items) + { + if (dscItem.Key == "replay_id") + { + transactionTracer.DynamicSamplingContext!.Items["replay_id"].Should().Be(replaySessionIsActive + // We overwrite the replay_id when we have an active replay session + ? _fixture.ReplaySession.ActiveReplayId.ToString() + // Otherwise we propagate whatever was in the baggage header + : dscItem.Value); + } + else + { + transactionTracer.DynamicSamplingContext!.Items.Should() + .Contain(kvp => kvp.Key == dscItem.Key && kvp.Value == dscItem.Value); + } + } } - [Fact] - public void StartTransaction_NoDynamicSamplingContext_UsesActiveReplaySessionId() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void StartTransaction_NoDynamicSamplingContext_UsesActiveReplaySessionId(bool replaySessionIsActive) { // Arrange var transactionContext = new TransactionContext("name", "operation"); - + _fixture.ReplaySession.ActiveReplayId.Returns(replaySessionIsActive ? SentryId.Create() : null); var hub = _fixture.GetSut(); // Act @@ -727,8 +745,15 @@ public void StartTransaction_NoDynamicSamplingContext_UsesActiveReplaySessionId( var transactionTracer = ((TransactionTracer)transaction); transactionTracer.SampleRand.Should().NotBeNull(); transactionTracer.DynamicSamplingContext.Should().NotBeNull(); - transactionTracer.DynamicSamplingContext!.Items.Should().ContainKey("replay_id"); - transactionTracer.DynamicSamplingContext.Items["replay_id"].Should().Be(ReplaySession.TestReplayId.Value!.ToString()); + if (replaySessionIsActive) + { + // We add the replay_id when we have an active replay session + transactionTracer.DynamicSamplingContext!.Items["replay_id"].Should().Be(_fixture.ReplaySession.ActiveReplayId.ToString()); + } + else + { + transactionTracer.DynamicSamplingContext!.Items.Should().NotContainKey("replay_id"); + } } [Fact] @@ -756,12 +781,11 @@ public void StartTransaction_DynamicSamplingContextWithoutSampleRand_SampleRandN { // Arrange var transactionContext = new TransactionContext("name", "operation"); - var customContext = new Dictionary(); var hub = _fixture.GetSut(); // Act - var transaction = hub.StartTransaction(transactionContext, customContext, DynamicSamplingContext.Empty); + var transaction = hub.StartTransaction(transactionContext, new Dictionary(), DynamicSamplingContext.Empty); // Assert var transactionTracer = ((TransactionTracer)transaction); @@ -776,7 +800,7 @@ public void StartTransaction_DynamicSamplingContextWithSampleRand_InheritsSample { // Arrange var transactionContext = new TransactionContext("name", "operation"); - var customContext = new Dictionary(); + var dummyReplaySession = Substitute.For(); var dsc = BaggageHeader.Create(new List> { {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, @@ -784,13 +808,13 @@ public void StartTransaction_DynamicSamplingContextWithSampleRand_InheritsSample {"sentry-sampled", "true"}, {"sentry-sample_rate", "0.5"}, // Required in the baggage header, but ignored by sampling logic {"sentry-sample_rand", "0.1234"} - }).CreateDynamicSamplingContext(); + }).CreateDynamicSamplingContext(dummyReplaySession); _fixture.Options.TracesSampleRate = 0.4; var hub = _fixture.GetSut(); // Act - var transaction = hub.StartTransaction(transactionContext, customContext, dsc); + var transaction = hub.StartTransaction(transactionContext, new Dictionary(), dsc); // Assert var transactionTracer = ((TransactionTracer)transaction); @@ -815,7 +839,7 @@ public void StartTransaction_TraceSampler_UsesSampleRand(double sampleRate, bool {"sentry-sampled", "true"}, {"sentry-sample_rate", "0.5"}, {"sentry-sample_rand", "0.1234"} - }).CreateDynamicSamplingContext(); + }).CreateDynamicSamplingContext(_fixture.ReplaySession); _fixture.Options.TracesSampler = _ => sampleRate; var hub = _fixture.GetSut(); @@ -839,13 +863,14 @@ public void StartTransaction_StaticSampler_UsesSampleRand(double sampleRate, boo // Arrange var transactionContext = new TransactionContext("name", "operation"); var customContext = new Dictionary(); + var dummyReplaySession = Substitute.For(); var dsc = BaggageHeader.Create(new List> { {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, {"sentry-sample_rate", "0.5"}, // Static sampling ignores this and uses options.TracesSampleRate instead {"sentry-sample_rand", "0.1234"} - }).CreateDynamicSamplingContext(); + }).CreateDynamicSamplingContext(dummyReplaySession); _fixture.Options.TracesSampleRate = sampleRate; var hub = _fixture.GetSut(); diff --git a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs index 57d62a57c0..0521c49437 100644 --- a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs +++ b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs @@ -1,6 +1,5 @@ namespace Sentry.Tests.Protocol; -[Collection(ReplayCollection.Name)] public class SentryTransactionTests { private readonly IDiagnosticLogger _testOutputLogger; @@ -62,6 +61,7 @@ public void NewTransactionTracer_PropagationContextHasReplayId_UsesActiveSession // Arrange var hub = Substitute.For(); var traceHeader = new SentryTraceHeader(SentryId.Create(), SpanId.Create(), null); + var replayContext = Substitute.For(); var baggageHeader = BaggageHeader.Create(new List> { { "sentry-sample_rate", "1.0" }, @@ -70,7 +70,7 @@ public void NewTransactionTracer_PropagationContextHasReplayId_UsesActiveSession { "sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff" }, { "sentry-replay_id", "bfd31b89a59d41c99d96dc2baf840ecd" } }); - var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, baggageHeader); + var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, baggageHeader, replayContext); var scope = new Scope(hub.GetSentryOptions(), propagationContext); hub.ConfigureScope(Arg.Do>(callback => callback(scope))); var transactionContext = new TransactionContext("test-name", "test-operation"); @@ -79,7 +79,6 @@ public void NewTransactionTracer_PropagationContextHasReplayId_UsesActiveSession var actualTransaction = new TransactionTracer(hub, transactionContext); // Assert - actualTransaction.Contexts.Replay.ReplayId.Should().Be(ReplaySession.TestReplayId.Value); Assert.NotEqual(DateTimeOffset.MinValue, actualTransaction.StartTimestamp); } diff --git a/test/Sentry.Tests/ReplayFixture.cs b/test/Sentry.Tests/ReplayFixture.cs deleted file mode 100644 index d9f7828855..0000000000 --- a/test/Sentry.Tests/ReplayFixture.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Sentry.Tests; - -public class ReplayFixture -{ - public ReplayFixture() - { - ReplaySession.InitTestReplayId(); - } -} - -/// -/// This class has no code, and is never created. Its purpose is simply to be the place to apply [CollectionDefinition] -/// and the interface. -/// -[CollectionDefinition("Replay collection")] -public class ReplayCollection : ICollectionFixture -{ - public const string Name = "Replay collection"; - // TODO: When we upgrade to Xcode 3, it would be cleaner to use an AssemblyFixture -} diff --git a/test/Sentry.Tests/SentryPropagationContextTests.cs b/test/Sentry.Tests/SentryPropagationContextTests.cs index dc658d2ed9..7e9cba386f 100644 --- a/test/Sentry.Tests/SentryPropagationContextTests.cs +++ b/test/Sentry.Tests/SentryPropagationContextTests.cs @@ -2,11 +2,29 @@ namespace Sentry.Tests; public class SentryPropagationContextTests { + class Fixture + { + public SentryId ActiveReplayId { get; } = SentryId.Create(); + public IReplaySession InactiveReplaySession { get; } + public IReplaySession ActiveReplaySession { get; } + + public Fixture() + { + ActiveReplaySession = Substitute.For(); + ActiveReplaySession.ActiveReplayId.Returns(ActiveReplayId); + + InactiveReplaySession = Substitute.For(); + InactiveReplaySession.ActiveReplayId.Returns((SentryId?)null); + } + } + + Fixture _fixture = new(); + [Fact] public void CopyConstructor_CreatesCopy() { var original = new SentryPropagationContext(); - original.GetOrCreateDynamicSamplingContext(new SentryOptions { Dsn = ValidDsn }); + original.GetOrCreateDynamicSamplingContext(new SentryOptions { Dsn = ValidDsn }, _fixture.InactiveReplaySession); var copy = new SentryPropagationContext(original); @@ -22,7 +40,7 @@ public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNull_Creat var propagationContext = new SentryPropagationContext(); Assert.Null(propagationContext._dynamicSamplingContext); // Sanity check - _ = propagationContext.GetOrCreateDynamicSamplingContext(options); + _ = propagationContext.GetOrCreateDynamicSamplingContext(options, _fixture.InactiveReplaySession); Assert.NotNull(propagationContext._dynamicSamplingContext); } @@ -32,9 +50,9 @@ public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNotNull_Re { var options = new SentryOptions { Dsn = ValidDsn }; var propagationContext = new SentryPropagationContext(); - var firstDynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(options); + var firstDynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(options, _fixture.InactiveReplaySession); - var secondDynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(options); + var secondDynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(options, _fixture.InactiveReplaySession); Assert.Same(firstDynamicSamplingContext, secondDynamicSamplingContext); } @@ -42,7 +60,7 @@ public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNotNull_Re [Fact] public void CreateFromHeaders_HeadersNull_CreatesPropagationContextWithTraceAndSpanId() { - var propagationContext = SentryPropagationContext.CreateFromHeaders(null, null, null); + var propagationContext = SentryPropagationContext.CreateFromHeaders(null, null, null, _fixture.InactiveReplaySession); Assert.NotEqual(propagationContext.TraceId, SentryId.Empty); Assert.NotEqual(propagationContext.SpanId, SpanId.Empty); @@ -53,7 +71,7 @@ public void CreateFromHeaders_TraceHeaderNotNull_CreatesPropagationContextFromTr { var traceHeader = new SentryTraceHeader(SentryId.Create(), SpanId.Create(), null); - var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, null); + var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, null, _fixture.InactiveReplaySession); Assert.Equal(traceHeader.TraceId, propagationContext.TraceId); Assert.NotEqual(traceHeader.SpanId, propagationContext.SpanId); // Sanity check @@ -71,7 +89,7 @@ public void CreateFromHeaders_TraceHeaderNullButBaggageExists_CreatesPropagation { "sentry-replay_id", "bfd31b89a59d41c99d96dc2baf840ecd" } }); - var propagationContext = SentryPropagationContext.CreateFromHeaders(null, null, baggageHeader); + var propagationContext = SentryPropagationContext.CreateFromHeaders(null, null, baggageHeader, _fixture.InactiveReplaySession); Assert.Null(propagationContext._dynamicSamplingContext); } @@ -89,8 +107,8 @@ public void CreateFromHeaders_BaggageHeaderNotNull_CreatesPropagationContextWith { "sentry-replay_id", "bfd31b89a59d41c99d96dc2baf840ecd" } }); - var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, baggageHeader); + var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, baggageHeader, _fixture.InactiveReplaySession); - Assert.Equal(5, propagationContext.GetOrCreateDynamicSamplingContext(new SentryOptions()).Items.Count); + Assert.Equal(5, propagationContext.GetOrCreateDynamicSamplingContext(new SentryOptions(), _fixture.InactiveReplaySession).Items.Count); } } diff --git a/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt b/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt index d60ed49633..1c801c9b72 100644 --- a/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt +++ b/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt @@ -9,11 +9,10 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - replay_id: Guid_2, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_3, + trace_id: Guid_2, transaction: my transaction } }, @@ -37,7 +36,7 @@ } }, User: { - Id: Guid_4 + Id: Guid_3 }, Environment: production } diff --git a/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt b/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt index 4214e4298f..0e9cc2a73b 100644 --- a/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt +++ b/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt @@ -9,11 +9,10 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - replay_id: Guid_2, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_3, + trace_id: Guid_2, transaction: my transaction } }, @@ -37,7 +36,7 @@ } }, User: { - Id: Guid_4 + Id: Guid_3 }, Environment: production } @@ -47,7 +46,7 @@ }, { Header: { - event_id: Guid_5, + event_id: Guid_4, sdk: { name: sentry.dotnet }, @@ -55,11 +54,10 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - replay_id: Guid_2, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_3, + trace_id: Guid_2, transaction: my transaction } }, @@ -80,7 +78,6 @@ Request: {}, Contexts: { key: value, - replay: {}, trace: { Operation: my operation, Description: , @@ -89,7 +86,7 @@ } }, User: { - Id: Guid_4 + Id: Guid_3 }, Environment: production, IsFinished: true diff --git a/test/Sentry.Tests/TransactionProcessorTests.verify.cs b/test/Sentry.Tests/TransactionProcessorTests.verify.cs index 4a0687307b..ce2cb3d0b8 100644 --- a/test/Sentry.Tests/TransactionProcessorTests.verify.cs +++ b/test/Sentry.Tests/TransactionProcessorTests.verify.cs @@ -2,7 +2,6 @@ namespace Sentry.Tests; -[Collection(ReplayCollection.Name)] public partial class TransactionProcessorTests { [Fact] From 66e3f80a9c3250de3f4867b265f365d135220399 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 2 May 2025 17:23:23 +1200 Subject: [PATCH 18/28] Add replayId scenarios to SentryPropagationContextTests --- .../SentryPropagationContextTests.cs | 90 ++++++++++++++----- 1 file changed, 70 insertions(+), 20 deletions(-) diff --git a/test/Sentry.Tests/SentryPropagationContextTests.cs b/test/Sentry.Tests/SentryPropagationContextTests.cs index 7e9cba386f..bb90359d0a 100644 --- a/test/Sentry.Tests/SentryPropagationContextTests.cs +++ b/test/Sentry.Tests/SentryPropagationContextTests.cs @@ -2,7 +2,7 @@ namespace Sentry.Tests; public class SentryPropagationContextTests { - class Fixture + private class Fixture { public SentryId ActiveReplayId { get; } = SentryId.Create(); public IReplaySession InactiveReplaySession { get; } @@ -18,10 +18,12 @@ public Fixture() } } - Fixture _fixture = new(); + private readonly Fixture _fixture = new(); - [Fact] - public void CopyConstructor_CreatesCopy() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CopyConstructor_CreatesCopyWithReplayId(bool replaySessionIsActive) { var original = new SentryPropagationContext(); original.GetOrCreateDynamicSamplingContext(new SentryOptions { Dsn = ValidDsn }, _fixture.InactiveReplaySession); @@ -30,40 +32,70 @@ public void CopyConstructor_CreatesCopy() Assert.Equal(original.TraceId, copy.TraceId); Assert.Equal(original.SpanId, copy.SpanId); - Assert.Equal(original._dynamicSamplingContext, copy._dynamicSamplingContext); + Assert.Equal(original._dynamicSamplingContext!.Items.Count, copy._dynamicSamplingContext!.Items.Count); + foreach (var dscItem in original._dynamicSamplingContext!.Items) + { + if (dscItem.Key == "replay_id") + { + copy._dynamicSamplingContext!.Items["replay_id"].Should().Be(replaySessionIsActive + // We overwrite the replay_id when we have an active replay session + ? _fixture.ActiveReplayId.ToString() + // Otherwise we propagate whatever was in the baggage header + : dscItem.Value); + } + else + { + copy._dynamicSamplingContext!.Items.Should() + .Contain(kvp => kvp.Key == dscItem.Key && kvp.Value == dscItem.Value); + } + } } - [Fact] - public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNull_CreatesDynamicSamplingContext() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNull_CreatesDynamicSamplingContext(bool replaySessionIsActive) { var options = new SentryOptions { Dsn = ValidDsn }; var propagationContext = new SentryPropagationContext(); Assert.Null(propagationContext._dynamicSamplingContext); // Sanity check - _ = propagationContext.GetOrCreateDynamicSamplingContext(options, _fixture.InactiveReplaySession); + _ = propagationContext.GetOrCreateDynamicSamplingContext(options, replaySessionIsActive ? _fixture.ActiveReplaySession : _fixture.InactiveReplaySession); Assert.NotNull(propagationContext._dynamicSamplingContext); + if (replaySessionIsActive) + { + // We add the replay_id automatically when we have an active replay session + Assert.Equal(_fixture.ActiveReplayId.ToString(), Assert.Contains("replay_id", propagationContext._dynamicSamplingContext.Items)); + } + else + { + Assert.DoesNotContain("replay_id", propagationContext._dynamicSamplingContext.Items); + } } - [Fact] - public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNotNull_ReturnsSameDynamicSamplingContext() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNotNull_ReturnsSameDynamicSamplingContext(bool replaySessionIsActive) { var options = new SentryOptions { Dsn = ValidDsn }; var propagationContext = new SentryPropagationContext(); - var firstDynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(options, _fixture.InactiveReplaySession); + var firstDynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(options, replaySessionIsActive ? _fixture.ActiveReplaySession : _fixture.InactiveReplaySession); - var secondDynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(options, _fixture.InactiveReplaySession); + var secondDynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(options, replaySessionIsActive ? _fixture.ActiveReplaySession : _fixture.InactiveReplaySession); Assert.Same(firstDynamicSamplingContext, secondDynamicSamplingContext); } [Fact] - public void CreateFromHeaders_HeadersNull_CreatesPropagationContextWithTraceAndSpanId() + public void CreateFromHeaders_HeadersNull_CreatesPropagationContextWithTraceAndSpanAndReplayId() { - var propagationContext = SentryPropagationContext.CreateFromHeaders(null, null, null, _fixture.InactiveReplaySession); + var propagationContext = SentryPropagationContext.CreateFromHeaders(null, null, null, _fixture.ActiveReplaySession); Assert.NotEqual(propagationContext.TraceId, SentryId.Empty); Assert.NotEqual(propagationContext.SpanId, SpanId.Empty); + Assert.Null(propagationContext._dynamicSamplingContext); } [Fact] @@ -71,15 +103,16 @@ public void CreateFromHeaders_TraceHeaderNotNull_CreatesPropagationContextFromTr { var traceHeader = new SentryTraceHeader(SentryId.Create(), SpanId.Create(), null); - var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, null, _fixture.InactiveReplaySession); + var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, null, _fixture.ActiveReplaySession); Assert.Equal(traceHeader.TraceId, propagationContext.TraceId); Assert.NotEqual(traceHeader.SpanId, propagationContext.SpanId); // Sanity check Assert.Equal(traceHeader.SpanId, propagationContext.ParentSpanId); + Assert.Null(propagationContext._dynamicSamplingContext); } [Fact] - public void CreateFromHeaders_TraceHeaderNullButBaggageExists_CreatesPropagationContextWithoutDynamicSamplingContext() + public void CreateFromHeaders_BaggageExistsButTraceHeaderNull_CreatesPropagationContextWithoutDynamicSamplingContext() { var baggageHeader = BaggageHeader.Create(new List> { @@ -94,9 +127,12 @@ public void CreateFromHeaders_TraceHeaderNullButBaggageExists_CreatesPropagation Assert.Null(propagationContext._dynamicSamplingContext); } - [Fact] - public void CreateFromHeaders_BaggageHeaderNotNull_CreatesPropagationContextWithDynamicSamplingContext() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CreateFromHeaders_BaggageHeaderNotNull_CreatesPropagationContextWithDynamicSamplingContext(bool replaySessionIsActive) { + // Arrange var traceHeader = new SentryTraceHeader(SentryId.Create(), SpanId.Create(), null); var baggageHeader = BaggageHeader.Create(new List> { @@ -106,9 +142,23 @@ public void CreateFromHeaders_BaggageHeaderNotNull_CreatesPropagationContextWith { "sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff" }, { "sentry-replay_id", "bfd31b89a59d41c99d96dc2baf840ecd" } }); + var replaySession = replaySessionIsActive ? _fixture.ActiveReplaySession : _fixture.InactiveReplaySession; - var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, baggageHeader, _fixture.InactiveReplaySession); + // Act + var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, baggageHeader, replaySession); - Assert.Equal(5, propagationContext.GetOrCreateDynamicSamplingContext(new SentryOptions(), _fixture.InactiveReplaySession).Items.Count); + // Assert + var dsc = propagationContext.GetOrCreateDynamicSamplingContext(new SentryOptions(), replaySession); + Assert.Equal(5, dsc.Items.Count); + if (replaySessionIsActive) + { + // We add the replay_id automatically when we have an active replay session + Assert.Equal(_fixture.ActiveReplayId.ToString(), Assert.Contains("replay_id", dsc.Items)); + } + else + { + // Otherwise we inherit the replay_id from the baggage header + Assert.Equal("bfd31b89a59d41c99d96dc2baf840ecd", Assert.Contains("replay_id", dsc.Items)); + } } } From edc0b9c76ce582c3de909f49e375ac48f16324e1 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Fri, 2 May 2025 05:36:46 +0000 Subject: [PATCH 19/28] Format code --- src/Sentry/Internal/ReplaySession.cs | 7 ++++--- test/Sentry.Tests/DynamicSamplingContextTests.cs | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Sentry/Internal/ReplaySession.cs b/src/Sentry/Internal/ReplaySession.cs index e42d8d8f3f..c84ef78f39 100644 --- a/src/Sentry/Internal/ReplaySession.cs +++ b/src/Sentry/Internal/ReplaySession.cs @@ -12,7 +12,7 @@ namespace Sentry.Internal; // replay id for all tests in a Test class... which makes it difficult to test alternate scenarios). internal interface IReplaySession { - SentryId? ActiveReplayId { get; } + public SentryId? ActiveReplayId { get; } } internal class ReplaySession : IReplaySession @@ -27,7 +27,8 @@ private ReplaySession() public SentryId? ActiveReplayId { - get { + get + { #if __ANDROID__ // Check to see if a Replay ID is available var replayId = JavaSdk.ScopesAdapter.Instance?.Options?.ReplayController?.ReplayId?.ToSentryId(); @@ -38,7 +39,7 @@ public SentryId? ActiveReplayId } } - private class DisabledReplaySession : IReplaySession + private class DisabledReplaySession : IReplaySession { public SentryId? ActiveReplayId => null; } diff --git a/test/Sentry.Tests/DynamicSamplingContextTests.cs b/test/Sentry.Tests/DynamicSamplingContextTests.cs index ebdf99c51c..a3fdb6a8ff 100644 --- a/test/Sentry.Tests/DynamicSamplingContextTests.cs +++ b/test/Sentry.Tests/DynamicSamplingContextTests.cs @@ -4,7 +4,7 @@ namespace Sentry.Tests; public class DynamicSamplingContextTests { - class Fixture + private class Fixture { public SentryId ActiveReplayId { get; } = SentryId.Create(); public IReplaySession InactiveReplaySession { get; } @@ -20,7 +20,7 @@ public Fixture() } } - Fixture _fixture = new(); + private Fixture _fixture = new(); [Fact] public void EmptyContext() From 7b95524550e0deff2fb009e7dc7461c0ab82fe58 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Fri, 2 May 2025 17:57:58 +1200 Subject: [PATCH 20/28] Windows verify tests --- .../SqlListenerTests.LoggingAsync.DotNet8_0.verified.txt | 1 - .../SqlListenerTests.LoggingAsync.DotNet9_0.verified.txt | 1 - ...qlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt | 1 - ...qlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt | 1 - .../SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt | 1 - .../SqlListenerTests.RecordsSqlAsync.verified.txt | 1 - .../IntegrationTests.Simple.verified.txt | 1 - ...xceptionTransactionEndedAsCrashed.Net4_8.verified.txt | 9 +++------ 8 files changed, 3 insertions(+), 13 deletions(-) diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet8_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet8_0.verified.txt index fe2cd0f50b..348b185b6f 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet8_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet8_0.verified.txt @@ -10,7 +10,6 @@ SampleRate: 1.0, Request: {}, Contexts: { - replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet9_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet9_0.verified.txt index fe2cd0f50b..348b185b6f 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet9_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.LoggingAsync.DotNet9_0.verified.txt @@ -10,7 +10,6 @@ SampleRate: 1.0, Request: {}, Contexts: { - replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt index 22b9a3b1d1..05803effbe 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt @@ -45,7 +45,6 @@ SampleRate: 1.0, Request: {}, Contexts: { - replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt index 22b9a3b1d1..05803effbe 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt @@ -45,7 +45,6 @@ SampleRate: 1.0, Request: {}, Contexts: { - replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt index 53cb9a722d..75d6b3e321 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt @@ -45,7 +45,6 @@ SampleRate: 1.0, Request: {}, Contexts: { - replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt index f86bcfdbe2..e317514b27 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt @@ -45,7 +45,6 @@ SampleRate: 1.0, Request: {}, Contexts: { - replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt b/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt index 7fdd0e0858..826fb58e95 100644 --- a/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt +++ b/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt @@ -46,7 +46,6 @@ SampleRate: 1.0, Request: {}, Contexts: { - replay: {}, trace: { Operation: my operation, Description: , diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt index 4516665826..f41bcaa626 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt @@ -31,11 +31,10 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_4, + trace_id: Guid_3, transaction: my transaction } }, @@ -140,7 +139,7 @@ }, { Header: { - event_id: Guid_5, + event_id: Guid_4, sdk: { name: sentry.dotnet }, @@ -148,11 +147,10 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, - replay_id: Guid_3, sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, - trace_id: Guid_4, + trace_id: Guid_3, transaction: my transaction } }, @@ -172,7 +170,6 @@ SampleRate: 1.0, Request: {}, Contexts: { - replay: {}, trace: { Operation: my operation, Description: , From 39e56fdcb42d072301a141d3210ef21cfe85507c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 2 May 2025 18:04:28 +1200 Subject: [PATCH 21/28] Update ReplaySession.cs --- src/Sentry/Internal/ReplaySession.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Sentry/Internal/ReplaySession.cs b/src/Sentry/Internal/ReplaySession.cs index c84ef78f39..d3ce1fd68a 100644 --- a/src/Sentry/Internal/ReplaySession.cs +++ b/src/Sentry/Internal/ReplaySession.cs @@ -4,12 +4,6 @@ namespace Sentry.Internal; -// TODO: This static class is pretty ugly... let's refactor it into an IReplaySession interface so that we can -// inject a mock in unit tests. If no IReplaySession is provided to the various classes that need it then we can -// fall back to a singleton instance of this class. -// -// We should be able to remove the ReplayFixture then as well (which is ugly - it forces us to initialise the test -// replay id for all tests in a Test class... which makes it difficult to test alternate scenarios). internal interface IReplaySession { public SentryId? ActiveReplayId { get; } From 4f2f39c4508394dba4ee4dd74848bf1745421c0e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 5 May 2025 10:56:06 +1200 Subject: [PATCH 22/28] Ensure trace or propagation context always gets applied to event --- src/Sentry/Internal/Hub.cs | 7 ++----- test/Sentry.Tests/HubTests.cs | 11 ++++++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 99222b9e54..67ce32ebcd 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -473,12 +473,9 @@ private SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Scope scope) { // We get the span linked to the event or fall back to the current span var span = GetLinkedSpan(evt) ?? scope.Span; - if (span is not null) + if (span is not null && span.IsSampled is not false) { - if (span.IsSampled is not false) - { - ApplyTraceContextToEvent(evt, span); - } + ApplyTraceContextToEvent(evt, span); } else { diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index b66c54f92c..897a245417 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -201,7 +201,12 @@ public void CaptureException_ActiveSpanExistsOnScopeButIsSampledOut_EventIsNotLi var exception = new Exception("error"); var transaction = hub.StartTransaction("foo", "bar"); - hub.ConfigureScope(scope => scope.Transaction = transaction); + SentryPropagationContext propagationContext = null; + hub.ConfigureScope(scope => + { + scope.Transaction = transaction; + propagationContext = scope.PropagationContext; + }); // Act hub.CaptureException(exception); @@ -209,8 +214,8 @@ public void CaptureException_ActiveSpanExistsOnScopeButIsSampledOut_EventIsNotLi // Assert _fixture.Client.Received(1).CaptureEvent( Arg.Is(evt => - evt.Contexts.Trace.TraceId == default && - evt.Contexts.Trace.SpanId == default), + evt.Contexts.Trace.TraceId == propagationContext.TraceId && + evt.Contexts.Trace.SpanId == propagationContext.SpanId), Arg.Any(), Arg.Any()); } From 8ef8d66eb9db40f54e3d08eee5cf275e93a5f32b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 5 May 2025 10:58:00 +1200 Subject: [PATCH 23/28] Update CHANGELOG.md --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81e3a0eec1..9fa73319be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Associate replays with errors and traces on Android ([#4133](https://github.com/getsentry/sentry-dotnet/pull/4133)) + ## 5.7.0-beta.0 ### Features @@ -7,7 +13,6 @@ - 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)) -- Associate replays with errors and traces on Android ([#4133](https://github.com/getsentry/sentry-dotnet/pull/4133)) ### Fixes From 4ce0049a2c137375fee039cc67deeec0032be16a Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 8 May 2025 21:02:05 +1200 Subject: [PATCH 24/28] Removed ReplaySession.DisabledInstance --- src/Sentry.AspNet/HttpContextExtensions.cs | 2 +- .../SentryTracingMiddleware.cs | 2 +- src/Sentry/DynamicSamplingContext.cs | 40 +++++++++---------- src/Sentry/Internal/ReplaySession.cs | 7 ---- 4 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/Sentry.AspNet/HttpContextExtensions.cs b/src/Sentry.AspNet/HttpContextExtensions.cs index 42c0cf2bd2..557217bdc3 100644 --- a/src/Sentry.AspNet/HttpContextExtensions.cs +++ b/src/Sentry.AspNet/HttpContextExtensions.cs @@ -128,7 +128,7 @@ public static ITransactionTracer StartSentryTransaction(this HttpContext httpCon // Set the Dynamic Sampling Context from the baggage header, if it exists // Note: We don't record Session Replays in ASP.NET - var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(ReplaySession.DisabledInstance); + var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(null); if (traceHeader is not null && baggageHeader is null) { diff --git a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs index e8b2a64518..1a0c547693 100644 --- a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs @@ -67,7 +67,7 @@ public SentryTracingMiddleware( ? baggageHeaderObject as BaggageHeader : null; // Note: We don't record Session Replays in ASP.NET core - var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(ReplaySession.DisabledInstance); + var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(null); if (traceHeader is not null && baggageHeader is null) { diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs index af5950c4c4..68338ff66c 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -20,17 +20,15 @@ internal class DynamicSamplingContext /// public static readonly DynamicSamplingContext Empty = new(new Dictionary().AsReadOnly()); - private DynamicSamplingContext( - IReplaySession replaySession, - SentryId traceId, + private DynamicSamplingContext(SentryId traceId, string publicKey, bool? sampled, double? sampleRate = null, double? sampleRand = null, string? release = null, string? environment = null, - string? transactionName = null - ) + string? transactionName = null, + IReplaySession? replaySession = null) { // Validate and set required values if (traceId == SentryId.Empty) @@ -90,7 +88,7 @@ private DynamicSamplingContext( items.Add("transaction", transactionName); } - if (replaySession.ActiveReplayId is { } replayId && replayId != SentryId.Empty) + if (replaySession?.ActiveReplayId is { } replayId && replayId != SentryId.Empty) { items.Add("replay_id", replayId.ToString()); } @@ -100,9 +98,9 @@ private DynamicSamplingContext( public BaggageHeader ToBaggageHeader() => BaggageHeader.Create(Items, useSentryPrefix: true); - public DynamicSamplingContext WithReplayId(IReplaySession replaySession) + public DynamicSamplingContext WithReplayId(IReplaySession? replaySession) { - if (replaySession.ActiveReplayId is not { } replayId || replayId == SentryId.Empty) + if (replaySession?.ActiveReplayId is not { } replayId || replayId == SentryId.Empty) { return this; } @@ -112,7 +110,7 @@ public DynamicSamplingContext WithReplayId(IReplaySession replaySession) return new DynamicSamplingContext(items); } - public static DynamicSamplingContext? CreateFromBaggageHeader(BaggageHeader baggage, IReplaySession replaySession) + public static DynamicSamplingContext? CreateFromBaggageHeader(BaggageHeader baggage, IReplaySession? replaySession) { var items = baggage.GetSentryMembers(); @@ -164,7 +162,7 @@ public DynamicSamplingContext WithReplayId(IReplaySession replaySession) items.Add("sample_rand", rand.ToString("N4", CultureInfo.InvariantCulture)); } - if (replaySession.ActiveReplayId is { } replayId) + if (replaySession?.ActiveReplayId is { } replayId) { // Overwrite any existing value - the DSC is simply used as a transport mechanism so that SDKs can // communicate the replayId to Sentry Relay (SDKs don't need to propagate the replayId to each other). @@ -174,7 +172,7 @@ public DynamicSamplingContext WithReplayId(IReplaySession replaySession) return new DynamicSamplingContext(items); } - public static DynamicSamplingContext CreateFromTransaction(TransactionTracer transaction, SentryOptions options, IReplaySession replaySession) + public static DynamicSamplingContext CreateFromTransaction(TransactionTracer transaction, SentryOptions options, IReplaySession? replaySession) { // These should already be set on the transaction. var publicKey = options.ParsedDsn.PublicKey; @@ -188,19 +186,18 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra var release = options.SettingLocator.GetRelease(); var environment = options.SettingLocator.GetEnvironment(); - return new DynamicSamplingContext( - replaySession, - traceId, + return new DynamicSamplingContext(traceId, publicKey, sampled, sampleRate, sampleRand, release, environment, - transactionName); + transactionName, + replaySession); } - public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession replaySession) + public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) { var traceId = propagationContext.TraceId; var publicKey = options.ParsedDsn.PublicKey; @@ -208,23 +205,24 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat var environment = options.SettingLocator.GetEnvironment(); return new DynamicSamplingContext( - replaySession, traceId, publicKey, null, release: release, - environment: environment); + environment: environment, + replaySession: replaySession + ); } } internal static class DynamicSamplingContextExtensions { - public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage, IReplaySession replaySession) + public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage, IReplaySession? replaySession) => DynamicSamplingContext.CreateFromBaggageHeader(baggage, replaySession); - public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options, IReplaySession replaySession) + public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options, IReplaySession? replaySession) => DynamicSamplingContext.CreateFromTransaction(transaction, options, replaySession); - public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options, IReplaySession replaySession) + public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) => DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options, replaySession); } diff --git a/src/Sentry/Internal/ReplaySession.cs b/src/Sentry/Internal/ReplaySession.cs index d3ce1fd68a..30f151c894 100644 --- a/src/Sentry/Internal/ReplaySession.cs +++ b/src/Sentry/Internal/ReplaySession.cs @@ -13,8 +13,6 @@ internal class ReplaySession : IReplaySession { public static readonly IReplaySession Instance = new ReplaySession(); - internal static readonly IReplaySession DisabledInstance = new DisabledReplaySession(); - private ReplaySession() { } @@ -32,9 +30,4 @@ public SentryId? ActiveReplayId #endif } } - - private class DisabledReplaySession : IReplaySession - { - public SentryId? ActiveReplayId => null; - } } From c3ab0b42f80c43cebb9ca2b05457e363c8b89a3f Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 9 May 2025 18:29:03 +1200 Subject: [PATCH 25/28] Review feedback --- src/Sentry.AspNet/HttpContextExtensions.cs | 3 +- .../SentryTracingMiddleware.cs | 4 +- src/Sentry/DynamicSamplingContext.cs | 2 +- src/Sentry/Internal/Hub.cs | 2 +- src/Sentry/Protocol/Replay.cs | 88 ------------------- src/Sentry/SentryContexts.cs | 9 -- src/Sentry/SentryTransaction.cs | 6 -- ...piApprovalTests.Run.DotNet8_0.verified.txt | 12 --- ...piApprovalTests.Run.DotNet9_0.verified.txt | 12 --- .../ApiApprovalTests.Run.Net4_8.verified.txt | 12 --- test/Sentry.Tests/HubTests.cs | 13 +-- .../Protocol/Context/ReplayTests.cs | 63 ------------- .../TransactionProcessorTests.verify.cs | 2 - 13 files changed, 8 insertions(+), 220 deletions(-) delete mode 100644 src/Sentry/Protocol/Replay.cs delete mode 100644 test/Sentry.Tests/Protocol/Context/ReplayTests.cs diff --git a/src/Sentry.AspNet/HttpContextExtensions.cs b/src/Sentry.AspNet/HttpContextExtensions.cs index 557217bdc3..b652062969 100644 --- a/src/Sentry.AspNet/HttpContextExtensions.cs +++ b/src/Sentry.AspNet/HttpContextExtensions.cs @@ -127,8 +127,7 @@ public static ITransactionTracer StartSentryTransaction(this HttpContext httpCon }; // Set the Dynamic Sampling Context from the baggage header, if it exists - // Note: We don't record Session Replays in ASP.NET - var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(null); + var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(); if (traceHeader is not null && baggageHeader is null) { diff --git a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs index 1a0c547693..d878e1c00a 100644 --- a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs @@ -65,9 +65,7 @@ public SentryTracingMiddleware( ? traceHeaderObject as SentryTraceHeader : null; var baggageHeader = context.Items.TryGetValue(SentryMiddleware.BaggageHeaderItemKey, out var baggageHeaderObject) ? baggageHeaderObject as BaggageHeader : null; - - // Note: We don't record Session Replays in ASP.NET core - var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(null); + var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(); if (traceHeader is not null && baggageHeader is null) { diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs index 68338ff66c..fdbee91aaa 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -217,7 +217,7 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat internal static class DynamicSamplingContextExtensions { - public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage, IReplaySession? replaySession) + public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage, IReplaySession? replaySession = null) => DynamicSamplingContext.CreateFromBaggageHeader(baggage, replaySession); public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options, IReplaySession? replaySession) diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 67ce32ebcd..ce896d2600 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -473,7 +473,7 @@ private SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Scope scope) { // We get the span linked to the event or fall back to the current span var span = GetLinkedSpan(evt) ?? scope.Span; - if (span is not null && span.IsSampled is not false) + if (span is not null) { ApplyTraceContextToEvent(evt, span); } diff --git a/src/Sentry/Protocol/Replay.cs b/src/Sentry/Protocol/Replay.cs deleted file mode 100644 index 5a74789c7b..0000000000 --- a/src/Sentry/Protocol/Replay.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Sentry.Extensibility; -using Sentry.Internal; -using Sentry.Internal.Extensions; - -namespace Sentry.Protocol; - -/// -/// Sentry Replay context interface. -/// -/// -/// { -/// "contexts": { -/// "replay": { -/// "replay_id": "12312012123120121231201212312012" -/// } -/// } -/// } -/// -/// -public sealed class Replay : ISentryJsonSerializable, ICloneable, IUpdatable -{ - /// - /// Tells Sentry which type of context this is. - /// - public const string Type = "replay"; - - /// - /// The name of the runtime. - /// - public SentryId? ReplayId { get; set; } - - /// - /// Clones this instance. - /// - public Replay Clone() - { - var response = new Replay(); - - response.UpdateFrom(this); - - return response; - } - - /// - /// Updates this instance with data from the properties in the , - /// unless there is already a value in the existing property. - /// - public void UpdateFrom(Replay source) - { - ReplayId ??= source.ReplayId; - } - - /// - /// Updates this instance with data from the properties in the , - /// unless there is already a value in the existing property. - /// - public void UpdateFrom(object source) - { - if (source is Replay response) - { - UpdateFrom(response); - } - } - - /// - public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) - { - writer.WriteStartObject(); - - writer.WriteString("type", Type); - writer.WriteSerializableIfNotNull("replay_id", ReplayId, logger); - - writer.WriteEndObject(); - } - - /// - /// Parses from JSON. - /// - public static Replay FromJson(JsonElement json) - { - var replayId = json.GetPropertyOrNull("replay_id")?.Pipe(SentryId.FromJson); - - return new Replay - { - ReplayId = replayId - }; - } -} diff --git a/src/Sentry/SentryContexts.cs b/src/Sentry/SentryContexts.cs index 74aa51b8d4..b0abb14227 100644 --- a/src/Sentry/SentryContexts.cs +++ b/src/Sentry/SentryContexts.cs @@ -55,11 +55,6 @@ public SentryFeedback? Feedback /// public OperatingSystem OperatingSystem => _innerDictionary.GetOrCreate(OperatingSystem.Type); - /// - /// Replay interface that contains information about the Session Replay (if any) related to the event. - /// - public Replay Replay => _innerDictionary.GetOrCreate(Replay.Type); - /// /// Response interface that contains information on any HTTP response related to the event. /// @@ -177,10 +172,6 @@ public static SentryContexts FromJson(JsonElement json) { result[name] = OperatingSystem.FromJson(value); } - else if (string.Equals(type, Replay.Type, StringComparison.OrdinalIgnoreCase)) - { - result[name] = Replay.FromJson(value); - } else if (string.Equals(type, Response.Type, StringComparison.OrdinalIgnoreCase)) { result[name] = Response.FromJson(value); diff --git a/src/Sentry/SentryTransaction.cs b/src/Sentry/SentryTransaction.cs index e70ccb3a64..386891ec01 100644 --- a/src/Sentry/SentryTransaction.cs +++ b/src/Sentry/SentryTransaction.cs @@ -274,12 +274,6 @@ public SentryTransaction(ITransactionTracer tracer) { SampleRate = transactionTracer.SampleRate; DynamicSamplingContext = transactionTracer.DynamicSamplingContext; - if (DynamicSamplingContext?.Items.TryGetValue("replay_id", out var replayId) == true) - { - // Sentry Relay should populate this from the DSC automatically, but just for good measure - tracer.Contexts.Replay.ReplayId = SentryId.Parse(replayId); - } - TransactionProfiler = transactionTracer.TransactionProfiler; if (transactionTracer.HasMetrics) { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 5cd48c0d63..f70df81000 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -505,7 +505,6 @@ namespace Sentry public object this[string key] { get; set; } public System.Collections.Generic.ICollection Keys { get; } public Sentry.Protocol.OperatingSystem OperatingSystem { get; } - public Sentry.Protocol.Replay Replay { get; } public Sentry.Protocol.Response Response { get; } public Sentry.Protocol.Runtime Runtime { get; } public Sentry.Protocol.Trace Trace { get; } @@ -1767,17 +1766,6 @@ namespace Sentry.Protocol public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } public static Sentry.Protocol.OperatingSystem FromJson(System.Text.Json.JsonElement json) { } } - public sealed class Replay : Sentry.ISentryJsonSerializable - { - public const string Type = "replay"; - public Replay() { } - public Sentry.SentryId? ReplayId { get; set; } - public Sentry.Protocol.Replay Clone() { } - public void UpdateFrom(Sentry.Protocol.Replay source) { } - public void UpdateFrom(object source) { } - public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } - public static Sentry.Protocol.Replay FromJson(System.Text.Json.JsonElement json) { } - } public sealed class Response : Sentry.ISentryJsonSerializable { public const string Type = "response"; diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 5cd48c0d63..f70df81000 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -505,7 +505,6 @@ namespace Sentry public object this[string key] { get; set; } public System.Collections.Generic.ICollection Keys { get; } public Sentry.Protocol.OperatingSystem OperatingSystem { get; } - public Sentry.Protocol.Replay Replay { get; } public Sentry.Protocol.Response Response { get; } public Sentry.Protocol.Runtime Runtime { get; } public Sentry.Protocol.Trace Trace { get; } @@ -1767,17 +1766,6 @@ namespace Sentry.Protocol public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } public static Sentry.Protocol.OperatingSystem FromJson(System.Text.Json.JsonElement json) { } } - public sealed class Replay : Sentry.ISentryJsonSerializable - { - public const string Type = "replay"; - public Replay() { } - public Sentry.SentryId? ReplayId { get; set; } - public Sentry.Protocol.Replay Clone() { } - public void UpdateFrom(Sentry.Protocol.Replay source) { } - public void UpdateFrom(object source) { } - public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } - public static Sentry.Protocol.Replay FromJson(System.Text.Json.JsonElement json) { } - } public sealed class Response : Sentry.ISentryJsonSerializable { public const string Type = "response"; diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 44355e3e50..3060d147e9 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -493,7 +493,6 @@ namespace Sentry public object this[string key] { get; set; } public System.Collections.Generic.ICollection Keys { get; } public Sentry.Protocol.OperatingSystem OperatingSystem { get; } - public Sentry.Protocol.Replay Replay { get; } public Sentry.Protocol.Response Response { get; } public Sentry.Protocol.Runtime Runtime { get; } public Sentry.Protocol.Trace Trace { get; } @@ -1749,17 +1748,6 @@ namespace Sentry.Protocol public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? _) { } public static Sentry.Protocol.OperatingSystem FromJson(System.Text.Json.JsonElement json) { } } - public sealed class Replay : Sentry.ISentryJsonSerializable - { - public const string Type = "replay"; - public Replay() { } - public Sentry.SentryId? ReplayId { get; set; } - public Sentry.Protocol.Replay Clone() { } - public void UpdateFrom(Sentry.Protocol.Replay source) { } - public void UpdateFrom(object source) { } - public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } - public static Sentry.Protocol.Replay FromJson(System.Text.Json.JsonElement json) { } - } public sealed class Response : Sentry.ISentryJsonSerializable { public const string Type = "response"; diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index 897a245417..871c5f1f39 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -193,7 +193,7 @@ public void CaptureException_TransactionFinished_Gets_DSC_From_LinkedSpan() } [Fact] - public void CaptureException_ActiveSpanExistsOnScopeButIsSampledOut_EventIsNotLinkedToSpan() + public void CaptureException_ActiveSpanExistsOnScopeButIsSampledOut_EventIsLinkedToSpan() { // Arrange _fixture.Options.TracesSampleRate = 0.0; @@ -201,12 +201,7 @@ public void CaptureException_ActiveSpanExistsOnScopeButIsSampledOut_EventIsNotLi var exception = new Exception("error"); var transaction = hub.StartTransaction("foo", "bar"); - SentryPropagationContext propagationContext = null; - hub.ConfigureScope(scope => - { - scope.Transaction = transaction; - propagationContext = scope.PropagationContext; - }); + hub.ConfigureScope(scope => scope.Transaction = transaction); // Act hub.CaptureException(exception); @@ -214,8 +209,8 @@ public void CaptureException_ActiveSpanExistsOnScopeButIsSampledOut_EventIsNotLi // Assert _fixture.Client.Received(1).CaptureEvent( Arg.Is(evt => - evt.Contexts.Trace.TraceId == propagationContext.TraceId && - evt.Contexts.Trace.SpanId == propagationContext.SpanId), + evt.Contexts.Trace.TraceId == transaction.TraceId && + evt.Contexts.Trace.SpanId == transaction.SpanId), Arg.Any(), Arg.Any()); } diff --git a/test/Sentry.Tests/Protocol/Context/ReplayTests.cs b/test/Sentry.Tests/Protocol/Context/ReplayTests.cs deleted file mode 100644 index c0de75983c..0000000000 --- a/test/Sentry.Tests/Protocol/Context/ReplayTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Sentry.Tests.Protocol.Context; - -public class ReplayTests -{ - private readonly IDiagnosticLogger _testOutputLogger; - - public ReplayTests(ITestOutputHelper output) - { - _testOutputLogger = new TestOutputDiagnosticLogger(output); - } - - [Fact] - public void Ctor_NoPropertyFilled_SerializesEmptyObject() - { - // Arrange - var replay = new Replay(); - - // Act - var actual = replay.ToJsonString(_testOutputLogger); - - // Assert - Assert.Equal("""{"type":"replay"}""", actual); - } - - [Fact] - public void SerializeObject_AllPropertiesSetToNonDefault_SerializesValidObject() - { - // Arrange - var replay = new Replay - { - ReplayId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8") - }; - - // Act - var actual = replay.ToJsonString(_testOutputLogger, indented: true); - - // Assert - Assert.Equal( - """ - { - "type": "replay", - "replay_id": "75302ac48a024bde9a3b3734a82e36c8" - } - """, - actual); - } - - [Fact] - public void Clone_CopyValues() - { - // Arrange - var replay = new Replay - { - ReplayId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8") - }; - - // Act - var clone = replay.Clone(); - - // Assert - Assert.Equal(replay.ReplayId, clone.ReplayId); - } -} diff --git a/test/Sentry.Tests/TransactionProcessorTests.verify.cs b/test/Sentry.Tests/TransactionProcessorTests.verify.cs index ce2cb3d0b8..7d6425405b 100644 --- a/test/Sentry.Tests/TransactionProcessorTests.verify.cs +++ b/test/Sentry.Tests/TransactionProcessorTests.verify.cs @@ -1,5 +1,3 @@ -using Sentry.Tests.Internals; - namespace Sentry.Tests; public partial class TransactionProcessorTests From cf96b6062b44f8f3d9af6824632d392df4ce2aea Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 9 May 2025 18:37:33 +1200 Subject: [PATCH 26/28] Update CHANGELOG.md --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f95953baa..c64badb539 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,12 @@ ### Features - Associate replays with errors and traces on Android ([#4133](https://github.com/getsentry/sentry-dotnet/pull/4133)) +- New source generator allows Sentry to see true build variables like PublishAot and PublishTrimmed to properly adapt checks in the Sentry SDK ([#4101](https://github.com/getsentry/sentry-dotnet/pull/4101)) ### Fixes - Redact Authorization headers before sending events to Sentry ([#4164](https://github.com/getsentry/sentry-dotnet/pull/4164)) -### Features - -- New source generator allows Sentry to see true build variables like PublishAot and PublishTrimmed to properly adapt checks in the Sentry SDK ([#4101](https://github.com/getsentry/sentry-dotnet/pull/4101)) - ### Dependencies - Bump CLI from v2.43.1 to v2.44.0 ([#4169](https://github.com/getsentry/sentry-dotnet/pull/4169)) From 54e55369da61ee4172b7b521423951b366528548 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 13 May 2025 11:10:54 +1200 Subject: [PATCH 27/28] Update src/Sentry/DynamicSamplingContext.cs --- src/Sentry/DynamicSamplingContext.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs index fdbee91aaa..cda413c7bc 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -164,8 +164,9 @@ public DynamicSamplingContext WithReplayId(IReplaySession? replaySession) if (replaySession?.ActiveReplayId is { } replayId) { - // Overwrite any existing value - the DSC is simply used as a transport mechanism so that SDKs can - // communicate the replayId to Sentry Relay (SDKs don't need to propagate the replayId to each other). + // Any upstream replay_id will be propagated only if the current process hasn't started it's own replay session. + // Otherwise we have to overwrite this as it's the only way to communicate the replayId to Sentry Relay. + // In Mobile apps this should never be a problem. items["replay_id"] = replayId.ToString(); } From 438c4e6f1d44751671131ab5b399d3f7b7396524 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 13 May 2025 08:28:14 +0200 Subject: [PATCH 28/28] chore: fixup changelog merge changes --- CHANGELOG.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 459b464579..508cd01edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ ### Features -- Associate replays with errors and traces on Android ([#4133](https://github.com/getsentry/sentry-dotnet/pull/4133)) - New source generator allows Sentry to see true build variables like PublishAot and PublishTrimmed to properly adapt checks in the Sentry SDK ([#4101](https://github.com/getsentry/sentry-dotnet/pull/4101)) +- Auto breadcrumbs now include all .NET MAUI gesture recognizer events ([#4124](https://github.com/getsentry/sentry-dotnet/pull/4124)) +- Associate replays with errors and traces on Android ([#4133](https://github.com/getsentry/sentry-dotnet/pull/4133)) ### Fixes @@ -13,11 +14,6 @@ - Remove Strong Naming from Sentry.Hangfire ([#4099](https://github.com/getsentry/sentry-dotnet/pull/4099)) - Increase `RequestSize.Small` threshold from 1 kB to 4 kB to match other SDKs ([#4177](https://github.com/getsentry/sentry-dotnet/pull/4177)) -### Features - -- New source generator allows Sentry to see true build variables like PublishAot and PublishTrimmed to properly adapt checks in the Sentry SDK ([#4101](https://github.com/getsentry/sentry-dotnet/pull/4101)) -- Auto breadcrumbs now include all .NET MAUI gesture recognizer events ([#4124](https://github.com/getsentry/sentry-dotnet/pull/4124)) - ### Dependencies - Bump CLI from v2.43.1 to v2.45.0 ([#4169](https://github.com/getsentry/sentry-dotnet/pull/4169), [#4179](https://github.com/getsentry/sentry-dotnet/pull/4179)) @@ -28,7 +24,7 @@ ### 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)) +- 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)) @@ -50,7 +46,7 @@ ### Features - Option to disable the SentryNative integration ([#4107](https://github.com/getsentry/sentry-dotnet/pull/4107), [#4134](https://github.com/getsentry/sentry-dotnet/pull/4134)) - - To disable it, add this msbuild property: `false` + - To disable it, add this msbuild property: `false` - Reintroduced experimental support for Session Replay on Android ([#4097](https://github.com/getsentry/sentry-dotnet/pull/4097)) - If an incoming HTTP request has the `traceparent` header, it is now parsed and interpreted like the `sentry-trace` header. Outgoing requests now contain the `traceparent` header to facilitate integration with servesr that only support the [W3C Trace Context](https://www.w3.org/TR/trace-context/). ([#4084](https://github.com/getsentry/sentry-dotnet/pull/4084))