diff --git a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs index e53d2b20b2..af8f23bc21 100644 --- a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs +++ b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs @@ -7,6 +7,14 @@ var builder = WebApplication.CreateBuilder(args); +// Read the Sentry DSN from the environment variable if it's not already set in code. +#if SENTRY_DSN_DEFINED_IN_ENV +var dsn = Environment.GetEnvironmentVariable("SENTRY_DSN") + ?? throw new InvalidOperationException("SENTRY_DSN environment variable is not set"); +#else +var dsn = SamplesShared.Dsn; +#endif + // OpenTelemetry Configuration // See https://opentelemetry.io/docs/instrumentation/net/getting-started/ builder.Services.AddOpenTelemetry() @@ -20,8 +28,8 @@ // The two lines below take care of configuring sources for ASP.NET Core and HttpClient .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() - // Finally we configure OpenTelemetry to send traces to Sentry - .AddSentry() + // Finally, we configure OpenTelemetry over OTLP to send traces to Sentry + .AddSentryOtlp(dsn) ); builder.WebHost.UseSentry(options => @@ -35,7 +43,7 @@ options.Debug = builder.Environment.IsDevelopment(); options.SendDefaultPii = true; options.TracesSampleRate = 1.0; - options.UseOpenTelemetry(); // <-- Configure Sentry to use OpenTelemetry trace information + options.UseOtlp(); // <-- Configure Sentry to use OpenTelemetry trace information }); builder.Services diff --git a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj index a07956f015..4b2791c341 100644 --- a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj +++ b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Sentry.DiagnosticSource/Internal/DiagnosticSource/SentryDiagnosticListenerIntegration.cs b/src/Sentry.DiagnosticSource/Internal/DiagnosticSource/SentryDiagnosticListenerIntegration.cs index 39ced41aa5..45da25e6bc 100644 --- a/src/Sentry.DiagnosticSource/Internal/DiagnosticSource/SentryDiagnosticListenerIntegration.cs +++ b/src/Sentry.DiagnosticSource/Internal/DiagnosticSource/SentryDiagnosticListenerIntegration.cs @@ -3,7 +3,7 @@ namespace Sentry.Internal.DiagnosticSource; -internal class SentryDiagnosticListenerIntegration : ISdkIntegration +internal class SentryDiagnosticListenerIntegration : ISdkIntegration, ISentryTracingIntegration { public void Register(IHub hub, SentryOptions options) { diff --git a/src/Sentry.EntityFramework/DbInterceptionIntegration.cs b/src/Sentry.EntityFramework/DbInterceptionIntegration.cs index bf561ef039..a75b873625 100644 --- a/src/Sentry.EntityFramework/DbInterceptionIntegration.cs +++ b/src/Sentry.EntityFramework/DbInterceptionIntegration.cs @@ -1,6 +1,6 @@ namespace Sentry.EntityFramework; -internal class DbInterceptionIntegration : ISdkIntegration +internal class DbInterceptionIntegration : ISdkIntegration, ISentryTracingIntegration { // Internal for testing. internal IDbInterceptor? SqlInterceptor { get; private set; } diff --git a/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs b/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs index 2f22bcbbbd..eebe40e6d6 100644 --- a/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs +++ b/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs @@ -1,3 +1,4 @@ +using Sentry.Extensibility; using Sentry.Maui.CommunityToolkit.Mvvm; namespace Sentry.Maui; @@ -12,7 +13,14 @@ public static class SentryOptionsExtensions /// public static SentryMauiOptions AddCommunityToolkitIntegration(this SentryMauiOptions options) { - options.AddIntegrationEventBinder(); + if (options.DisableSentryTracing) + { + options.LogWarning("Skipping CommunityToolkit.Mvvm integration because OpenTelemetry is enabled."); + } + else + { + options.AddIntegrationEventBinder(); + } return options; } } diff --git a/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs b/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs index 4ab485dde1..a6423ef254 100644 --- a/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs +++ b/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal.OpenTelemetry; namespace Sentry.OpenTelemetry; diff --git a/src/Sentry.OpenTelemetry/OtelPropagationContext.cs b/src/Sentry.OpenTelemetry/OtelPropagationContext.cs new file mode 100644 index 0000000000..73839612ff --- /dev/null +++ b/src/Sentry.OpenTelemetry/OtelPropagationContext.cs @@ -0,0 +1,28 @@ +using Sentry.Extensibility; +using Sentry.Internal; + +namespace Sentry.OpenTelemetry; + +internal class OtelPropagationContext : IPropagationContext +{ + public DynamicSamplingContext? DynamicSamplingContext { get; private set; } + + public SentryId TraceId => Activity.Current?.TraceId.AsSentryId() ?? default; + public SpanId SpanId => Activity.Current?.SpanId.AsSentrySpanId() ?? default; + public SpanId? ParentSpanId => Activity.Current?.ParentSpanId.AsSentrySpanId(); + + /// + /// Warning: this method may throw an exception if Activity.Current is null. + /// This method should not be used when instrumenting with OTEL. + /// + 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, replaySession); + } + + return DynamicSamplingContext; + } +} diff --git a/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj b/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj index 6067738ff9..ef2d1fb369 100644 --- a/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj +++ b/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj @@ -5,6 +5,8 @@ $(PackageTags);OpenTelemetry $(CurrentTfms);netstandard2.1;netstandard2.0;net462 enable + + $(NoWarn);AD0001 @@ -17,7 +19,7 @@ - + diff --git a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs index 2035787a19..e98c39b782 100644 --- a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs +++ b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs @@ -9,11 +9,11 @@ namespace Sentry.OpenTelemetry; public static class SentryOptionsExtensions { /// - /// Enables OpenTelemetry instrumentation with Sentry + /// Configures Sentry to use OpenTelemetry for distributed tracing. /// - /// instance - /// - /// + /// The instance. + /// + /// /// The default TextMapPropagator to be used by OpenTelemetry. /// /// If this parameter is not supplied, the will be used, which propagates the @@ -24,34 +24,98 @@ public static class SentryOptionsExtensions /// could wrap this in a if you needed other propagators as well. /// /// - public static void UseOpenTelemetry( - this SentryOptions options, - TracerProviderBuilder traceProviderBuilder, - TextMapPropagator? defaultTextMapPropagator = null - ) + /// Whether to disable traces created using Sentry's tracing instrumentation. + /// It's recommended that you set this to true since mixing OpenTelemetry and Sentry traces may yield + /// unexpected results. It is false by default for backward compatibility only. + /// + /// + /// This method of initialising the Sentry OpenTelemetry integration will be depricated in a future major release. + /// We recommend you use instead. + /// + public static void UseOpenTelemetry(this SentryOptions options, TracerProviderBuilder builder, + TextMapPropagator? textMapPropagator = null, bool disableSentryTracing = false) + { + options.UseOpenTelemetry(disableSentryTracing); + builder.AddSentry(textMapPropagator); + } + + /// + /// Configures Sentry to use OpenTelemetry for distributed tracing. + /// + /// + /// Note: if you are using this overload to configure Sentry to work with OpenTelemetry, you will also have to call + /// when building your + /// to ensure OpenTelemetry sends trace information to Sentry. + /// + /// + /// The instance. + /// Whether to disable traces created using Sentry's tracing instrumentation. + /// It's recommended that you set this to true since mixing OpenTelemetry and Sentry traces may yield + /// unexpected results. It is false by default for backward compatibility only. + /// + /// + /// This method of initialising the Sentry OpenTelemetry integration will be depricated in a future major release. + /// We recommend you use instead. + /// + public static void UseOpenTelemetry(this SentryOptions options, bool disableSentryTracing = false) { options.Instrumenter = Instrumenter.OpenTelemetry; + options.DisableSentryTracing = disableSentryTracing; + options.PropagationContextFactory = _ => new OtelPropagationContext(); options.AddTransactionProcessor( new OpenTelemetryTransactionProcessor() - ); + ); + } - traceProviderBuilder.AddSentry(defaultTextMapPropagator); + /// + /// Configures Sentry to use OpenTelemetry for distributed tracing. Sentry instrumented traces will be + /// disabled (so all tracing instrumentation must be done using the OpenTelemetry classes). + /// + /// + /// This is the recommended way to set up Sentry's OpenTelemetry integration. + /// + /// + /// The instance. + /// + /// + /// The default TextMapPropagator to be used by OpenTelemetry. + /// + /// If this parameter is not supplied, the will be used, which propagates the + /// baggage header as well as Sentry trace headers. + /// + /// + /// The is required for Sentry's OpenTelemetry integration to work but you + /// could wrap this in a if you needed other propagators as well. + /// + /// + public static void UseOtlp(this SentryOptions options, TracerProviderBuilder builder, TextMapPropagator? textMapPropagator = null) + { + if (string.IsNullOrWhiteSpace(options.Dsn)) + { + throw new ArgumentException("Sentry DSN must be set before calling `SentryOptions.UseOTLP`", nameof(options.Dsn)); + } + builder.AddSentryOtlp(options.Dsn, textMapPropagator); + options.UseOtlp(); } /// - /// Configures Sentry to use OpenTelemetry for distributed tracing. + /// Configures Sentry to use OpenTelemetry for distributed tracing. Sentry instrumented traces will be + /// disabled (so all tracing instrumentation must be done using the OpenTelemetry classes). + /// /// - /// Note: if you are using this method to configure Sentry to work with OpenTelemetry you will also have to call - /// when building your - /// to ensure OpenTelemetry sends trace information to Sentry. + /// This is the recommended way to set up Sentry's OpenTelemetry integration. /// /// - /// instance - public static void UseOpenTelemetry(this SentryOptions options) + /// + /// Note: if you are using this overload to configure Sentry to work with OpenTelemetry, you will also have to call + /// , when building your + /// to ensure OpenTelemetry sends trace information to Sentry. + /// + /// The instance. + public static void UseOtlp(this SentryOptions options) { options.Instrumenter = Instrumenter.OpenTelemetry; - options.AddTransactionProcessor( - new OpenTelemetryTransactionProcessor() - ); + options.DisableSentryTracing = true; + options.PropagationContextFactory = _ => new OtelPropagationContext(); } } diff --git a/src/Sentry.OpenTelemetry/SentryPropagator.cs b/src/Sentry.OpenTelemetry/SentryPropagator.cs index 78fd960d6b..f8b4696c21 100644 --- a/src/Sentry.OpenTelemetry/SentryPropagator.cs +++ b/src/Sentry.OpenTelemetry/SentryPropagator.cs @@ -2,6 +2,7 @@ using OpenTelemetry; using OpenTelemetry.Context.Propagation; using Sentry.Extensibility; +using Sentry.Internal.OpenTelemetry; namespace Sentry.OpenTelemetry; @@ -54,7 +55,7 @@ public override PropagationContext Extract(PropagationContext context, T carr Options?.LogDebug("SentryPropagator.Extract"); var result = base.Extract(context, carrier, getter); - var baggage = result.Baggage; // The Otel .NET SDK takes care of baggage headers alread + var baggage = result.Baggage; // The Otel .NET SDK takes care of baggage headers already Options?.LogDebug("Baggage"); foreach (var entry in baggage) diff --git a/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs index 96472eda98..c57d23003f 100644 --- a/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs +++ b/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using OpenTelemetry; using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Exporter; using OpenTelemetry.Trace; using Sentry.Extensibility; @@ -11,10 +12,22 @@ namespace Sentry.OpenTelemetry; /// public static class TracerProviderBuilderExtensions { + internal const string MissingDsnWarning = "Invalid DSN passed to AddSentryOTLP"; + /// - /// Ensures OpenTelemetry trace information is sent to Sentry. + /// + /// Ensures OpenTelemetry trace information is sent to Sentry. OpenTelemetry spans will be converted to Sentry spans + /// using a span processor. This is no longer recommended. SDK users should consider using + /// instead, which is the recommended + /// way to send OpenTelemetry trace information to Sentry moving forward. + /// + /// + /// Note that if you use this method to configure the trace builder, you will also need to call + /// when initialising Sentry, for Sentry + /// to work properly with OpenTelemetry. + /// /// - /// . + /// The . /// /// The default TextMapPropagator to be used by OpenTelemetry. /// @@ -27,7 +40,8 @@ public static class TracerProviderBuilderExtensions /// /// /// The supplied for chaining. - public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerProviderBuilder, TextMapPropagator? defaultTextMapPropagator = null) + public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerProviderBuilder, + TextMapPropagator? defaultTextMapPropagator = null) { defaultTextMapPropagator ??= new SentryPropagator(); Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator); @@ -36,7 +50,7 @@ public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerP internal static BaseProcessor ImplementationFactory(IServiceProvider services) { - List enrichers = new(); + List enrichers = []; // AspNetCoreEnricher var userFactory = services.GetService(); @@ -55,4 +69,56 @@ internal static BaseProcessor ImplementationFactory(IServiceProvider s logger?.LogWarning("Sentry is disabled so no OpenTelemetry spans will be sent to Sentry."); return DisabledSpanProcessor.Instance; } + + /// + /// + /// Ensures OpenTelemetry trace information is sent to the Sentry OTLP endpoint. + /// + /// + /// Note that if you use this method to configure the trace builder, you will also need to call + /// when initialising Sentry, for Sentry to work + /// properly with OpenTelemetry. + /// + /// + /// The . + /// The DSN for your Sentry project + /// + /// The default TextMapPropagator to be used by OpenTelemetry. + /// + /// If this parameter is not supplied, the will be used, which propagates the + /// baggage header as well as Sentry trace headers. + /// + /// + /// The is required for Sentry's OpenTelemetry integration to work but you + /// could wrap this in a if you needed other propagators as well. + /// + /// + /// The supplied for chaining. + public static TracerProviderBuilder AddSentryOtlp(this TracerProviderBuilder tracerProviderBuilder, string dsnString, + TextMapPropagator? defaultTextMapPropagator = null) + { + if (Dsn.TryParse(dsnString) is not { } dsn) + { + throw new ArgumentException("Invalid DSN passed to AddSentryOTLP", nameof(dsnString)); + } + + defaultTextMapPropagator ??= new SentryPropagator(); + Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator); + + tracerProviderBuilder.AddOtlpExporter(options => OtlpConfigurationCallback(options, dsn)); + return tracerProviderBuilder; + } + + // Internal helper method for testing purposes + internal static void OtlpConfigurationCallback(OtlpExporterOptions options, Dsn dsn) + { + options.Endpoint = dsn.GetOtlpTracesEndpointUri(); + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.HttpClientFactory = () => + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-Sentry-Auth", $"sentry sentry_key={dsn.PublicKey}"); + return client; + }; + } } diff --git a/src/Sentry/Dsn.cs b/src/Sentry/Dsn.cs index 011af39169..a132d89945 100644 --- a/src/Sentry/Dsn.cs +++ b/src/Sentry/Dsn.cs @@ -58,6 +58,8 @@ private Dsn( public Uri GetEnvelopeEndpointUri() => new(ApiBaseUri, "envelope/"); + public Uri GetOtlpTracesEndpointUri() => new(ApiBaseUri, "integration/otlp/v1/traces"); + public override string ToString() => Source; public static bool IsDisabled(string? dsn) => diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs index f39d7fa5a7..3f9b82ee0e 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -1,5 +1,4 @@ using Sentry.Internal; -using Sentry.Internal.Extensions; namespace Sentry; @@ -228,7 +227,13 @@ public static DynamicSamplingContext CreateFromUnsampledTransaction(UnsampledTra replaySession); } - public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) + /// + /// Creates a from the given . + /// + /// Can be thrown when using OpenTelemetry instrumentation and + /// System.Diagnostics.Activity.Current is null. This method should not be used when instrumenting with OTEL. + /// + public static DynamicSamplingContext CreateFromPropagationContext(IPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) { var traceId = propagationContext.TraceId; var publicKey = options.ParsedDsn.PublicKey; @@ -257,6 +262,12 @@ public static DynamicSamplingContext CreateDynamicSamplingContext(this Transacti public static DynamicSamplingContext CreateDynamicSamplingContext(this UnsampledTransaction transaction, SentryOptions options, IReplaySession? replaySession) => DynamicSamplingContext.CreateFromUnsampledTransaction(transaction, options, replaySession); - public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) + /// + /// Creates a from the given . + /// + /// Can be thrown when using OpenTelemetry instrumentation and + /// System.Diagnostics.Activity.Current is null. This method should not be used when instrumenting with OTEL. + /// + public static DynamicSamplingContext CreateDynamicSamplingContext(this IPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) => DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options, replaySession); } diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index a5a7afd9c9..c42a1f784d 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -70,6 +70,9 @@ public ITransactionTracer StartTransaction( /// /// Gets the Sentry baggage header that allows tracing across services /// + /// Can be thrown when using OpenTelemetry instrumentation and + /// System.Diagnostics.Activity.Current is null. This method should not be used when instrumenting with OTEL. + /// public BaggageHeader? GetBaggage(); /// diff --git a/src/Sentry/Integrations/ISdkIntegration.cs b/src/Sentry/Integrations/ISdkIntegration.cs index 898e5b39db..1a1eadc558 100644 --- a/src/Sentry/Integrations/ISdkIntegration.cs +++ b/src/Sentry/Integrations/ISdkIntegration.cs @@ -15,3 +15,11 @@ public interface ISdkIntegration /// The options. public void Register(IHub hub, SentryOptions options); } + +/// +/// Marker interface to indicate that an integration provides native Sentry tracing capabilities. We do NOT initialise +/// these integrations when using OTEL instrumentation. +/// +internal interface ISentryTracingIntegration +{ +} diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index be71b2f1d3..def0ca82d1 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -185,6 +185,12 @@ internal ITransactionTracer StartTransaction( return NoOpTransaction.Instance; } + if (_options.DisableSentryTracing) + { + _options.LogWarning("Sentry transaction dropped because OpenTelemetry is enabled"); + return NoOpTransaction.Instance; + } + bool? isSampled = null; double? sampleRate = null; DiscardReason? discardReason = null; @@ -306,6 +312,11 @@ public SentryTraceHeader GetTraceHeader() public BaggageHeader GetBaggage() { + if (_options.Instrumenter is Instrumenter.OpenTelemetry) + { + _options.LogWarning("GetBaggage should not be called when using OpenTelemetry - it may throw an exception"); + } + var span = GetSpan(); if (span?.GetTransaction().GetDynamicSamplingContext() is { IsEmpty: false } dsc) { @@ -478,12 +489,15 @@ private void ApplyTraceContextToEvent(SentryEvent evt, ISpan span) } } - private void ApplyTraceContextToEvent(SentryEvent evt, SentryPropagationContext propagationContext) + private void ApplyTraceContextToEvent(SentryEvent evt, IPropagationContext propagationContext) { evt.Contexts.Trace.TraceId = propagationContext.TraceId; evt.Contexts.Trace.SpanId = propagationContext.SpanId; evt.Contexts.Trace.ParentSpanId = propagationContext.ParentSpanId; - evt.DynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession); + if (_options.Instrumenter is Instrumenter.Sentry) + { + evt.DynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession); + } } public bool CaptureEnvelope(Envelope envelope) => CurrentClient.CaptureEnvelope(envelope); diff --git a/src/Sentry/Internal/IPropagationContext.cs b/src/Sentry/Internal/IPropagationContext.cs new file mode 100644 index 0000000000..00f2c781af --- /dev/null +++ b/src/Sentry/Internal/IPropagationContext.cs @@ -0,0 +1,12 @@ +namespace Sentry.Internal; + +internal interface IPropagationContext +{ + public DynamicSamplingContext? DynamicSamplingContext { get; } + + public SentryId TraceId { get; } + public SpanId SpanId { get; } + public SpanId? ParentSpanId { get; } + + public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options, IReplaySession replaySession); +} diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index 04fab75ff1..4638a4255a 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; using Sentry.Extensibility; using Sentry.Internal; using Sentry.Internal.Extensions; +using Sentry.Internal.OpenTelemetry; namespace Sentry; @@ -249,7 +245,7 @@ public ITransactionTracer? Transaction } } - internal SentryPropagationContext PropagationContext { get; private set; } + internal IPropagationContext PropagationContext { get; private set; } internal SessionUpdate? SessionUpdate { get; set; } @@ -297,10 +293,10 @@ public Scope(SentryOptions? options) { } - internal Scope(SentryOptions? options, SentryPropagationContext? propagationContext) + internal Scope(SentryOptions? options, IPropagationContext? propagationContext) { Options = options ?? new SentryOptions(); - PropagationContext = new SentryPropagationContext(propagationContext); + PropagationContext = Options.PropagationContextFactory(propagationContext); } // For testing. Should explicitly require SentryOptions. @@ -420,7 +416,7 @@ public void Clear() _extra.Clear(); _tags.Clear(); ClearAttachments(); - PropagationContext = new(); + PropagationContext = Options.PropagationContextFactory(null); } /// diff --git a/src/Sentry/SentryGraphQLHttpMessageHandler.cs b/src/Sentry/SentryGraphQLHttpMessageHandler.cs index 7666e46455..229ddafd5b 100644 --- a/src/Sentry/SentryGraphQLHttpMessageHandler.cs +++ b/src/Sentry/SentryGraphQLHttpMessageHandler.cs @@ -50,6 +50,12 @@ internal SentryGraphQLHttpMessageHandler(IHub? hub, SentryOptions? options, } request.SetFused(graphQlRequestContent); + if (_options?.DisableSentryTracing ?? false) + { + _options.LogDebug("Skipping span creation in SentryGraphQLHttpMessageHandler because OpenTelemetry is enabled"); + return null; + } + // Start a span that tracks this request // (may be null if transaction is not set on the scope) var span = _hub.GetSpan()?.StartChild( diff --git a/src/Sentry/SentryHttpMessageHandler.cs b/src/Sentry/SentryHttpMessageHandler.cs index f75f958b8a..eb9d62f8f2 100644 --- a/src/Sentry/SentryHttpMessageHandler.cs +++ b/src/Sentry/SentryHttpMessageHandler.cs @@ -65,6 +65,12 @@ internal SentryHttpMessageHandler(IHub? hub, SentryOptions? options, HttpMessage /// protected internal override ISpan? ProcessRequest(HttpRequestMessage request, string method, string url) { + if (_options?.DisableSentryTracing ?? false) + { + _options.LogDebug("Skipping span creation in SentryHttpMessageHandler because OpenTelemetry is enabled"); + return null; + } + // Start a span that tracks this request // (may be null if transaction is not set on the scope) var span = _hub.GetSpan()?.StartChild( diff --git a/src/Sentry/SentryMessageHandler.cs b/src/Sentry/SentryMessageHandler.cs index 9c8515e93e..72c0247a08 100644 --- a/src/Sentry/SentryMessageHandler.cs +++ b/src/Sentry/SentryMessageHandler.cs @@ -133,6 +133,15 @@ private void PropagateTraceHeaders(HttpRequestMessage request, string url, ISpan } } + // We only propogate trace headers for Sentry's native intstumentation. It isn't possible to propogate + // headers when OTEL instrumentation is used since the traceId can be SentryId.Empty if there is no active + // OTEL span... which would result in an exception being thrown when trying to create the + // DynamicSamplingContext. + if (_options?.Instrumenter is Instrumenter.Sentry) + { + return; + } + if (_options?.TracePropagationTargets.MatchesSubstringOrRegex(url) is true or null) { AddSentryTraceHeader(request, parentSpan); diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 7fc69a600f..bedcc39bf1 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -206,7 +206,7 @@ internal IEnumerable Integrations #endif #if HAS_DIAGNOSTIC_INTEGRATION - if ((_defaultIntegrations & DefaultIntegrations.SentryDiagnosticListenerIntegration) != 0) + if (!DisableSentryTracing && (_defaultIntegrations & DefaultIntegrations.SentryDiagnosticListenerIntegration) != 0) { yield return new SentryDiagnosticListenerIntegration(); } @@ -222,6 +222,10 @@ internal IEnumerable Integrations foreach (var integration in _integrations) { + if (DisableSentryTracing && integration is ISentryTracingIntegration) + { + continue; + } yield return integration; } } @@ -1156,6 +1160,20 @@ public StackTraceMode StackTraceMode /// internal Instrumenter Instrumenter { get; set; } = Instrumenter.Sentry; + /// + /// During the transition period to OTLP we give SDK users the option to keep using Sentry's tracing in conjunction + /// with OTEL instrumentation. Setting this to true will disable Sentry's tracing entirely, which is the recommended + /// setting but would be a moajor change in behaviour, so we've made it opt-in for now. + /// TODO: Remove this option in a future major release and make it true / non-optional when using OTEL (i.e. implied by the Instrumenter) + /// + internal bool DisableSentryTracing { get; set; } = false; + + /// + /// The default factory creates SentryPropagationContext instances... this should be replaced when using OTEL + /// + internal Func PropagationContextFactory { get; set; } = sourceContext => + new SentryPropagationContext(sourceContext); + /// /// /// Set to `true` to prevents Sentry from automatically registering . diff --git a/src/Sentry/SentryPropagationContext.cs b/src/Sentry/SentryPropagationContext.cs index 6183262241..2ae4adba09 100644 --- a/src/Sentry/SentryPropagationContext.cs +++ b/src/Sentry/SentryPropagationContext.cs @@ -3,23 +3,23 @@ namespace Sentry; -internal class SentryPropagationContext +internal class SentryPropagationContext : IPropagationContext { public SentryId TraceId { get; } public SpanId SpanId { get; } public SpanId? ParentSpanId { get; } - internal DynamicSamplingContext? _dynamicSamplingContext; + public DynamicSamplingContext? DynamicSamplingContext { get; private set; } public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options, IReplaySession replaySession) { - if (_dynamicSamplingContext is null) + if (DynamicSamplingContext is null) { options.LogDebug("Creating the Dynamic Sampling Context from the Propagation Context."); - _dynamicSamplingContext = this.CreateDynamicSamplingContext(options, replaySession); + DynamicSamplingContext = this.CreateDynamicSamplingContext(options, replaySession); } - return _dynamicSamplingContext; + return DynamicSamplingContext; } internal SentryPropagationContext( @@ -30,7 +30,7 @@ internal SentryPropagationContext( TraceId = traceId; SpanId = SpanId.Create(); ParentSpanId = parentSpanId; - _dynamicSamplingContext = dynamicSamplingContext; + DynamicSamplingContext = dynamicSamplingContext; } public SentryPropagationContext() @@ -39,13 +39,13 @@ public SentryPropagationContext() SpanId = SpanId.Create(); } - public SentryPropagationContext(SentryPropagationContext? other) + public SentryPropagationContext(IPropagationContext? other) { TraceId = other?.TraceId ?? SentryId.Create(); SpanId = other?.SpanId ?? SpanId.Create(); ParentSpanId = other?.ParentSpanId; - _dynamicSamplingContext = other?._dynamicSamplingContext; + DynamicSamplingContext = other?.DynamicSamplingContext; } public static SentryPropagationContext CreateFromHeaders(IDiagnosticLogger? logger, SentryTraceHeader? traceHeader, BaggageHeader? baggageHeader, IReplaySession replaySession) diff --git a/test/Sentry.OpenTelemetry.Tests/OpenTelemetryExtensionsTests.cs b/test/Sentry.OpenTelemetry.Tests/OpenTelemetryExtensionsTests.cs index 636a54f9fb..580002204d 100644 --- a/test/Sentry.OpenTelemetry.Tests/OpenTelemetryExtensionsTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/OpenTelemetryExtensionsTests.cs @@ -1,3 +1,5 @@ +using Sentry.Internal.OpenTelemetry; + namespace Sentry.OpenTelemetry.Tests; public class OpenTelemetryExtensionsTests diff --git a/test/Sentry.OpenTelemetry.Tests/OpenTelemetryTransactionProcessorTests.cs b/test/Sentry.OpenTelemetry.Tests/OpenTelemetryTransactionProcessorTests.cs index 714caf1cb7..cffe5eb6c4 100644 --- a/test/Sentry.OpenTelemetry.Tests/OpenTelemetryTransactionProcessorTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/OpenTelemetryTransactionProcessorTests.cs @@ -1,3 +1,5 @@ +using Sentry.Internal.OpenTelemetry; + namespace Sentry.OpenTelemetry.Tests; public class OpenTelemetryTransactionProcessorTests : ActivitySourceTests diff --git a/test/Sentry.OpenTelemetry.Tests/OtelPropagationContextTests.cs b/test/Sentry.OpenTelemetry.Tests/OtelPropagationContextTests.cs new file mode 100644 index 0000000000..1d12319f12 --- /dev/null +++ b/test/Sentry.OpenTelemetry.Tests/OtelPropagationContextTests.cs @@ -0,0 +1,167 @@ +namespace Sentry.OpenTelemetry.Tests; + +public class OtelPropagationContextTests +{ + private class Fixture + { + public SentryId ActiveReplayId { get; } = SentryId.Create(); + public IReplaySession ActiveReplaySession { get; } + public SentryOptions SentryOptions { get; } + + public Fixture() + { + ActiveReplaySession = Substitute.For(); + ActiveReplaySession.ActiveReplayId.Returns(ActiveReplayId); + + SentryOptions = new SentryOptions { Dsn = "https://examplePublicKey@o0.ingest.sentry.io/123456" }; + } + } + + private readonly Fixture _fixture = new(); + + [Fact] + public void TraceId_NoActivityCurrent_ReturnsDefault() + { + // Arrange + var sut = new OtelPropagationContext(); + Activity.Current = null; + + // Act + var traceId = sut.TraceId; + + // Assert + traceId.Should().Be(default(SentryId)); + } + + [Fact] + public void TraceId_WithActivityCurrent_ReturnsSentryIdFromActivityTraceId() + { + // Arrange + using var activity = new Activity("test").Start(); + var sut = new OtelPropagationContext(); + + // Act + var traceId = sut.TraceId; + + // Assert + traceId.Should().NotBe(default(SentryId)); + traceId.Should().Be(activity.TraceId.AsSentryId()); + } + + [Fact] + public void SpanId_NoActivityCurrent_ReturnsDefault() + { + // Arrange + var sut = new OtelPropagationContext(); + Activity.Current = null; + + // Act + var spanId = sut.SpanId; + + // Assert + spanId.Should().Be(default(SpanId)); + } + + [Fact] + public void SpanId_WithActivityCurrent_ReturnsSpanIdFromActivitySpanId() + { + // Arrange + using var activity = new Activity("test").Start(); + var sut = new OtelPropagationContext(); + + // Act + var spanId = sut.SpanId; + + // Assert + spanId.Should().NotBe(default(SpanId)); + spanId.Should().Be(activity.SpanId.AsSentrySpanId()); + } + + [Fact] + public void ParentSpanId_NoActivityCurrent_ReturnsNull() + { + // Arrange + var sut = new OtelPropagationContext(); + Activity.Current = null; + + // Act + var parentSpanId = sut.ParentSpanId; + + // Assert + parentSpanId.Should().BeNull(); + } + + [Fact] + public void ParentSpanId_WithActivityCurrent_ReturnsParentSpanIdFromActivity() + { + // Arrange + using var parentActivity = new Activity("parent").Start(); + using var childActivity = new Activity("child").Start(); + var sut = new OtelPropagationContext(); + + // Act + var parentSpanId = sut.ParentSpanId; + + // Assert + parentSpanId.Should().NotBeNull(); + parentSpanId.Should().Be(parentActivity.SpanId.AsSentrySpanId()); + } + + [Fact] + public void DynamicSamplingContext_ByDefault_IsNull() + { + // Arrange & Act + var sut = new OtelPropagationContext(); + + // Assert + sut.DynamicSamplingContext.Should().BeNull(); + } + + [Fact] + public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNull_CreatesDynamicSamplingContext() + { + // Arrange + using var activity = new Activity("test").Start(); + var sut = new OtelPropagationContext(); + sut.DynamicSamplingContext.Should().BeNull(); + + // Act + var result = sut.GetOrCreateDynamicSamplingContext(_fixture.SentryOptions, _fixture.ActiveReplaySession); + + // Assert + result.Should().NotBeNull(); + sut.DynamicSamplingContext.Should().NotBeNull(); + sut.DynamicSamplingContext.Should().BeSameAs(result); + } + + [Fact] + public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNotNull_ReturnsSameDynamicSamplingContext() + { + // Arrange + using var activity = new Activity("test").Start(); + var sut = new OtelPropagationContext(); + var firstResult = sut.GetOrCreateDynamicSamplingContext(_fixture.SentryOptions, _fixture.ActiveReplaySession); + + // Act + var secondResult = sut.GetOrCreateDynamicSamplingContext(_fixture.SentryOptions, _fixture.ActiveReplaySession); + + // Assert + firstResult.Should().BeSameAs(secondResult); + sut.DynamicSamplingContext.Should().BeSameAs(firstResult); + } + + [Fact] + public void GetOrCreateDynamicSamplingContext_WithActiveReplaySession_IncludesReplayIdInDynamicSamplingContext() + { + // Arrange + using var activity = new Activity("test").Start(); + var sut = new OtelPropagationContext(); + + // Act + var result = sut.GetOrCreateDynamicSamplingContext(_fixture.SentryOptions, _fixture.ActiveReplaySession); + + // Assert + result.Should().NotBeNull(); + result.Items.Should().Contain(kvp => kvp.Key == "replay_id" && kvp.Value == _fixture.ActiveReplayId.ToString()); + } +} diff --git a/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs b/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs index 693619aece..cdd24560a3 100644 --- a/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Primitives; using OpenTelemetry; using OpenTelemetry.Context.Propagation; +using Sentry.Internal.OpenTelemetry; namespace Sentry.OpenTelemetry.Tests; diff --git a/test/Sentry.OpenTelemetry.Tests/TracerProviderBuilderExtensionsTests.cs b/test/Sentry.OpenTelemetry.Tests/TracerProviderBuilderExtensionsTests.cs index d63f64c985..89e747432c 100644 --- a/test/Sentry.OpenTelemetry.Tests/TracerProviderBuilderExtensionsTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/TracerProviderBuilderExtensionsTests.cs @@ -1,3 +1,6 @@ +using OpenTelemetry.Exporter; +using OpenTelemetry.Trace; + namespace Sentry.OpenTelemetry.Tests; public class TracerProviderBuilderExtensionsTests @@ -91,4 +94,66 @@ public void ImplementationFactory_WithDisabledHub_ReturnsDisabledSpanProcessor() // Assert result.Should().BeOfType(); // FluentAssertions } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("foo")] + public void AddSentryOltp_InvalidDsn_ThrowsArgumentException(string dsn) + { + // Arrange + var tracerProviderBuilder = Substitute.For(); + + // Act + Action act = () => tracerProviderBuilder.AddSentryOtlp(dsn); + + // Assert + act.Should().Throw() + .WithMessage($"{TracerProviderBuilderExtensions.MissingDsnWarning}*"); + } + + [Fact] + public void OtlpConfigurationCallback_SetsEndpointFromDsn() + { + // Arrange + var dsn = Dsn.Parse("https://examplePublicKey@o0.ingest.sentry.io/123456"); + var options = new OtlpExporterOptions(); + + // Act + TracerProviderBuilderExtensions.OtlpConfigurationCallback(options, dsn); + + // Assert + options.Endpoint.Should().Be(dsn.GetOtlpTracesEndpointUri()); + } + + [Fact] + public void OtlpConfigurationCallback_SetsProtocolToHttpProtobuf() + { + // Arrange + var dsn = Dsn.Parse("https://examplePublicKey@o0.ingest.sentry.io/123456"); + var options = new OtlpExporterOptions(); + + // Act + TracerProviderBuilderExtensions.OtlpConfigurationCallback(options, dsn); + + // Assert + options.Protocol.Should().Be(OtlpExportProtocol.HttpProtobuf); + } + + [Fact] + public void OtlpConfigurationCallback_HttpClientFactoryCreatesClientWithSentryAuthHeader() + { + // Arrange + var dsn = Dsn.Parse("https://examplePublicKey@o0.ingest.sentry.io/123456"); + var options = new OtlpExporterOptions(); + + // Act + TracerProviderBuilderExtensions.OtlpConfigurationCallback(options, dsn); + var client = options.HttpClientFactory!.Invoke(); + + // Assert + client.DefaultRequestHeaders.Should().Contain(h => h.Key == "X-Sentry-Auth"); + var headerValues = client.DefaultRequestHeaders.GetValues("X-Sentry-Auth"); + headerValues.Should().ContainSingle(v => v == $"sentry sentry_key={dsn.PublicKey}"); + } } diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index c8bb6ea702..11171d8e0c 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1510,8 +1510,8 @@ public void ContinueTrace_ReceivesHeaders_SetsPropagationContextAndReturnsTransa { scope.PropagationContext.TraceId.Should().Be(SentryId.Parse("5bd5f6d346b442dd9177dce9302fd737")); scope.PropagationContext.ParentSpanId.Should().Be(SpanId.Parse("2000000000000000")); - Assert.NotNull(scope.PropagationContext._dynamicSamplingContext); - scope.PropagationContext._dynamicSamplingContext.Items.Should().Contain(baggageHeader.GetSentryMembers()); + Assert.NotNull(scope.PropagationContext.DynamicSamplingContext); + scope.PropagationContext.DynamicSamplingContext.Items.Should().Contain(baggageHeader.GetSentryMembers()); }); transactionContext.TraceId.Should().Be(SentryId.Parse("5bd5f6d346b442dd9177dce9302fd737")); @@ -1531,7 +1531,7 @@ public void ContinueTrace_DoesNotReceiveHeaders_CreatesRootTrace() hub.ScopeManager.ConfigureScope(scope => { Assert.Null(scope.PropagationContext.ParentSpanId); - Assert.Null(scope.PropagationContext._dynamicSamplingContext); + Assert.Null(scope.PropagationContext.DynamicSamplingContext); }); transactionContext.Name.Should().Be("test-name"); @@ -1564,8 +1564,8 @@ public void ContinueTrace_ReceivesHeadersAsStrings_SetsPropagationContextAndRetu { scope.PropagationContext.TraceId.Should().Be(SentryId.Parse("5bd5f6d346b442dd9177dce9302fd737")); scope.PropagationContext.ParentSpanId.Should().Be(SpanId.Parse("2000000000000000")); - Assert.NotNull(scope.PropagationContext._dynamicSamplingContext); - scope.PropagationContext._dynamicSamplingContext.ToBaggageHeader().Members.Should().Contain(BaggageHeader.TryParse(baggageHeader)!.Members); + Assert.NotNull(scope.PropagationContext.DynamicSamplingContext); + scope.PropagationContext.DynamicSamplingContext.ToBaggageHeader().Members.Should().Contain(BaggageHeader.TryParse(baggageHeader)!.Members); }); transactionContext.TraceId.Should().Be(SentryId.Parse("5bd5f6d346b442dd9177dce9302fd737")); @@ -1585,7 +1585,7 @@ public void ContinueTrace_DoesNotReceiveHeadersAsStrings_CreatesRootTrace() hub.ScopeManager.ConfigureScope(scope => { Assert.Null(scope.PropagationContext.ParentSpanId); - Assert.Null(scope.PropagationContext._dynamicSamplingContext); + Assert.Null(scope.PropagationContext.DynamicSamplingContext); }); transactionContext.Name.Should().Be("test-name"); diff --git a/test/Sentry.Tests/SentryPropagationContextTests.cs b/test/Sentry.Tests/SentryPropagationContextTests.cs index bb90359d0a..fa307db89a 100644 --- a/test/Sentry.Tests/SentryPropagationContextTests.cs +++ b/test/Sentry.Tests/SentryPropagationContextTests.cs @@ -32,12 +32,12 @@ public void CopyConstructor_CreatesCopyWithReplayId(bool replaySessionIsActive) Assert.Equal(original.TraceId, copy.TraceId); Assert.Equal(original.SpanId, copy.SpanId); - Assert.Equal(original._dynamicSamplingContext!.Items.Count, copy._dynamicSamplingContext!.Items.Count); - foreach (var dscItem in original._dynamicSamplingContext!.Items) + 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 + 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 @@ -45,7 +45,7 @@ public void CopyConstructor_CreatesCopyWithReplayId(bool replaySessionIsActive) } else { - copy._dynamicSamplingContext!.Items.Should() + copy.DynamicSamplingContext!.Items.Should() .Contain(kvp => kvp.Key == dscItem.Key && kvp.Value == dscItem.Value); } } @@ -59,18 +59,18 @@ public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNull_Creat var options = new SentryOptions { Dsn = ValidDsn }; var propagationContext = new SentryPropagationContext(); - Assert.Null(propagationContext._dynamicSamplingContext); // Sanity check + Assert.Null(propagationContext.DynamicSamplingContext); // Sanity check _ = propagationContext.GetOrCreateDynamicSamplingContext(options, replaySessionIsActive ? _fixture.ActiveReplaySession : _fixture.InactiveReplaySession); - Assert.NotNull(propagationContext._dynamicSamplingContext); + 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)); + Assert.Equal(_fixture.ActiveReplayId.ToString(), Assert.Contains("replay_id", propagationContext.DynamicSamplingContext.Items)); } else { - Assert.DoesNotContain("replay_id", propagationContext._dynamicSamplingContext.Items); + Assert.DoesNotContain("replay_id", propagationContext.DynamicSamplingContext.Items); } } @@ -95,7 +95,7 @@ public void CreateFromHeaders_HeadersNull_CreatesPropagationContextWithTraceAndS Assert.NotEqual(propagationContext.TraceId, SentryId.Empty); Assert.NotEqual(propagationContext.SpanId, SpanId.Empty); - Assert.Null(propagationContext._dynamicSamplingContext); + Assert.Null(propagationContext.DynamicSamplingContext); } [Fact] @@ -108,7 +108,7 @@ public void CreateFromHeaders_TraceHeaderNotNull_CreatesPropagationContextFromTr Assert.Equal(traceHeader.TraceId, propagationContext.TraceId); Assert.NotEqual(traceHeader.SpanId, propagationContext.SpanId); // Sanity check Assert.Equal(traceHeader.SpanId, propagationContext.ParentSpanId); - Assert.Null(propagationContext._dynamicSamplingContext); + Assert.Null(propagationContext.DynamicSamplingContext); } [Fact] @@ -124,7 +124,7 @@ public void CreateFromHeaders_BaggageExistsButTraceHeaderNull_CreatesPropagation var propagationContext = SentryPropagationContext.CreateFromHeaders(null, null, baggageHeader, _fixture.InactiveReplaySession); - Assert.Null(propagationContext._dynamicSamplingContext); + Assert.Null(propagationContext.DynamicSamplingContext); } [Theory]