diff --git a/CHANGELOG.md b/CHANGELOG.md index 156cc1d0c4..508cd01edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,18 @@ ## Unreleased +### 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)) +- Associate replays with errors and traces on Android ([#4133](https://github.com/getsentry/sentry-dotnet/pull/4133)) + ### Fixes - Redact Authorization headers before sending events to Sentry ([#4164](https://github.com/getsentry/sentry-dotnet/pull/4164)) - 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)) @@ -23,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)) @@ -45,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)) diff --git a/src/Sentry.AspNet/HttpContextExtensions.cs b/src/Sentry.AspNet/HttpContextExtensions.cs index 220413140f..b652062969 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,7 +126,7 @@ public static ITransactionTracer StartSentryTransaction(this HttpContext httpCon ["__HttpContext"] = httpContext, }; - // Set the Dynamic Sampling Context from the baggage header, if it exists. + // Set the Dynamic Sampling Context from the baggage header, if it exists 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 60c43b2587..d878e1c00a 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; @@ -64,7 +65,6 @@ public SentryTracingMiddleware( ? traceHeaderObject as SentryTraceHeader : null; var baggageHeader = context.Items.TryGetValue(SentryMiddleware.BaggageHeaderItemKey, out var baggageHeaderObject) ? baggageHeaderObject as BaggageHeader : null; - var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(); 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 e85ec88b84..cda413c7bc 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -20,15 +20,15 @@ internal class DynamicSamplingContext /// public static readonly DynamicSamplingContext Empty = new(new Dictionary().AsReadOnly()); - private DynamicSamplingContext( - 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) @@ -51,7 +51,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, @@ -88,12 +88,29 @@ private DynamicSamplingContext( items.Add("transaction", transactionName); } + if (replaySession?.ActiveReplayId is { } replayId && replayId != SentryId.Empty) + { + items.Add("replay_id", replayId.ToString()); + } + Items = items; } 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(); @@ -144,10 +161,19 @@ private DynamicSamplingContext( } items.Add("sample_rand", rand.ToString("N4", CultureInfo.InvariantCulture)); } + + if (replaySession?.ActiveReplayId is { } replayId) + { + // 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(); + } + 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; @@ -161,18 +187,18 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra var release = options.SettingLocator.GetRelease(); var environment = options.SettingLocator.GetEnvironment(); - return new DynamicSamplingContext( - traceId, + return new DynamicSamplingContext(traceId, publicKey, sampled, sampleRate, sampleRand, release, environment, - transactionName); + transactionName, + replaySession); } - 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; @@ -184,18 +210,20 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat publicKey, null, release: release, - environment: environment); + environment: environment, + replaySession: replaySession + ); } } internal static class DynamicSamplingContextExtensions { - public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage) - => DynamicSamplingContext.CreateFromBaggageHeader(baggage); + public static DynamicSamplingContext? CreateDynamicSamplingContext(this BaggageHeader baggage, IReplaySession? replaySession = null) + => 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 cc9cbd3ef3..ce896d2600 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) @@ -178,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. @@ -224,7 +226,7 @@ public BaggageHeader GetBaggage() } var propagationContext = CurrentScope.PropagationContext; - return propagationContext.GetOrCreateDynamicSamplingContext(_options).ToBaggageHeader(); + return propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession).ToBaggageHeader(); } public TransactionContext ContinueTrace( @@ -254,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( @@ -382,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); @@ -473,10 +475,7 @@ private SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Scope scope) var span = GetLinkedSpan(evt) ?? scope.Span; if (span is not null) { - if (span.IsSampled is not false) - { - ApplyTraceContextToEvent(evt, span); - } + ApplyTraceContextToEvent(evt, span); } else { diff --git a/src/Sentry/Internal/ReplaySession.cs b/src/Sentry/Internal/ReplaySession.cs new file mode 100644 index 0000000000..30f151c894 --- /dev/null +++ b/src/Sentry/Internal/ReplaySession.cs @@ -0,0 +1,33 @@ +#if __ANDROID__ +using Sentry.Android.Extensions; +#endif + +namespace Sentry.Internal; + +internal interface IReplaySession +{ + public SentryId? ActiveReplayId { get; } +} + +internal class ReplaySession : IReplaySession +{ + public static readonly IReplaySession Instance = new ReplaySession(); + + private ReplaySession() + { + } + + 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; +#else + return null; +#endif + } + } +} diff --git a/src/Sentry/SentryPropagationContext.cs b/src/Sentry/SentryPropagationContext.cs index 19e9bd30d3..24f9550638 100644 --- a/src/Sentry/SentryPropagationContext.cs +++ b/src/Sentry/SentryPropagationContext.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal; namespace Sentry; @@ -10,12 +11,12 @@ internal class SentryPropagationContext internal DynamicSamplingContext? _dynamicSamplingContext; - 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; @@ -47,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."); @@ -57,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/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs b/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs index 1b199c9fc4..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); } } @@ -287,7 +289,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/DynamicSamplingContextTests.cs b/test/Sentry.Tests/DynamicSamplingContextTests.cs index 374be0c371..a3fdb6a8ff 100644 --- a/test/Sentry.Tests/DynamicSamplingContextTests.cs +++ b/test/Sentry.Tests/DynamicSamplingContextTests.cs @@ -1,7 +1,27 @@ +using Sentry.Tests.Internals; + namespace Sentry.Tests; public class DynamicSamplingContextTests { + private 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); + } + } + + private Fixture _fixture = new(); + [Fact] public void EmptyContext() { @@ -19,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); } @@ -34,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); } @@ -49,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); } @@ -63,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); } @@ -78,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); } @@ -92,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); } @@ -107,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); } @@ -122,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); } @@ -137,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); } @@ -153,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); } @@ -169,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); } @@ -185,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); } @@ -200,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); @@ -223,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); @@ -252,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> { @@ -267,18 +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(4, 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); + 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> { @@ -290,10 +323,11 @@ 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(); + var dsc = baggage.CreateDynamicSamplingContext(replaySessionIsActive ? _fixture.ActiveReplaySession : _fixture.InactiveReplaySession); Assert.NotNull(dsc); Assert.Equal(baggage.Members.Count, dsc.Items.Count); @@ -306,6 +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)); + 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] @@ -320,10 +364,11 @@ 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", _fixture.ActiveReplayId.ToString()} }); - var dsc = original.CreateDynamicSamplingContext(); + var dsc = original.CreateDynamicSamplingContext(_fixture.ActiveReplaySession); var result = dsc?.ToBaggageHeader(); @@ -362,10 +407,10 @@ public void CreateFromTransaction(bool? isSampled) }, }; - var dsc = transaction.CreateDynamicSamplingContext(options); + var dsc = transaction.CreateDynamicSamplingContext(options, _fixture.ActiveReplaySession); 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,21 +426,34 @@ 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)); + // We add the replay_id automatically when we have an active replay session + 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 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)); + 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.verify.cs b/test/Sentry.Tests/EventProcessorTests.verify.cs index ea324e8283..6dd6394fd5 100644 --- a/test/Sentry.Tests/EventProcessorTests.verify.cs +++ b/test/Sentry.Tests/EventProcessorTests.verify.cs @@ -1,3 +1,5 @@ +using Sentry.Tests.Internals; + namespace Sentry.Tests; public class EventProcessorTests diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index cc9c510a57..efbca9830b 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1,5 +1,6 @@ using System.IO.Abstractions.TestingHelpers; using Sentry.Internal.Http; +using Sentry.Tests.Internals; namespace Sentry.Tests; @@ -10,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() { @@ -29,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(); @@ -173,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, @@ -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; @@ -209,8 +209,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 == transaction.TraceId && + evt.Contexts.Trace.SpanId == transaction.SpanId), Arg.Any(), Arg.Any()); } @@ -680,6 +680,82 @@ public void StartTransaction_SameInstrumenter_SampledIn() transaction.IsSampled.Should().BeTrue(); } + [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"}, + {"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(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 + var transaction = hub.StartTransaction(transactionContext, new Dictionary(), dsc); + + // Assert + var transactionTracer = ((TransactionTracer)transaction); + transactionTracer.IsSampled.Should().Be(true); + transactionTracer.DynamicSamplingContext.Should().NotBeNull(); + 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); + } + } + } + + [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 + var transaction = hub.StartTransaction(transactionContext, new Dictionary()); + + // Assert + var transactionTracer = ((TransactionTracer)transaction); + transactionTracer.SampleRand.Should().NotBeNull(); + transactionTracer.DynamicSamplingContext.Should().NotBeNull(); + 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] public void StartTransaction_NoDynamicSamplingContext_GeneratesSampleRand() { @@ -705,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); @@ -725,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"}, @@ -733,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); @@ -764,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(); @@ -788,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 c4f075dd00..7a85611425 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_UsesActiveSessionReplayIdInstead() + { + // 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" }, + { "sentry-sample_rand", "0.1234" }, + { "sentry-trace_id", "75302ac48a024bde9a3b3734a82e36c8" }, + { "sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff" }, + { "sentry-replay_id", "bfd31b89a59d41c99d96dc2baf840ecd" } + }); + 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"); + + // Act + var actualTransaction = new TransactionTracer(hub, transactionContext); + + // Assert + Assert.NotEqual(DateTimeOffset.MinValue, actualTransaction.StartTimestamp); + } + [Fact] public void Redact_Redacts_Urls() { diff --git a/test/Sentry.Tests/SentryPropagationContextTests.cs b/test/Sentry.Tests/SentryPropagationContextTests.cs index dc658d2ed9..bb90359d0a 100644 --- a/test/Sentry.Tests/SentryPropagationContextTests.cs +++ b/test/Sentry.Tests/SentryPropagationContextTests.cs @@ -2,50 +2,100 @@ namespace Sentry.Tests; public class SentryPropagationContextTests { - [Fact] - public void CopyConstructor_CreatesCopy() + private 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); + } + } + + private readonly Fixture _fixture = new(); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CopyConstructor_CreatesCopyWithReplayId(bool replaySessionIsActive) { var original = new SentryPropagationContext(); - original.GetOrCreateDynamicSamplingContext(new SentryOptions { Dsn = ValidDsn }); + original.GetOrCreateDynamicSamplingContext(new SentryOptions { Dsn = ValidDsn }, _fixture.InactiveReplaySession); var copy = new SentryPropagationContext(original); 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); + _ = 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); + var firstDynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(options, replaySessionIsActive ? _fixture.ActiveReplaySession : _fixture.InactiveReplaySession); - var secondDynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(options); + 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); + 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] @@ -53,15 +103,16 @@ 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.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> { @@ -71,14 +122,17 @@ 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); } - [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> { @@ -88,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); + // Act + var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, baggageHeader, replaySession); - Assert.Equal(5, propagationContext.GetOrCreateDynamicSamplingContext(new SentryOptions()).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)); + } } }