diff --git a/CHANGELOG.md b/CHANGELOG.md index cc97e73d36..5cef687a66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Reduced memory pressure when sampling less than 100% of traces/transactions ([#4212](https://github.com/getsentry/sentry-dotnet/pull/4212)) + ### Fixes - Support Linux arm64 on Native AOT ([#3700](https://github.com/getsentry/sentry-dotnet/pull/3700)) diff --git a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs index d878e1c00a..8c192a89a1 100644 --- a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs @@ -146,7 +146,11 @@ public async Task InvokeAsync(HttpContext context) } finally { - if (transaction is not null) + if (transaction is UnsampledTransaction) + { + transaction.Finish(); + } + else if (transaction is TransactionTracer tracer) { // The Transaction name was altered during the pipeline execution, // That could be done by user interference or by some Event Capture @@ -183,7 +187,7 @@ public async Task InvokeAsync(HttpContext context) if (!string.IsNullOrEmpty(customTransactionName)) { transaction.Name = $"{method} {customTransactionName}"; - ((TransactionTracer)transaction).NameSource = TransactionNameSource.Custom; + tracer.NameSource = TransactionNameSource.Custom; } else { @@ -191,7 +195,7 @@ public async Task InvokeAsync(HttpContext context) // e.g. "GET /pets/1" var path = context.Request.Path; transaction.Name = $"{method} {path}"; - ((TransactionTracer)transaction).NameSource = TransactionNameSource.Url; + tracer.NameSource = TransactionNameSource.Url; } } @@ -200,7 +204,7 @@ public async Task InvokeAsync(HttpContext context) transaction.Finish(status); } // Status code not yet changed to 500 but an exception does exist - // so lets avoid passing the misleading 200 down and close only with + // so let's avoid passing the misleading 200 down and close only with // the exception instance that will be inferred as errored. else if (status == SpanStatus.Ok) { diff --git a/src/Sentry.Azure.Functions.Worker/SentryFunctionsWorkerMiddleware.cs b/src/Sentry.Azure.Functions.Worker/SentryFunctionsWorkerMiddleware.cs index c3c57fe71e..72eebe5d76 100644 --- a/src/Sentry.Azure.Functions.Worker/SentryFunctionsWorkerMiddleware.cs +++ b/src/Sentry.Azure.Functions.Worker/SentryFunctionsWorkerMiddleware.cs @@ -92,7 +92,7 @@ private async Task StartOrContinueTraceAsync(FunctionContext if (requestData is null) { // not an HTTP trigger - return SentrySdk.ContinueTrace((SentryTraceHeader?)null, (BaggageHeader?)null, transactionName, Operation); + return _hub.ContinueTrace((SentryTraceHeader?)null, (BaggageHeader?)null, transactionName, Operation); } var httpMethod = requestData.Method.ToUpperInvariant(); diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs index cda413c7bc..7f1e1b82ba 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -198,6 +198,31 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra replaySession); } + public static DynamicSamplingContext CreateFromUnsampledTransaction(UnsampledTransaction transaction, SentryOptions options, IReplaySession? replaySession) + { + // These should already be set on the transaction. + var publicKey = options.ParsedDsn.PublicKey; + var traceId = transaction.TraceId; + var sampled = transaction.IsSampled; + var sampleRate = transaction.SampleRate!.Value; + var sampleRand = transaction.SampleRand; + var transactionName = transaction.NameSource.IsHighQuality() ? transaction.Name : null; + + // These two may not have been set yet on the transaction, but we can get them directly. + var release = options.SettingLocator.GetRelease(); + var environment = options.SettingLocator.GetEnvironment(); + + return new DynamicSamplingContext(traceId, + publicKey, + sampled, + sampleRate, + sampleRand, + release, + environment, + transactionName, + replaySession); + } + public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) { var traceId = propagationContext.TraceId; @@ -224,6 +249,9 @@ internal static class DynamicSamplingContextExtensions public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options, IReplaySession? replaySession) => DynamicSamplingContext.CreateFromTransaction(transaction, options, replaySession); + 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) => DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options, replaySession); } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 7e8eb01826..fea369de0f 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -129,61 +129,72 @@ internal ITransactionTracer StartTransaction( IReadOnlyDictionary customSamplingContext, DynamicSamplingContext? dynamicSamplingContext) { - var transaction = new TransactionTracer(this, context) - { - SampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var sampleRand) ?? false - ? double.Parse(sampleRand, NumberStyles.Float, CultureInfo.InvariantCulture) - : SampleRandHelper.GenerateSampleRand(context.TraceId.ToString()) - }; - // If the hub is disabled, we will always sample out. In other words, starting a transaction // after disposing the hub will result in that transaction not being sent to Sentry. - // Additionally, we will always sample out if tracing is explicitly disabled. - // Do not invoke the TracesSampler, evaluate the TracesSampleRate, and override any sampling decision - // that may have been already set (i.e.: from a sentry-trace header). if (!IsEnabled) { - transaction.IsSampled = false; - transaction.SampleRate = 0.0; + return NoOpTransaction.Instance; } - else - { - // Except when tracing is disabled, TracesSampler runs regardless of whether a decision - // has already been made, as it can be used to override it. - if (_options.TracesSampler is { } tracesSampler) - { - var samplingContext = new TransactionSamplingContext( - context, - customSamplingContext); - if (tracesSampler(samplingContext) is { } sampleRate) - { - transaction.IsSampled = SampleRandHelper.IsSampled(transaction.SampleRand.Value, sampleRate); - transaction.SampleRate = sampleRate; - } - } + bool? isSampled = null; + double? sampleRate = null; + var sampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var dscsampleRand) ?? false + ? double.Parse(dscsampleRand, NumberStyles.Float, CultureInfo.InvariantCulture) + : SampleRandHelper.GenerateSampleRand(context.TraceId.ToString()); - // Random sampling runs only if the sampling decision hasn't been made already. - if (transaction.IsSampled == null) + // TracesSampler runs regardless of whether a decision has already been made, as it can be used to override it. + if (_options.TracesSampler is { } tracesSampler) + { + var samplingContext = new TransactionSamplingContext( + context, + customSamplingContext); + + if (tracesSampler(samplingContext) is { } samplerSampleRate) { - var sampleRate = _options.TracesSampleRate ?? 0.0; - transaction.IsSampled = SampleRandHelper.IsSampled(transaction.SampleRand.Value, sampleRate); - transaction.SampleRate = sampleRate; + // The TracesSampler trumps all other sampling decisions (even the trace header) + sampleRate = samplerSampleRate; + isSampled = SampleRandHelper.IsSampled(sampleRand, sampleRate.Value); } + } - if (transaction.IsSampled is true && - _options.TransactionProfilerFactory is { } profilerFactory && - _randomValuesFactory.NextBool(_options.ProfilesSampleRate ?? 0.0)) + // If the sampling decision isn't made by a trace sampler we check the trace header first (from the context) or + // finally fallback to Random sampling if the decision has been made by no other means + sampleRate ??= _options.TracesSampleRate ?? 0.0; + isSampled ??= context.IsSampled ?? SampleRandHelper.IsSampled(sampleRand, sampleRate.Value); + + // Make sure there is a replayId (if available) on the provided DSC (if any). + dynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession); + + if (isSampled is false) + { + var unsampledTransaction = new UnsampledTransaction(this, context) { - // TODO cancellation token based on Hub being closed? - transaction.TransactionProfiler = profilerFactory.Start(transaction, CancellationToken.None); - } + SampleRate = sampleRate, + SampleRand = sampleRand, + DynamicSamplingContext = dynamicSamplingContext // Default to the provided DSC + }; + // If no DSC was provided, create one based on this transaction. + // Must be done AFTER the sampling decision has been made (the DSC propagates sampling decisions). + unsampledTransaction.DynamicSamplingContext ??= unsampledTransaction.CreateDynamicSamplingContext(_options, _replaySession); + return unsampledTransaction; } - // 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?.WithReplayId(_replaySession) - ?? transaction.CreateDynamicSamplingContext(_options, _replaySession); + var transaction = new TransactionTracer(this, context) + { + SampleRate = sampleRate, + SampleRand = sampleRand, + DynamicSamplingContext = dynamicSamplingContext // Default to the provided DSC + }; + // If no DSC was provided, create one based on this transaction. + // Must be done AFTER the sampling decision has been made (the DSC propagates sampling decisions). + transaction.DynamicSamplingContext ??= transaction.CreateDynamicSamplingContext(_options, _replaySession); + + if (_options.TransactionProfilerFactory is { } profilerFactory && + _randomValuesFactory.NextBool(_options.ProfilesSampleRate ?? 0.0)) + { + // TODO cancellation token based on Hub being closed? + transaction.TransactionProfiler = profilerFactory.Start(transaction, CancellationToken.None); + } // 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. @@ -220,7 +231,7 @@ public SentryTraceHeader GetTraceHeader() public BaggageHeader GetBaggage() { var span = GetSpan(); - if (span?.GetTransaction() is TransactionTracer { DynamicSamplingContext: { IsEmpty: false } dsc }) + if (span?.GetTransaction().GetDynamicSamplingContext() is { IsEmpty: false } dsc) { return dsc.ToBaggageHeader(); } @@ -373,9 +384,9 @@ private void ApplyTraceContextToEvent(SentryEvent evt, ISpan span) evt.Contexts.Trace.TraceId = span.TraceId; evt.Contexts.Trace.ParentSpanId = span.ParentSpanId; - if (span.GetTransaction() is TransactionTracer transactionTracer) + if (span.GetTransaction().GetDynamicSamplingContext() is { } dsc) { - evt.DynamicSamplingContext = transactionTracer.DynamicSamplingContext; + evt.DynamicSamplingContext = dsc; } } diff --git a/src/Sentry/Internal/ITransactionExtensions.cs b/src/Sentry/Internal/ITransactionExtensions.cs new file mode 100644 index 0000000000..247fd79cea --- /dev/null +++ b/src/Sentry/Internal/ITransactionExtensions.cs @@ -0,0 +1,17 @@ +namespace Sentry.Internal; + +internal static class TransactionExtensions +{ + public static DynamicSamplingContext? GetDynamicSamplingContext(this ITransactionTracer transaction) + { + if (transaction is UnsampledTransaction unsampledTransaction) + { + return unsampledTransaction.DynamicSamplingContext; + } + if (transaction is TransactionTracer transactionTracer) + { + return transactionTracer.DynamicSamplingContext; + } + return null; + } +} diff --git a/src/Sentry/Internal/NoOpSpan.cs b/src/Sentry/Internal/NoOpSpan.cs index f6e49e5a2c..d57aa34261 100644 --- a/src/Sentry/Internal/NoOpSpan.cs +++ b/src/Sentry/Internal/NoOpSpan.cs @@ -13,18 +13,18 @@ protected NoOpSpan() { } - public SpanId SpanId => SpanId.Empty; + public virtual SpanId SpanId => SpanId.Empty; public SpanId? ParentSpanId => SpanId.Empty; - public SentryId TraceId => SentryId.Empty; - public bool? IsSampled => default; + public virtual SentryId TraceId => SentryId.Empty; + public virtual bool? IsSampled => default; public IReadOnlyDictionary Tags => ImmutableDictionary.Empty; public IReadOnlyDictionary Extra => ImmutableDictionary.Empty; public IReadOnlyDictionary Data => ImmutableDictionary.Empty; public DateTimeOffset StartTimestamp => default; - public DateTimeOffset? EndTimestamp => default; - public bool IsFinished => default; + public DateTimeOffset? EndTimestamp => null; + public virtual bool IsFinished => false; - public string Operation + public virtual string Operation { get => string.Empty; set { } @@ -42,21 +42,21 @@ public SpanStatus? Status set { } } - public ISpan StartChild(string operation) => this; + public virtual ISpan StartChild(string operation) => this; - public void Finish() + public virtual void Finish() { } - public void Finish(SpanStatus status) + public virtual void Finish(SpanStatus status) { } - public void Finish(Exception exception, SpanStatus status) + public virtual void Finish(Exception exception, SpanStatus status) { } - public void Finish(Exception exception) + public virtual void Finish(Exception exception) { } @@ -76,7 +76,7 @@ public void SetData(string key, object? value) { } - public SentryTraceHeader GetTraceHeader() => SentryTraceHeader.Empty; + public virtual SentryTraceHeader GetTraceHeader() => SentryTraceHeader.Empty; public IReadOnlyDictionary Measurements => ImmutableDictionary.Empty; diff --git a/src/Sentry/Internal/NoOpTransaction.cs b/src/Sentry/Internal/NoOpTransaction.cs index 9709bcc7d4..ebbcb38d71 100644 --- a/src/Sentry/Internal/NoOpTransaction.cs +++ b/src/Sentry/Internal/NoOpTransaction.cs @@ -7,13 +7,13 @@ internal class NoOpTransaction : NoOpSpan, ITransactionTracer { public new static ITransactionTracer Instance { get; } = new NoOpTransaction(); - private NoOpTransaction() + protected NoOpTransaction() { } public SdkVersion Sdk => SdkVersion.Instance; - public string Name + public virtual string Name { get => string.Empty; set { } @@ -87,7 +87,7 @@ public IReadOnlyList Fingerprint set { } } - public IReadOnlyCollection Spans => ImmutableList.Empty; + public virtual IReadOnlyCollection Spans => ImmutableList.Empty; public IReadOnlyCollection Breadcrumbs => ImmutableList.Empty; diff --git a/src/Sentry/Internal/UnsampledTransaction.cs b/src/Sentry/Internal/UnsampledTransaction.cs new file mode 100644 index 0000000000..277d909e73 --- /dev/null +++ b/src/Sentry/Internal/UnsampledTransaction.cs @@ -0,0 +1,103 @@ +using Sentry.Extensibility; + +namespace Sentry.Internal; + +/// +/// We know already, when starting a transaction, whether it's going to be sampled or not. When it's not sampled, we can +/// avoid lots of unecessary processing. The only thing we need to track is the number of spans that would have been +/// created (the client reports detailing discarded events includes this detail). +/// +internal sealed class UnsampledTransaction : NoOpTransaction +{ + // Although it's a little bit wasteful to create separate individual class instances here when all we're going to + // report to sentry is the span count (in the client report), SDK users may refer to things like + // `ITransaction.Spans.Count`, so we create an actual collection + private readonly ConcurrentBag _spans = []; + private readonly IHub _hub; + private readonly ITransactionContext _context; + private readonly SentryOptions? _options; + + public UnsampledTransaction(IHub hub, ITransactionContext context) + { + _hub = hub; + _options = _hub.GetSentryOptions(); + _options?.LogDebug("Starting unsampled transaction"); + _context = context; + } + + internal DynamicSamplingContext? DynamicSamplingContext { get; set; } + + private bool _isFinished; + public override bool IsFinished => _isFinished; + + public override IReadOnlyCollection Spans => _spans; + + public override SpanId SpanId => _context.SpanId; + + public override SentryId TraceId => _context.TraceId; + + public override bool? IsSampled => false; + + public double? SampleRate { get; set; } + + public double? SampleRand { get; set; } + + public override string Name + { + get => _context.Name; + set { } + } + + public override string Operation + { + get => _context.Operation; + set { } + } + + public override void Finish() + { + _options?.LogDebug("Finishing unsampled transaction"); + + // Ensure the transaction is really cleared from the scope + // See: https://github.com/getsentry/sentry-dotnet/issues/4198 + _isFinished = true; + + // Clear the transaction from the scope and regenerate the Propagation Context, so new events don't have a + // trace context that is "older" than the transaction that just finished + _hub.ConfigureScope(scope => + { + scope.ResetTransaction(this); + scope.SetPropagationContext(new SentryPropagationContext()); + }); + + // Record the discarded events + var spanCount = Spans.Count + 1; // 1 for each span + 1 for the transaction itself + _options?.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Transaction); + _options?.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Span, spanCount); + + _options?.LogDebug("Finished unsampled transaction"); + } + + public override void Finish(SpanStatus status) => Finish(); + + public override void Finish(Exception exception, SpanStatus status) => Finish(); + + public override void Finish(Exception exception) => Finish(); + + /// + public override SentryTraceHeader GetTraceHeader() => new(TraceId, SpanId, IsSampled); + + public override ISpan StartChild(string operation) + { + var span = new UnsampledSpan(this); + _spans.Add(span); + return span; + } + + private class UnsampledSpan(UnsampledTransaction transaction) : NoOpSpan + { + public override ISpan StartChild(string operation) => transaction.StartChild(operation); + + public override bool? IsSampled => false; + } +} diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index 23c54a5fa2..d3f60ceb5f 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -85,15 +85,7 @@ public SpanStatus? Status } /// - public bool? IsSampled - { - get => Contexts.Trace.IsSampled; - internal set - { - Contexts.Trace.IsSampled = value; - SampleRate ??= value == null ? null : value.Value ? 1.0 : 0.0; - } - } + public bool? IsSampled => true; // Implicitly if we instantiate this class then the transaction is sampled in /// /// The sample rate used for this transaction. @@ -234,6 +226,7 @@ internal TransactionTracer(IHub hub, string name, string operation, TransactionN /// internal TransactionTracer(IHub hub, ITransactionContext context, TimeSpan? idleTimeout = null) { + Debug.Assert(context.IsSampled ?? true, "context.IsSampled should always be true when creating a TransactionTracer"); _hub = hub; _options = _hub.GetSentryOptions(); Name = context.Name; @@ -244,7 +237,6 @@ internal TransactionTracer(IHub hub, ITransactionContext context, TimeSpan? idle TraceId = context.TraceId; Description = context.Description; Status = context.Status; - IsSampled = context.IsSampled; StartTimestamp = _stopwatch.StartDateTimeOffset; if (context is TransactionContext transactionContext) diff --git a/test/Sentry.AspNet.Tests/HttpContextExtensionsTests.cs b/test/Sentry.AspNet.Tests/HttpContextExtensionsTests.cs index 0de6f4c6f5..5cf274b260 100644 --- a/test/Sentry.AspNet.Tests/HttpContextExtensionsTests.cs +++ b/test/Sentry.AspNet.Tests/HttpContextExtensionsTests.cs @@ -11,7 +11,8 @@ public void StartSentryTransaction_CreatesValidTransaction() using var _ = SentrySdk.UseHub(new Hub( new SentryOptions { - Dsn = ValidDsn + Dsn = ValidDsn, + TracesSampleRate = 1.0 }, Substitute.For() )); @@ -57,7 +58,8 @@ public void FinishSentryTransaction_FinishesTransaction() using var _ = SentrySdk.UseHub(new Hub( new SentryOptions { - Dsn = ValidDsn + Dsn = ValidDsn, + TracesSampleRate = 1.0 }, Substitute.For() )); @@ -81,6 +83,7 @@ public void StartSentryTransaction_SendDefaultPii_set_to_true_sets_cookies() new SentryOptions { Dsn = ValidDsn, + TracesSampleRate = 1.0, SendDefaultPii = true }, Substitute.For() @@ -102,6 +105,7 @@ public void StartSentryTransaction_SendDefaultPii_set_to_true_does_not_set_cooki new SentryOptions { Dsn = ValidDsn, + TracesSampleRate = 1.0, SendDefaultPii = true }, Substitute.For() diff --git a/test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs b/test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs index 13b34e7385..f20b4cf1c5 100644 --- a/test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs +++ b/test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs @@ -68,7 +68,7 @@ public async Task Transaction_is_bound_on_the_scope_automatically() var sentryClient = Substitute.For(); - var hub = new Hub(new SentryOptions { Dsn = ValidDsn }, sentryClient); + var hub = new Hub(new SentryOptions { Dsn = ValidDsn, TracesSampleRate = 1.0 }, sentryClient); var server = new TestServer(new WebHostBuilder() .UseDefaultServiceProvider(di => di.EnableValidation()) @@ -105,8 +105,10 @@ public async Task Transaction_is_bound_on_the_scope_automatically() transaction.NameSource.Should().Be(TransactionNameSource.Route); } - [Fact] - public async Task Transaction_is_started_automatically_from_incoming_trace_header() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Transaction_is_started_automatically_from_incoming_trace_header(bool isSampled) { // Arrange var sentryClient = Substitute.For(); @@ -133,27 +135,41 @@ public async Task Transaction_is_started_automatically_from_incoming_trace_heade var client = server.CreateClient(); // Act + var sentryTraceHeader = isSampled + ? "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-1" + : "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0"; + using var request = new HttpRequestMessage(HttpMethod.Get, "/person/13") { Headers = { - {"sentry-trace", "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0"} + {"sentry-trace", sentryTraceHeader} } }; await client.SendAsync(request); // Assert - sentryClient.Received(1).CaptureTransaction(Arg.Is(t => - t.Name == "GET /person/{id}" && - t.NameSource == TransactionNameSource.Route && - t.TraceId == SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8") && - t.ParentSpanId == SpanId.Parse("1000000000000000") && - t.IsSampled == false - ), - Arg.Any(), - Arg.Any() - ); + if (isSampled) + { + sentryClient.Received(1).CaptureTransaction(Arg.Is(t => + t.Name == "GET /person/{id}" && + t.NameSource == TransactionNameSource.Route && + t.TraceId == SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8") && + t.ParentSpanId == SpanId.Parse("1000000000000000") && + t.IsSampled == true + ), + Arg.Any(), + Arg.Any() + ); + } + else + { + sentryClient.DidNotReceive().CaptureTransaction(Arg.Any(), + Arg.Any(), + Arg.Any() + ); + } } [Fact] diff --git a/test/Sentry.Azure.Functions.Worker.Tests/SentryFunctionsWorkerMiddlewareTests.cs b/test/Sentry.Azure.Functions.Worker.Tests/SentryFunctionsWorkerMiddlewareTests.cs index 898a830036..2408e00d1a 100644 --- a/test/Sentry.Azure.Functions.Worker.Tests/SentryFunctionsWorkerMiddlewareTests.cs +++ b/test/Sentry.Azure.Functions.Worker.Tests/SentryFunctionsWorkerMiddlewareTests.cs @@ -65,6 +65,7 @@ public async Task Original_exception_rethrown() [Fact] public async Task Transaction_PropertiesAreSet() { + // Arrange var functionContext = Substitute.For(); var functionDefinition = Substitute.For(); functionContext.FunctionDefinition.Returns(functionDefinition); @@ -72,10 +73,11 @@ public async Task Transaction_PropertiesAreSet() var sut = _fixture.GetSut(); + // Act await sut.Invoke(functionContext, context => HttpFunction(null, context)); + // Assert var transaction = _fixture.Transaction; - transaction.Should().NotBeNull(); transaction.Name.Should().Be(functionDefinition.Name); transaction.Operation.Should().Be("function"); diff --git a/test/Sentry.DiagnosticSource.Tests/Integration/SQLite/SentryDiagnosticListenerTests.cs b/test/Sentry.DiagnosticSource.Tests/Integration/SQLite/SentryDiagnosticListenerTests.cs index 2a260be1ae..606df264c6 100644 --- a/test/Sentry.DiagnosticSource.Tests/Integration/SQLite/SentryDiagnosticListenerTests.cs +++ b/test/Sentry.DiagnosticSource.Tests/Integration/SQLite/SentryDiagnosticListenerTests.cs @@ -50,10 +50,7 @@ public Fixture() public ITransactionTracer StartTransaction(IHub hub, ITransactionContext context) { - var transaction = new TransactionTracer(hub, context) - { - IsSampled = true - }; + var transaction = new TransactionTracer(hub, context); var (currentScope, _) = ScopeManager.GetCurrent(); currentScope.Transaction = transaction; return transaction; diff --git a/test/Sentry.DiagnosticSource.Tests/SentryEFCoreListenerTests.cs b/test/Sentry.DiagnosticSource.Tests/SentryEFCoreListenerTests.cs index d38af64f6f..7659ac1cb8 100644 --- a/test/Sentry.DiagnosticSource.Tests/SentryEFCoreListenerTests.cs +++ b/test/Sentry.DiagnosticSource.Tests/SentryEFCoreListenerTests.cs @@ -24,7 +24,11 @@ private class ThrowToStringClass private class Fixture { - internal TransactionTracer Tracer { get; } + private TransactionTracer GetSampledTransaction() => new TransactionTracer(Hub, "foo", "bar"); + + private UnsampledTransaction GetUnsampledTransaction() => new UnsampledTransaction(Hub, new TransactionContext("foo", "bar")); + + internal ITransactionTracer Tracer { get; } public SentryOptions Options { get; } @@ -32,18 +36,13 @@ private class Fixture public IHub Hub { get; } - public Fixture() + public Fixture(bool isSampled = true) { Hub = Substitute.For(); - Tracer = new TransactionTracer(Hub, "foo", "bar") - { - IsSampled = true - }; - var scope = new Scope - { - Transaction = Tracer - }; + Tracer = isSampled ? GetSampledTransaction() : GetUnsampledTransaction(); + + var scope = new Scope { Transaction = Tracer }; var logger = Substitute.For(); logger.IsEnabled(Arg.Any()).Returns(true); @@ -67,14 +66,13 @@ public Fixture() } } - private readonly Fixture _fixture = new(); - [Fact] public void OnNext_UnknownKey_SpanNotInvoked() { // Assert - var hub = _fixture.Hub; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); // Act interceptor.OnNext(new("Unknown", null)); @@ -90,13 +88,14 @@ public void OnNext_UnknownKey_SpanNotInvoked() public void OnNext_KnownKey_GetSpanInvoked(string key, string value) { // Arrange - var interceptor = new SentryEFCoreListener(_fixture.Hub, _fixture.Options); + var fixture = new Fixture(); + var interceptor = new SentryEFCoreListener(fixture.Hub, fixture.Options); // Act interceptor.OnNext(new(key, value)); // Assert - var child = _fixture.Spans.FirstOrDefault(s => GetValidator(key)(s)); + var child = fixture.Spans.FirstOrDefault(s => GetValidator(key)(s)); Assert.NotNull(child); } @@ -106,8 +105,9 @@ public void OnNext_KnownKey_GetSpanInvoked(string key, string value) public void OnNext_KnownKeyButDisabled_GetSpanNotInvoked(string key, string value) { // Arrange - var hub = _fixture.Hub; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); if (key == EFCommandExecuting) { @@ -131,16 +131,16 @@ public void OnNext_KnownKeyButDisabled_GetSpanNotInvoked(string key, string valu public void OnNext_KnownKeyButNotSampled_SpanNotCreated(string key, string value) { // Arrange - var hub = _fixture.Hub; - _fixture.Tracer.IsSampled = false; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(false); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); // Act interceptor.OnNext(new(key, value)); // Assert hub.Received(1).ConfigureScope(Arg.Any>()); - Assert.Empty(_fixture.Tracer.Spans); + Assert.Empty(fixture.Tracer.Spans); } [Theory] @@ -151,16 +151,16 @@ public void OnNext_KnownKeyButNotSampled_SpanNotCreated(string key, string value public void OnNext_TakeSpanButNotSampled_LogWarningNotInvoked(string key, string value) { // Arrange - var hub = _fixture.Hub; - _fixture.Tracer.IsSampled = false; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(false); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); // Act interceptor.OnNext(new(key, value)); // Assert hub.Received(1).ConfigureScope(Arg.Any>()); - _fixture.Options.DiagnosticLogger.DidNotReceiveWithAnyArgs()?.Log(default, default!); + fixture.Options.DiagnosticLogger.DidNotReceiveWithAnyArgs()?.Log(default, default!); } [Theory] @@ -175,8 +175,9 @@ public void OnNext_TakeSpanButNotSampled_LogWarningNotInvoked(string key, string public void OnNext_ConfigureScopeInvokedOnce(string key) { // Arrange - var hub = _fixture.Hub; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); // Act interceptor.OnNext(new(key, null)); @@ -230,8 +231,9 @@ public FakeDiagnosticCommandEventData(FakeDiagnosticConnectionEventData connecti public void OnNext_HappyPath_IsValid() { // Arrange - var hub = _fixture.Hub; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); var expectedSql = "SELECT * FROM ..."; var efSql = "ef Junk\r\nSELECT * FROM ..."; var efConn = "db username : password"; @@ -252,13 +254,13 @@ public void OnNext_HappyPath_IsValid() interceptor.OnNext(new(EFConnectionClosed, connectionEventData)); // Assert - var compilerSpan = _fixture.Spans.First(s => GetValidator(EFQueryCompiling)(s)); - var connectionSpan = _fixture.Spans.First(s => GetValidator(EFConnectionOpening)(s)); - var commandSpan = _fixture.Spans.First(s => GetValidator(EFCommandExecuting)(s)); + var compilerSpan = fixture.Spans.First(s => GetValidator(EFQueryCompiling)(s)); + var connectionSpan = fixture.Spans.First(s => GetValidator(EFConnectionOpening)(s)); + var commandSpan = fixture.Spans.First(s => GetValidator(EFCommandExecuting)(s)); // Validate if all spans were finished. // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - Assert.All(_fixture.Spans, span => + Assert.All(fixture.Spans, span => { Assert.True(span.IsFinished); Assert.Equal(SpanStatus.Ok, span.Status); @@ -284,10 +286,10 @@ public void OnNext_HappyPath_IsValid() Assert.Equal(expectedDbAddress, commandSpan.Extra.TryGetValue(OTelKeys.DbServer)); // Check connections between spans. - Assert.Equal(_fixture.Tracer.SpanId, compilerSpan.ParentSpanId); - Assert.Equal(_fixture.Tracer.SpanId, connectionSpan.ParentSpanId); - Assert.Equal(_fixture.Tracer.SpanId, commandSpan.ParentSpanId); - _fixture.Options.DiagnosticLogger.DidNotReceive()? + Assert.Equal(fixture.Tracer.SpanId, compilerSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, connectionSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, commandSpan.ParentSpanId); + fixture.Options.DiagnosticLogger.DidNotReceive()? .Log(Arg.Is(SentryLevel.Warning), Arg.Is("Trying to close a span that was already garbage collected. {0}"), null, Arg.Any()); @@ -301,8 +303,9 @@ public void OnNext_HappyPath_IsValid() public void OnNext_HappyPathInsideChildSpan_IsValid() { // Arrange - var hub = _fixture.Hub; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); var expectedSql = "SELECT * FROM ..."; var efSql = "ef Junk\r\nSELECT * FROM ..."; var efConn = "db username : password"; @@ -315,7 +318,7 @@ public void OnNext_HappyPathInsideChildSpan_IsValid() var commandEventData = new FakeDiagnosticCommandEventData(connectionEventData, efSql); // Act - var childSpan = _fixture.Tracer.StartChild("Child Span"); + var childSpan = fixture.Tracer.StartChild("Child Span"); interceptor.OnNext(new(EFQueryCompiling, queryEventData)); interceptor.OnNext(new(EFQueryCompiled, queryEventData)); interceptor.OnNext(new(EFConnectionOpening, connectionEventData)); @@ -325,13 +328,13 @@ public void OnNext_HappyPathInsideChildSpan_IsValid() childSpan.Finish(); // Assert - var compilerSpan = _fixture.Spans.First(s => GetValidator(EFQueryCompiling)(s)); - var connectionSpan = _fixture.Spans.First(s => GetValidator(EFConnectionOpening)(s)); - var commandSpan = _fixture.Spans.First(s => GetValidator(EFCommandExecuting)(s)); + var compilerSpan = fixture.Spans.First(s => GetValidator(EFQueryCompiling)(s)); + var connectionSpan = fixture.Spans.First(s => GetValidator(EFConnectionOpening)(s)); + var commandSpan = fixture.Spans.First(s => GetValidator(EFCommandExecuting)(s)); // Validate if all spans were finished. // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - Assert.All(_fixture.Spans, span => + Assert.All(fixture.Spans, span => { Assert.True(span.IsFinished); Assert.Equal(SpanStatus.Ok, span.Status); @@ -360,7 +363,7 @@ public void OnNext_HappyPathInsideChildSpan_IsValid() Assert.Equal(childSpan.SpanId, compilerSpan.ParentSpanId); Assert.Equal(childSpan.SpanId, connectionSpan.ParentSpanId); Assert.Equal(childSpan.SpanId, commandSpan.ParentSpanId); - _fixture.Options.DiagnosticLogger.DidNotReceive()? + fixture.Options.DiagnosticLogger.DidNotReceive()? .Log(Arg.Is(SentryLevel.Warning), Arg.Is("Trying to close a span that was already garbage collected. {0}"), null, Arg.Any()); } @@ -369,8 +372,9 @@ public void OnNext_HappyPathInsideChildSpan_IsValid() public void OnNext_HappyPathWithError_TransactionWithErroredCommand() { // Arrange - var hub = _fixture.Hub; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); var expectedSql = "SELECT * FROM ..."; var efSql = "ef Junk\r\nSELECT * FROM ..."; var efConn = "db username : password"; @@ -388,9 +392,9 @@ public void OnNext_HappyPathWithError_TransactionWithErroredCommand() interceptor.OnNext(new(EFConnectionClosed, connectionEventData)); // Assert - var compilerSpan = _fixture.Spans.First(s => GetValidator(EFQueryCompiling)(s)); - var connectionSpan = _fixture.Spans.First(s => GetValidator(EFConnectionOpening)(s)); - var commandSpan = _fixture.Spans.First(s => GetValidator(EFCommandFailed)(s)); + var compilerSpan = fixture.Spans.First(s => GetValidator(EFQueryCompiling)(s)); + var connectionSpan = fixture.Spans.First(s => GetValidator(EFConnectionOpening)(s)); + var commandSpan = fixture.Spans.First(s => GetValidator(EFCommandFailed)(s)); // Validate if all spans were finished. Assert.True(compilerSpan.IsFinished); @@ -403,9 +407,9 @@ public void OnNext_HappyPathWithError_TransactionWithErroredCommand() Assert.Equal(SpanStatus.InternalError, commandSpan.Status); // Check connections between spans. - Assert.Equal(_fixture.Tracer.SpanId, compilerSpan.ParentSpanId); - Assert.Equal(_fixture.Tracer.SpanId, connectionSpan.ParentSpanId); - Assert.Equal(_fixture.Tracer.SpanId, commandSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, compilerSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, connectionSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, commandSpan.ParentSpanId); Assert.Equal(expectedSql, commandSpan.Description); } @@ -414,8 +418,9 @@ public void OnNext_HappyPathWithError_TransactionWithErroredCommand() public void OnNext_HappyPathWithError_TransactionWithErroredCompiler() { // Arrange - var hub = _fixture.Hub; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); var expectedSql = "SELECT * FROM ..."; var efSql = "ef Junk\r\nSELECT * FROM ..."; @@ -424,12 +429,12 @@ public void OnNext_HappyPathWithError_TransactionWithErroredCompiler() hub.CaptureEvent(new SentryEvent()); // Assert - var compilerSpan = _fixture.Spans.First(s => GetValidator(EFQueryCompiling)(s)); + var compilerSpan = fixture.Spans.First(s => GetValidator(EFQueryCompiling)(s)); Assert.True(compilerSpan.IsFinished); Assert.Equal(SpanStatus.InternalError, compilerSpan.Status); - Assert.Equal(_fixture.Tracer.SpanId, compilerSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, compilerSpan.ParentSpanId); Assert.Equal(expectedSql, compilerSpan.Description); } @@ -438,8 +443,9 @@ public void OnNext_HappyPathWithError_TransactionWithErroredCompiler() public void OnNext_Same_Connections_Consolidated() { // Arrange - var hub = _fixture.Hub; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); var efSql = "ef Junk\r\nSELECT * FROM ..."; var efConn = "db username : password"; @@ -491,12 +497,12 @@ public void OnNext_Same_Connections_Consolidated() using (new AssertionScope()) { - var dbSpans = _fixture.Spans.Where(IsDbSpan).ToArray(); + var dbSpans = fixture.Spans.Where(IsDbSpan).ToArray(); dbSpans.Count().Should().Be(2); dbSpans.Should().Contain(s => forConnection(s, connectionA)); dbSpans.Should().Contain(s => forConnection(s, connectionB)); - var commandSpans = _fixture.Spans.Where(IsCommandSpan).ToArray(); + var commandSpans = fixture.Spans.Where(IsCommandSpan).ToArray(); commandSpans.Count().Should().Be(3); commandSpans.Should().Contain(s => forCommand(s, commandA)); commandSpans.Should().Contain(s => forCommand(s, commandB)); @@ -528,8 +534,9 @@ public void OnNext_Same_Connections_Consolidated() public void OnNext_ThrowsException_ExceptionIsolated() { // Arrange - var hub = _fixture.Hub; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); var exceptionReceived = false; // Act @@ -553,14 +560,15 @@ public void OnNext_ThrowsException_ExceptionIsolated() public void OnNext_TakeSpanWithoutSpan_ShowsGarbageCollectorError(string operation) { // Arrange - var hub = _fixture.Hub; - var interceptor = new SentryEFCoreListener(hub, _fixture.Options); + var fixture = new Fixture(); + var hub = fixture.Hub; + var interceptor = new SentryEFCoreListener(hub, fixture.Options); // Act interceptor.OnNext(new(operation, "ef Junk\r\nSELECT * FROM ...")); // Assert - _fixture.Options.DiagnosticLogger.Received(1)? + fixture.Options.DiagnosticLogger.Received(1)? .Log(Arg.Is(SentryLevel.Warning), Arg.Is("Tried to close {0} span but no matching span could be found."), null, Arg.Any()); } diff --git a/test/Sentry.DiagnosticSource.Tests/SentrySqlListenerTests.cs b/test/Sentry.DiagnosticSource.Tests/SentrySqlListenerTests.cs index de6ec84ff8..1d3eb43770 100644 --- a/test/Sentry.DiagnosticSource.Tests/SentrySqlListenerTests.cs +++ b/test/Sentry.DiagnosticSource.Tests/SentrySqlListenerTests.cs @@ -37,7 +37,11 @@ private class ThrowToOperationClass private class Fixture { - internal TransactionTracer Tracer { get; } + private TransactionTracer GetSampledTransaction() => new TransactionTracer(Hub, "foo", "bar"); + + private UnsampledTransaction GetUnsampledTransaction() => new UnsampledTransaction(Hub, new TransactionContext("foo", "bar")); + + internal ITransactionTracer Tracer { get; } public InMemoryDiagnosticLogger Logger { get; } @@ -46,7 +50,7 @@ private class Fixture public IReadOnlyCollection Spans => Tracer?.Spans; public IHub Hub { get; } - public Fixture() + public Fixture(bool isSampled = true) { Logger = new InMemoryDiagnosticLogger(); @@ -59,10 +63,7 @@ public Fixture() }; Hub = Substitute.For(); - Tracer = new TransactionTracer(Hub, "foo", "bar") - { - IsSampled = true - }; + Tracer = isSampled ? GetSampledTransaction() : GetUnsampledTransaction(); var scope = new Scope { @@ -74,13 +75,12 @@ public Fixture() } } - private readonly Fixture _fixture = new(); - [Fact] public void OnNext_UnknownKey_SpanNotInvoked() { // Assert - var hub = _fixture.Hub; + var fixture = new Fixture(); + var hub = fixture.Hub; var interceptor = new SentrySqlListener(hub, new SentryOptions()); // Act @@ -98,11 +98,12 @@ public void OnNext_UnknownKey_SpanNotInvoked() public void OnNext_KnownKey_GetSpanInvoked(string key, bool addConnectionSpan) { // Arrange - var hub = _fixture.Hub; + var fixture = new Fixture(); + var hub = fixture.Hub; var interceptor = new SentrySqlListener(hub, new SentryOptions()); if (addConnectionSpan) { - _fixture.Tracer.StartChild("abc").SetExtra(SqlKeys.DbConnectionId, Guid.Empty); + fixture.Tracer.StartChild("abc").SetExtra(SqlKeys.DbConnectionId, Guid.Empty); } // Act @@ -119,10 +120,10 @@ public void OnNext_KnownKey_GetSpanInvoked(string key, bool addConnectionSpan) })); // Assert - var spans = _fixture.Spans.Where(s => s.Operation != "abc"); + var spans = fixture.Spans.Where(s => s.Operation != "abc"); Assert.NotEmpty(spans); - var firstSpan = _fixture.Spans.OrderByDescending(x => x.StartTimestamp).First(); + var firstSpan = fixture.Spans.OrderByDescending(x => x.StartTimestamp).First(); Assert.True(GetValidator(key)(firstSpan)); } @@ -132,8 +133,8 @@ public void OnNext_KnownKey_GetSpanInvoked(string key, bool addConnectionSpan) public void OnNext_KnownButNotSampled_SpanNotCreated(string key) { // Arrange - var hub = _fixture.Hub; - _fixture.Tracer.IsSampled = false; + var fixture = new Fixture(false); + var hub = fixture.Hub; var interceptor = new SentrySqlListener(hub, new SentryOptions()); // Act @@ -149,7 +150,7 @@ public void OnNext_KnownButNotSampled_SpanNotCreated(string key) } })); - Assert.Empty(_fixture.Tracer.Spans); + Assert.Empty(fixture.Tracer.Spans); } [Theory] @@ -162,7 +163,8 @@ public void OnNext_HappyPaths_IsValid(string connectionOpenKey, string connectio string connectionCloseKey, string queryStartKey, string queryEndKey) { // Arrange - var hub = _fixture.Hub; + var fixture = new Fixture(); + var hub = fixture.Hub; var interceptor = new SentrySqlListener(hub, new SentryOptions()); var query = "SELECT * FROM ..."; var connectionId = Guid.NewGuid(); @@ -223,21 +225,21 @@ public void OnNext_HappyPaths_IsValid(string connectionOpenKey, string connectio })); // Assert - _fixture.Spans.Should().HaveCount(2); - var connectionSpan = _fixture.Spans.First(s => GetValidator(connectionOpenKey)(s)); - var commandSpan = _fixture.Spans.First(s => GetValidator(queryStartKey)(s)); + fixture.Spans.Should().HaveCount(2); + var connectionSpan = fixture.Spans.First(s => GetValidator(connectionOpenKey)(s)); + var commandSpan = fixture.Spans.First(s => GetValidator(queryStartKey)(s)); // Validate if all spans were finished. // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - Assert.All(_fixture.Spans, span => + Assert.All(fixture.Spans, span => { Assert.True(span.IsFinished); Assert.Equal(SpanStatus.Ok, span.Status); }); // Check connections between spans. - Assert.Equal(_fixture.Tracer.SpanId, connectionSpan.ParentSpanId); - Assert.Equal(_fixture.Tracer.SpanId, commandSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, connectionSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, commandSpan.ParentSpanId); // Validate descriptions and extra data is set correctly Assert.Equal(query, commandSpan.Description); @@ -266,7 +268,8 @@ public void OnNext_HappyPathsInsideChildSpan_IsValid(string connectionOpenKey, s string connectionCloseKey, string queryStartKey, string queryEndKey) { // Arrange - var hub = _fixture.Hub; + var fixture = new Fixture(); + var hub = fixture.Hub; var interceptor = new SentrySqlListener(hub, new SentryOptions()); var query = "SELECT * FROM ..."; var connectionId = Guid.NewGuid(); @@ -275,7 +278,7 @@ public void OnNext_HappyPathsInsideChildSpan_IsValid(string connectionOpenKey, s var queryOperationId = Guid.NewGuid(); // Act - var childSpan = _fixture.Tracer.StartChild("Child Span"); + var childSpan = fixture.Tracer.StartChild("Child Span"); interceptor.OnNext( new(connectionOpenKey, new @@ -317,13 +320,13 @@ public void OnNext_HappyPathsInsideChildSpan_IsValid(string connectionOpenKey, s childSpan.Finish(); // Assert - _fixture.Spans.Should().HaveCount(3); - var connectionSpan = _fixture.Spans.First(s => GetValidator(connectionOpenKey)(s)); - var commandSpan = _fixture.Spans.First(s => GetValidator(queryStartKey)(s)); + fixture.Spans.Should().HaveCount(3); + var connectionSpan = fixture.Spans.First(s => GetValidator(connectionOpenKey)(s)); + var commandSpan = fixture.Spans.First(s => GetValidator(queryStartKey)(s)); // Validate if all spans were finished. // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - Assert.All(_fixture.Spans, span => + Assert.All(fixture.Spans, span => { Assert.True(span.IsFinished); Assert.Equal(SpanStatus.Ok, span.Status); @@ -345,7 +348,8 @@ public void OnNext_TwoConnectionSpansWithSameId_FinishBothWithOk(string connecti string connectionUpdate, string connectionClose) { // Arrange - var hub = _fixture.Hub; + var fixture = new Fixture(); + var hub = fixture.Hub; var interceptor = new SentrySqlListener(hub, new SentryOptions()); var connectionId = Guid.NewGuid(); var connectionOperationIds = new List @@ -381,11 +385,11 @@ public void OnNext_TwoConnectionSpansWithSameId_FinishBothWithOk(string connecti } // Assert - _fixture.Spans.Should().HaveCount(2); + fixture.Spans.Should().HaveCount(2); // Validate if all spans were finished. // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - Assert.All(_fixture.Spans, span => + Assert.All(fixture.Spans, span => { Assert.True(span.IsFinished); Assert.Equal(SpanStatus.Ok, span.Status); @@ -403,7 +407,8 @@ public void OnNext_ExecuteQueryCalledBeforeConnectionId_ExecuteParentIsConnectio string connectionUpdate, string connectionClose, string executeBeforeKey, string executeAfterKey) { // Arrange - var hub = _fixture.Hub; + var fixture = new Fixture(); + var hub = fixture.Hub; var interceptor = new SentrySqlListener(hub, new SentryOptions()); var query = "SELECT * FROM ..."; var connectionId = Guid.NewGuid(); @@ -454,21 +459,21 @@ public void OnNext_ExecuteQueryCalledBeforeConnectionId_ExecuteParentIsConnectio })); // Assert - _fixture.Spans.Should().HaveCount(2); - var connectionSpan = _fixture.Spans.First(s => GetValidator(connectionBeforeKey)(s)); - var commandSpan = _fixture.Spans.First(s => GetValidator(executeBeforeKey)(s)); + fixture.Spans.Should().HaveCount(2); + var connectionSpan = fixture.Spans.First(s => GetValidator(connectionBeforeKey)(s)); + var commandSpan = fixture.Spans.First(s => GetValidator(executeBeforeKey)(s)); // Validate if all spans were finished. // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local - Assert.All(_fixture.Spans, span => + Assert.All(fixture.Spans, span => { Assert.True(span.IsFinished); Assert.Equal(SpanStatus.Ok, span.Status); }); // Check connections between spans. - Assert.Equal(_fixture.Tracer.SpanId, connectionSpan.ParentSpanId); - Assert.Equal(_fixture.Tracer.SpanId, commandSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, connectionSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, commandSpan.ParentSpanId); Assert.Equal(query, commandSpan.Description); } @@ -497,8 +502,9 @@ public async Task OnNext_ParallelExecution_IsValidAsync(int testNumber) { _ = testNumber; // Arrange - var hub = _fixture.Hub; - var interceptor = new SentrySqlListener(hub, _fixture.Options); + var fixture = new Fixture(); + var hub = fixture.Hub; + var interceptor = new SentrySqlListener(hub, fixture.Options); var maxItems = 8; var query = "SELECT * FROM ..."; var connectionsIds = Enumerable.Range(0, maxItems).Select(_ => Guid.NewGuid()).ToList(); @@ -543,13 +549,13 @@ void SimulateDbRequest(List connectionOperationIds, List queryOperat // Assert // 1 connection span and 1 query span, executed twice for 11 threads. - _fixture.Spans.Should().HaveCount(2 * 2 * maxItems); + fixture.Spans.Should().HaveCount(2 * 2 * maxItems); - var openSpans = _fixture.Spans.Where(span => !span.IsFinished); - var closedSpans = _fixture.Spans.Where(span => span.IsFinished); - var connectionSpans = _fixture.Spans.Where(span => span.Operation is "db.connection").ToList(); + var openSpans = fixture.Spans.Where(span => !span.IsFinished); + var closedSpans = fixture.Spans.Where(span => span.IsFinished); + var connectionSpans = fixture.Spans.Where(span => span.Operation is "db.connection").ToList(); var closedConnectionSpans = connectionSpans.Where(span => span.IsFinished).ToList(); - var querySpans = _fixture.Spans.Where(span => span.Operation is "db.query").ToList(); + var querySpans = fixture.Spans.Where(span => span.Operation is "db.query").ToList(); // We have two connections per thread, despite having the same ConnectionId, both will be closed. closedConnectionSpans.Should().HaveCount(2 * maxItems); @@ -565,27 +571,28 @@ void SimulateDbRequest(List connectionOperationIds, List queryOperat { Assert.NotNull(connectionSpan.Extra[SqlKeys.DbConnectionId]); Assert.NotNull(connectionSpan.Extra[SqlKeys.DbOperationId]); - Assert.Equal(_fixture.Tracer.SpanId, connectionSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, connectionSpan.ParentSpanId); }); // Assert all Query spans have the correct ParentId Set and record the Connection Id. Assert.All(querySpans, querySpan => { Assert.True(querySpan.IsFinished); - Assert.Equal(_fixture.Tracer.SpanId, querySpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, querySpan.ParentSpanId); var queryConnectionId = querySpan.Extra.TryGetValue(SqlKeys.DbConnectionId); queryConnectionId.Should().NotBeNull(); }); - _fixture.Logger.Entries.Should().BeEmpty(); + fixture.Logger.Entries.Should().BeEmpty(); } [Fact] public void OnNext_HappyPathWithError_TransactionWithErroredCommand() { // Arrange - var hub = _fixture.Hub; + var fixture = new Fixture(); + var hub = fixture.Hub; var interceptor = new SentrySqlListener(hub, new SentryOptions()); var query = "SELECT * FROM ..."; var connectionId = Guid.NewGuid(); @@ -635,10 +642,10 @@ public void OnNext_HappyPathWithError_TransactionWithErroredCommand() })); // Assert - _fixture.Spans.Should().HaveCount(2); + fixture.Spans.Should().HaveCount(2); - var connectionSpan = _fixture.Spans.First(s => GetValidator(SqlMicrosoftWriteConnectionOpenBeforeCommand)(s)); - var commandSpan = _fixture.Spans.First(s => GetValidator(SqlMicrosoftBeforeExecuteCommand)(s)); + var connectionSpan = fixture.Spans.First(s => GetValidator(SqlMicrosoftWriteConnectionOpenBeforeCommand)(s)); + var commandSpan = fixture.Spans.First(s => GetValidator(SqlMicrosoftBeforeExecuteCommand)(s)); Assert.True(connectionSpan.IsFinished); Assert.Equal(SpanStatus.Ok, connectionSpan.Status); @@ -648,8 +655,8 @@ public void OnNext_HappyPathWithError_TransactionWithErroredCommand() Assert.Equal(SpanStatus.InternalError, commandSpan.Status); // Check connections between spans. - Assert.Equal(_fixture.Tracer.SpanId, connectionSpan.ParentSpanId); - Assert.Equal(_fixture.Tracer.SpanId, commandSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, connectionSpan.ParentSpanId); + Assert.Equal(fixture.Tracer.SpanId, commandSpan.ParentSpanId); Assert.Equal(query, commandSpan.Description); } @@ -658,7 +665,8 @@ public void OnNext_HappyPathWithError_TransactionWithErroredCommand() public void OnNext_ThrowsException_ExceptionIsolated() { // Arrange - var hub = _fixture.Hub; + var fixture = new Fixture(); + var hub = fixture.Hub; var interceptor = new SentrySqlListener(hub, new SentryOptions()); var exceptionReceived = false; diff --git a/test/Sentry.Tests/DynamicSamplingContextTests.cs b/test/Sentry.Tests/DynamicSamplingContextTests.cs index a3fdb6a8ff..4c9f4ff3ae 100644 --- a/test/Sentry.Tests/DynamicSamplingContextTests.cs +++ b/test/Sentry.Tests/DynamicSamplingContextTests.cs @@ -376,12 +376,10 @@ public void ToBaggageHeader() Assert.Equal(original.Members, result.Members); } - [Theory] - [InlineData(false)] - [InlineData(true)] - [InlineData(null)] - public void CreateFromTransaction(bool? isSampled) + [Fact] + public void CreateFromTransaction() { + // Arrange var options = new SentryOptions { Dsn = ValidDsn, @@ -399,9 +397,50 @@ public void CreateFromTransaction(bool? isSampled) { Name = "GET /person/{id}", NameSource = TransactionNameSource.Route, - IsSampled = isSampled, SampleRate = 0.5, - SampleRand = (isSampled ?? true) ? 0.4000 : 0.6000, // Lower than the sample rate means sampled == true + SampleRand = 0.4000, // Lower than the sample rate means sampled == true + User = new SentryUser(), + }; + + // Act + var dsc = transaction.CreateDynamicSamplingContext(options, _fixture.ActiveReplaySession); + + // Assert + Assert.NotNull(dsc); + Assert.Equal(9, dsc.Items.Count); + Assert.Equal(traceId.ToString(), Assert.Contains("trace_id", dsc.Items)); + Assert.Equal("d4d82fc1c2c4032a83f3a29aa3a3aff", Assert.Contains("public_key", dsc.Items)); + Assert.Equal("true", Assert.Contains("sampled", dsc.Items)); + Assert.Equal("0.5", Assert.Contains("sample_rate", dsc.Items)); + Assert.Equal("0.4000", Assert.Contains("sample_rand", dsc.Items)); + 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 CreateFromUnsampledTransaction() + { + var options = new SentryOptions + { + Dsn = ValidDsn, + Release = "foo@2.4.5", + Environment = "staging" + }; + + var hub = Substitute.For(); + var ctx = Substitute.For(); + ctx.Name.Returns("GET /person/{id}"); + + var traceId = SentryId.Create(); + ctx.TraceId.Returns(traceId); + + var transaction = new UnsampledTransaction(hub, ctx) + { + SampleRate = 0.5, + SampleRand = 0.6000, // Lower than the sample rate means sampled == true User = { }, @@ -410,19 +449,12 @@ public void CreateFromTransaction(bool? isSampled) var dsc = transaction.CreateDynamicSamplingContext(options, _fixture.ActiveReplaySession); Assert.NotNull(dsc); - Assert.Equal(isSampled.HasValue ? 9 : 8, dsc.Items.Count); + Assert.Equal(9, 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) - { - Assert.Equal(sampled ? "true" : "false", Assert.Contains("sampled", dsc.Items)); - } - else - { - Assert.DoesNotContain("sampled", dsc.Items); - } + Assert.Equal("false", Assert.Contains("sampled", dsc.Items)); Assert.Equal("0.5", Assert.Contains("sample_rate", dsc.Items)); - Assert.Equal((isSampled ?? true) ? "0.4000" : "0.6000", Assert.Contains("sample_rand", dsc.Items)); + Assert.Equal("0.6000", Assert.Contains("sample_rand", dsc.Items)); 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)); diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index efbca9830b..89633c0752 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -848,11 +848,23 @@ public void StartTransaction_TraceSampler_UsesSampleRand(double sampleRate, bool var transaction = hub.StartTransaction(transactionContext, customContext, dsc); // Assert - var transactionTracer = ((TransactionTracer)transaction); - transactionTracer.IsSampled.Should().Be(expectedIsSampled); - transactionTracer.SampleRate.Should().Be(sampleRate); - transactionTracer.SampleRand.Should().Be(0.1234); - transactionTracer.DynamicSamplingContext.Should().Be(dsc); + if (expectedIsSampled) + { + transaction.Should().BeOfType(); + var transactionTracer = ((TransactionTracer)transaction); + transactionTracer.IsSampled.Should().Be(true); + transactionTracer.SampleRate.Should().Be(sampleRate); + transactionTracer.SampleRand.Should().Be(0.1234); + transactionTracer.DynamicSamplingContext.Should().Be(dsc); + } + else + { + transaction.Should().BeOfType(); + var unsampledTransaction = ((UnsampledTransaction)transaction); + unsampledTransaction.SampleRate.Should().Be(sampleRate); + unsampledTransaction.SampleRand.Should().Be(0.1234); + unsampledTransaction.DynamicSamplingContext.Should().Be(dsc); + } } [Theory] @@ -879,11 +891,23 @@ public void StartTransaction_StaticSampler_UsesSampleRand(double sampleRate, boo var transaction = hub.StartTransaction(transactionContext, customContext, dsc); // Assert - var transactionTracer = ((TransactionTracer)transaction); - transactionTracer.IsSampled.Should().Be(expectedIsSampled); - transactionTracer.SampleRate.Should().Be(sampleRate); - transactionTracer.SampleRand.Should().Be(0.1234); - transactionTracer.DynamicSamplingContext.Should().Be(dsc); + if (expectedIsSampled) + { + transaction.Should().BeOfType(); + var transactionTracer = ((TransactionTracer)transaction); + transactionTracer.IsSampled.Should().Be(expectedIsSampled); + transactionTracer.SampleRate.Should().Be(sampleRate); + transactionTracer.SampleRand.Should().Be(0.1234); + transactionTracer.DynamicSamplingContext.Should().Be(dsc); + } + else + { + transaction.Should().BeOfType(); + var unsampledTransaction = ((UnsampledTransaction)transaction); + unsampledTransaction.SampleRate.Should().Be(sampleRate); + unsampledTransaction.SampleRand.Should().Be(0.1234); + unsampledTransaction.DynamicSamplingContext.Should().Be(dsc); + } } [Fact] @@ -1059,26 +1083,25 @@ public void StartTransaction_TracesSampler_FallbackToStatic_SampledOut() transaction.IsSampled.Should().BeFalse(); } - [Fact] - public void GetTraceHeader_ReturnsHeaderForActiveSpan() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetTraceHeader_ReturnsHeaderForActiveSpan(bool isSampled) { // Arrange + _fixture.Options.TracesSampleRate = isSampled ? 1 : 0; var hub = _fixture.GetSut(); var transaction = hub.StartTransaction("foo", "bar"); + hub.ConfigureScope(scope => scope.Transaction = transaction); // Act - hub.ConfigureScope(scope => - { - scope.Transaction = transaction; - - var header = hub.GetTraceHeader(); + var header = hub.GetTraceHeader(); - // Assert - header.Should().NotBeNull(); - header.SpanId.Should().Be(transaction.SpanId); - header.TraceId.Should().Be(transaction.TraceId); - header.IsSampled.Should().Be(transaction.IsSampled); - }); + // Assert + header.Should().NotBeNull(); + header.SpanId.Should().Be(transaction.SpanId); + header.TraceId.Should().Be(transaction.TraceId); + header.IsSampled.Should().Be(transaction.IsSampled); } [Fact] @@ -1101,24 +1124,37 @@ public void GetTraceHeader_NoSpanActive_ReturnsHeaderFromPropagationContext() header.IsSampled.Should().BeNull(); } - [Fact] - public void GetBaggage_SpanActive_ReturnsBaggageFromSpan() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetBaggage_SpanActive_ReturnsBaggageFromSpan(bool isSampled) { // Arrange + _fixture.Options.TracesSampleRate = isSampled ? 1 : 0; var hub = _fixture.GetSut(); - var transaction = hub.StartTransaction("test-name", "_"); - // Act - hub.ConfigureScope(scope => + var expectedBaggage = BaggageHeader.Create(new List> { - scope.Transaction = transaction; + {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, + {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, + {"sentry-sample_rate", "0.0"} + }); + var replaySession = Substitute.For(); + replaySession.ActiveReplayId.Returns((SentryId?)null); + var dsc = expectedBaggage.CreateDynamicSamplingContext(replaySession); - var baggage = hub.GetBaggage(); + var transaction = hub.StartTransaction(new TransactionContext("name", "op"), + new Dictionary(), dsc); + hub.ConfigureScope(scope => scope.Transaction = transaction); - // Assert - baggage.Should().NotBeNull(); - Assert.Contains("test-name", baggage!.ToString()); - }); + // Act + var baggage = hub.GetBaggage(); + + // Assert + baggage.Should().NotBeNull(); + Assert.Equal("43365712692146d08ee11a729dfbcaca", Assert.Contains("trace_id", dsc.Items)); + Assert.Equal("d4d82fc1c2c4032a83f3a29aa3a3aff", Assert.Contains("public_key", dsc.Items)); + Assert.Equal("0.0", Assert.Contains("sample_rate", dsc.Items)); } [Fact] @@ -1786,7 +1822,16 @@ public void CaptureTransaction_HubEnabled(bool enabled) transaction.Finish(); // Assert - _fixture.Client.Received().CaptureTransaction(Arg.Is(t => t.IsSampled == enabled), Arg.Any(), Arg.Any()); + if (enabled) + { + _fixture.Client.Received().CaptureTransaction(Arg.Is(t => t.IsSampled == enabled), + Arg.Any(), Arg.Any()); + } + else + { + transaction.Should().Be(NoOpTransaction.Instance); + _fixture.Client.DidNotReceive().CaptureTransaction(Arg.Any(), Arg.Any(), Arg.Any()); + } } [Fact] diff --git a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs index 7a85611425..7f210116d8 100644 --- a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs +++ b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs @@ -269,6 +269,13 @@ public void SerializeObject_AllPropertiesSetToNonDefault_SerializesValidObject() ctx.Subject.Should().BeCloseTo(ctx.Expectation, TimeSpan.FromMilliseconds(1)) ).WhenTypeIs(); + // IsSampled isn't ever serialised, since we'd never send an unsampled transaction to Sentry + // See: https://develop.sentry.dev/sdk/data-model/event-payloads/contexts/#trace-context + // Note the absence of any `sampled` member for the Trace Context interface, which is where + // SentryTransaction derives it's value for IsSampled from + o.Excluding(ctx => ctx.Path == "Contexts[trace].IsSampled"); + o.Excluding(x => x.IsSampled); + return o; }); } @@ -334,10 +341,7 @@ public void StartChild_LevelTwo_Works() public void StartChild_Limit_Maintained() { // Arrange - var transaction = new TransactionTracer(DisabledHub.Instance, "my name", "my op") - { - IsSampled = true - }; + var transaction = new TransactionTracer(DisabledHub.Instance, "my name", "my op"); // Act var spans = Enumerable @@ -350,30 +354,11 @@ public void StartChild_Limit_Maintained() spans.Count(s => s.IsSampled == true).Should().Be(1000); } - [Fact] - public void StartChild_SamplingInherited_Null() - { - // Arrange - var transaction = new TransactionTracer(DisabledHub.Instance, "my name", "my op") - { - IsSampled = null - }; - - // Act - var child = transaction.StartChild("child op", "child desc"); - - // Assert - child.IsSampled.Should().BeNull(); - } - [Fact] public void StartChild_SamplingInherited_True() { // Arrange - var transaction = new TransactionTracer(DisabledHub.Instance, "my name", "my op") - { - IsSampled = true - }; + var transaction = new TransactionTracer(DisabledHub.Instance, "my name", "my op"); // Act var child = transaction.StartChild("child op", "child desc"); @@ -386,10 +371,7 @@ public void StartChild_SamplingInherited_True() public void StartChild_SamplingInherited_False() { // Arrange - var transaction = new TransactionTracer(DisabledHub.Instance, "my name", "my op") - { - IsSampled = false - }; + var transaction = new UnsampledTransaction(DisabledHub.Instance, new TransactionContext("n", "o")); // Act var child = transaction.StartChild("child op", "child desc"); diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index 6b85c74de9..0ebb3eb2af 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -960,11 +960,9 @@ public void CaptureTransaction_SampledOut_Dropped() var client = _fixture.GetSut(); var hub = Substitute.For(); - var transaction = new TransactionTracer(hub, "test name", "test operation"); + var transaction = new UnsampledTransaction(hub, new TransactionContext("test name", "test operation")); transaction.StartChild("span1"); transaction.StartChild("span2"); - transaction.IsSampled = false; // <-- *** Sampled out *** - transaction.EndTimestamp = DateTimeOffset.Now; // Act client.CaptureTransaction(new SentryTransaction(transaction)); @@ -1262,7 +1260,6 @@ public void CaptureTransaction_TransactionProcessorRejectsEvent_RecordDiscardedE var transaction = new TransactionTracer(hub, "test name", "test operation"); transaction.StartChild("span1"); transaction.StartChild("span2"); - transaction.IsSampled = true; transaction.EndTimestamp = DateTimeOffset.Now; // finished // Act @@ -1383,7 +1380,6 @@ public void CaptureTransaction_BeforeSendTransaction_RejectEvent_RecordsDiscard( var transaction = new TransactionTracer(hub, "test name", "test operation"); transaction.StartChild("span1"); transaction.StartChild("span2"); - transaction.IsSampled = true; transaction.EndTimestamp = DateTimeOffset.Now; // finished // Act diff --git a/test/Sentry.Tests/SerializationTests.cs b/test/Sentry.Tests/SerializationTests.cs index 51a57bdd34..9f5c578aff 100644 --- a/test/Sentry.Tests/SerializationTests.cs +++ b/test/Sentry.Tests/SerializationTests.cs @@ -15,15 +15,12 @@ public SerializationTests(ITestOutputHelper output) public void Serialization_TransactionAndSpanData() { var hub = Substitute.For(); - var context = new TransactionContext("name", "operation", new SentryTraceHeader(SentryId.Empty, SpanId.Empty, false)); + var context = new TransactionContext("name", "operation", new SentryTraceHeader(SentryId.Empty, SpanId.Empty, true)); var transactionTracer = new TransactionTracer(hub, context); var span = transactionTracer.StartChild("childop"); span.SetData("span1", "value1"); - var transaction = new SentryTransaction(transactionTracer) - { - IsSampled = false - }; + var transaction = new SentryTransaction(transactionTracer); transaction.SetData("transaction1", "transaction_value"); var json = transaction.ToJsonString(_testOutputLogger); _testOutputLogger.LogDebug(json);