diff --git a/CHANGELOG.md b/CHANGELOG.md index 67fdc8d632..85765b1688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Added non-allocating `ConfigureScope` and `ConfigureScopeAsync` overloads ([#4244](https://github.com/getsentry/sentry-dotnet/pull/4244)) + ### Fixes - The HTTP instrumentation uses the span created for the outgoing request in the sentry-trace header, fixing the parent-child relationship between client and server ([#4264](https://github.com/getsentry/sentry-dotnet/pull/4264)) diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index 33fca3e622..0b710a99b3 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -118,7 +118,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) context.Items.TryAdd(BaggageHeaderItemKey, baggageHeader); context.Items.TryAdd(TransactionContextItemKey, transactionContext); - hub.ConfigureScope(scope => + hub.ConfigureScope(static (scope, arg) => { // At the point lots of stuff from the request are not yet filled // Identity for example is added later on in the pipeline @@ -131,16 +131,16 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) // when the event fires. Use `activeScope`, not `scope` or `hub`. scope.OnEvaluating += (_, activeScope) => { - SyncOptionsScope(activeScope); - PopulateScope(context, activeScope); + arg.middleware.SyncOptionsScope(activeScope); + arg.middleware.PopulateScope(arg.context, activeScope); }; - }); + }, (middleware: this, context)); // Pre-create the Sentry Event ID and save it on the scope it so it's available throughout the pipeline, // even if there's no event actually being sent to Sentry. This allows for things like a custom exception // handler page to access the event ID, enabling user feedback, etc. var eventId = SentryId.Create(); - hub.ConfigureScope(scope => scope.LastEventId = eventId); + hub.ConfigureScope(static (scope, eventId) => scope.LastEventId = eventId, eventId); try { @@ -167,7 +167,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) { // The middleware pipeline finishes up before the Otel Activity.OnEnd callback is invoked so we need // so save a copy of the scope that can be restored by our SentrySpanProcessor - hub.ConfigureScope(scope => activity.SetFused(scope)); + hub.ConfigureScope(static (scope, activity) => activity.SetFused(scope), activity); } // When an exception was handled by other component (i.e: UseExceptionHandler feature). @@ -179,12 +179,12 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) "The web server likely returned a customized error page as a result of this exception."; #if NET6_0_OR_GREATER - hub.ConfigureScope(scope => + hub.ConfigureScope(static (scope, arg) => { scope.ExceptionProcessors.Add( - new ExceptionHandlerFeatureProcessor(originalMethod, exceptionFeature) + new ExceptionHandlerFeatureProcessor(arg.originalMethod, arg.exceptionFeature) ); - }); + }, (originalMethod, exceptionFeature)); #endif CaptureException(exceptionFeature.Error, eventId, "IExceptionHandlerFeature", description); } diff --git a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs index 8c192a89a1..96f5e5a95b 100644 --- a/src/Sentry.AspNetCore/SentryTracingMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryTracingMiddleware.cs @@ -130,10 +130,7 @@ public async Task InvokeAsync(HttpContext context) // Expose the transaction on the scope so that the user // can retrieve it and start child spans off of it. - hub.ConfigureScope(scope => - { - scope.Transaction = transaction; - }); + hub.ConfigureScope(static (scope, transaction) => scope.Transaction = transaction, transaction); Exception? exception = null; try diff --git a/src/Sentry.Extensions.Logging/SentryLoggerProvider.cs b/src/Sentry.Extensions.Logging/SentryLoggerProvider.cs index 94ca40dd80..454f070dd8 100644 --- a/src/Sentry.Extensions.Logging/SentryLoggerProvider.cs +++ b/src/Sentry.Extensions.Logging/SentryLoggerProvider.cs @@ -48,7 +48,7 @@ internal SentryLoggerProvider( if (hub.IsEnabled) { _scope = hub.PushScope(); - hub.ConfigureScope(s => + hub.ConfigureScope(static s => { if (s.Sdk is { } sdk) { diff --git a/src/Sentry.OpenTelemetry/AspNetCoreEnricher.cs b/src/Sentry.OpenTelemetry/AspNetCoreEnricher.cs index 6d2c53991a..832da26308 100644 --- a/src/Sentry.OpenTelemetry/AspNetCoreEnricher.cs +++ b/src/Sentry.OpenTelemetry/AspNetCoreEnricher.cs @@ -10,13 +10,13 @@ public void Enrich(ISpan span, Activity activity, IHub hub, SentryOptions? optio { if (options?.SendDefaultPii is true) { - hub.ConfigureScope(scope => + hub.ConfigureScope(static (scope, enricher) => { - if (!scope.HasUser() && _userFactory.Create() is { } user) + if (!scope.HasUser() && enricher._userFactory.Create() is { } user) { scope.User = user; } - }); + }, this); } } } diff --git a/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs b/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs index b0b747e2d4..eba966138a 100644 --- a/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs +++ b/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs @@ -172,7 +172,7 @@ private void CreateRootSpan(Activity data) tracer.Contexts.Trace.Origin = OpenTelemetryOrigin; tracer.StartTimestamp = data.StartTimeUtc; } - _hub.ConfigureScope(scope => scope.Transaction = transaction); + _hub.ConfigureScope(static (scope, transaction) => scope.Transaction = transaction, transaction); transaction.SetFused(data); _map[data.SpanId] = transaction; } diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 86eb6a1df5..339c295233 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -30,11 +30,23 @@ public void ConfigureScope(Action configureScope) { } + /// + /// No-Op. + /// + public void ConfigureScope(Action configureScope, TArg arg) + { + } + /// /// No-Op. /// public Task ConfigureScopeAsync(Func configureScope) => Task.CompletedTask; + /// + /// No-Op. + /// + public Task ConfigureScopeAsync(Func configureScope, TArg arg) => Task.CompletedTask; + /// /// No-Op. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index fb9196b718..c5953eeefa 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -39,6 +39,13 @@ private HubAdapter() { } public void ConfigureScope(Action configureScope) => SentrySdk.ConfigureScope(configureScope); + /// + /// Forwards the call to . + /// + [DebuggerStepThrough] + public void ConfigureScope(Action configureScope, TArg arg) + => SentrySdk.ConfigureScope(configureScope, arg); + /// /// Forwards the call to . /// @@ -46,6 +53,13 @@ public void ConfigureScope(Action configureScope) public Task ConfigureScopeAsync(Func configureScope) => SentrySdk.ConfigureScopeAsync(configureScope); + /// + /// Forwards the call to . + /// + [DebuggerStepThrough] + public Task ConfigureScopeAsync(Func configureScope, TArg arg) + => SentrySdk.ConfigureScopeAsync(configureScope, arg); + /// /// Forwards the call to . /// diff --git a/src/Sentry/HubExtensions.cs b/src/Sentry/HubExtensions.cs index a63ef45aa4..736c06ed12 100644 --- a/src/Sentry/HubExtensions.cs +++ b/src/Sentry/HubExtensions.cs @@ -56,14 +56,11 @@ public static ITransactionTracer StartTransaction( /// public static ISpan StartSpan(this IHub hub, string operation, string description) { - ITransactionTracer? currentTransaction = null; - hub.ConfigureScope(s => currentTransaction = s.Transaction); - return currentTransaction is { } transaction + return hub.GetTransaction() is { } transaction ? transaction.StartChild(operation, description) : hub.StartTransaction(operation, description); // this is actually in the wrong order but changing it may break other things } - /// /// Adds a breadcrumb to the current scope. /// @@ -158,8 +155,8 @@ public static void AddBreadcrumb( } hub.ConfigureScope( - s => s.AddBreadcrumb(breadcrumb, hint ?? new SentryHint()) - ); + static (s, arg) => s.AddBreadcrumb(arg.breadcrumb, arg.hint ?? new SentryHint()), + (breadcrumb, hint)); } /// @@ -175,13 +172,13 @@ public static void AddBreadcrumb( /// like Loggers which guarantee log messages are not lost. /// [EditorBrowsable(EditorBrowsableState.Never)] - public static void LockScope(this IHub hub) => hub.ConfigureScope(c => c.Locked = true); + public static void LockScope(this IHub hub) => hub.ConfigureScope(static s => s.Locked = true); /// /// Unlocks the current scope to allow subsequent calls to create new scopes. /// [EditorBrowsable(EditorBrowsableState.Never)] - public static void UnlockScope(this IHub hub) => hub.ConfigureScope(c => c.Locked = false); + public static void UnlockScope(this IHub hub) => hub.ConfigureScope(static s => s.Locked = false); private sealed class LockedScope : IDisposable { @@ -247,6 +244,11 @@ internal static ITransactionTracer StartTransaction( internal static ITransactionTracer? GetTransaction(this IHub hub) { + if (hub is Hub fullHub) + { + return fullHub.ScopeManager.GetCurrent().Key.Transaction; + } + ITransactionTracer? transaction = null; hub.ConfigureScope(scope => transaction = scope.Transaction); return transaction; diff --git a/src/Sentry/ISentryScopeManager.cs b/src/Sentry/ISentryScopeManager.cs index 25b58069db..d48f9ad4c6 100644 --- a/src/Sentry/ISentryScopeManager.cs +++ b/src/Sentry/ISentryScopeManager.cs @@ -10,18 +10,33 @@ namespace Sentry; public interface ISentryScopeManager { /// - /// Configures the current scope. + /// Configures the current scope through the callback. /// - /// The configure scope. + /// The configure scope callback. public void ConfigureScope(Action configureScope); /// - /// Asynchronously configure the current scope. + /// Configures the current scope through the callback. /// - /// The configure scope. + /// The configure scope callback. + /// The argument to pass to the configure scope callback. + public void ConfigureScope(Action configureScope, TArg arg); + + /// + /// Configures the current scope through the callback asynchronously. + /// + /// The configure scope callback. /// A task that completes when the callback is done or a completed task if the SDK is disabled. public Task ConfigureScopeAsync(Func configureScope); + /// + /// Configures the current scope through the callback asynchronously. + /// + /// The configure scope callback. + /// The argument to pass to the configure scope callback. + /// A task that completes when the callback is done or a completed task if the SDK is disabled. + public Task ConfigureScopeAsync(Func configureScope, TArg arg); + /// /// Sets a tag on the current scope. /// diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 9176ca3ea4..2084e20295 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -99,6 +99,18 @@ public void ConfigureScope(Action configureScope) } } + public void ConfigureScope(Action configureScope, TArg arg) + { + try + { + ScopeManager.ConfigureScope(configureScope, arg); + } + catch (Exception e) + { + _options.LogError(e, "Failure to ConfigureScope"); + } + } + public async Task ConfigureScopeAsync(Func configureScope) { try @@ -111,6 +123,18 @@ public async Task ConfigureScopeAsync(Func configureScope) } } + public async Task ConfigureScopeAsync(Func configureScope, TArg arg) + { + try + { + await ScopeManager.ConfigureScopeAsync(configureScope, arg).ConfigureAwait(false); + } + catch (Exception e) + { + _options.LogError(e, "Failure to ConfigureScopeAsync"); + } + } + public void SetTag(string key, string value) => ScopeManager.SetTag(key, value); public void UnsetTag(string key) => ScopeManager.UnsetTag(key); @@ -272,7 +296,7 @@ public TransactionContext ContinueTrace( string? operation = null) { var propagationContext = SentryPropagationContext.CreateFromHeaders(_options.DiagnosticLogger, traceHeader, baggageHeader, _replaySession); - ConfigureScope(scope => scope.SetPropagationContext(propagationContext)); + ConfigureScope(static (scope, propagationContext) => scope.SetPropagationContext(propagationContext), propagationContext); return new TransactionContext( name: name ?? string.Empty, diff --git a/src/Sentry/Internal/SentryScopeManager.cs b/src/Sentry/Internal/SentryScopeManager.cs index 61e68ac1f4..01fdba0303 100644 --- a/src/Sentry/Internal/SentryScopeManager.cs +++ b/src/Sentry/Internal/SentryScopeManager.cs @@ -38,12 +38,24 @@ public void ConfigureScope(Action? configureScope) configureScope?.Invoke(scope); } + public void ConfigureScope(Action? configureScope, TArg arg) + { + var (scope, _) = GetCurrent(); + configureScope?.Invoke(scope, arg); + } + public Task ConfigureScopeAsync(Func? configureScope) { var (scope, _) = GetCurrent(); return configureScope?.Invoke(scope) ?? Task.CompletedTask; } + public Task ConfigureScopeAsync(Func? configureScope, TArg arg) + { + var (scope, _) = GetCurrent(); + return configureScope?.Invoke(scope, arg) ?? Task.CompletedTask; + } + public void SetTag(string key, string value) { var (scope, _) = GetCurrent(); diff --git a/src/Sentry/Internal/UnsampledTransaction.cs b/src/Sentry/Internal/UnsampledTransaction.cs index 20b8ad013c..a14698f7f3 100644 --- a/src/Sentry/Internal/UnsampledTransaction.cs +++ b/src/Sentry/Internal/UnsampledTransaction.cs @@ -64,11 +64,11 @@ public override void Finish() // 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 => + _hub.ConfigureScope(static (scope, transactionTracer) => { - scope.ResetTransaction(this); + scope.ResetTransaction(transactionTracer); scope.SetPropagationContext(new SentryPropagationContext()); - }); + }, this); // Record the discarded events var spanCount = Spans.Count + 1; // 1 for each span + 1 for the transaction itself diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index decd76f6d1..d00b002576 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -381,14 +381,47 @@ public static void ConfigureScope(Action configureScope) => CurrentHub.ConfigureScope(configureScope); /// - /// Configures the scope asynchronously. + /// Configures the scope through the callback. + /// + /// + /// object someValue = ...; + /// SentrySdk.ConfigureScope(static (scope, arg) => scope.SetExtra("key", arg), someValue); + /// + /// /// /// The configure scope callback. - /// The Id of the event. + /// The argument to pass to the configure scope callback. + public static void ConfigureScope(Action configureScope, TArg arg) + => CurrentHub.ConfigureScope(configureScope, arg); + + /// + /// Configures the scope through the callback asynchronously. + /// + /// The configure scope callback. + /// A task that completes when the callback is done or a completed task if the SDK is disabled. [DebuggerStepThrough] public static Task ConfigureScopeAsync(Func configureScope) => CurrentHub.ConfigureScopeAsync(configureScope); + /// + /// Configures the scope through the callback asynchronously. + /// + /// + /// object someValue = ...; + /// SentrySdk.ConfigureScopeAsync(static async (scope, arg) => + /// { + /// scope.SetExtra("key", arg); + /// }, someValue); + /// + /// + /// + /// The configure scope callback. + /// The argument to pass to the configure scope callback. + /// A task that completes when the callback is done or a completed task if the SDK is disabled. + [DebuggerStepThrough] + public static Task ConfigureScopeAsync(Func configureScope, TArg arg) + => CurrentHub.ConfigureScopeAsync(configureScope, arg); + /// /// Sets a tag on the current scope. /// diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index d3f60ceb5f..5966a224da 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -386,11 +386,11 @@ public void Finish() // Clear the transaction from the scope and regenerate the Propagation Context // We do this so new events don't have a trace context that is "older" than the transaction that just finished - _hub.ConfigureScope(scope => + _hub.ConfigureScope(static (scope, transactionTracer) => { - scope.ResetTransaction(this); + scope.ResetTransaction(transactionTracer); scope.SetPropagationContext(new SentryPropagationContext()); - }); + }, this); // Client decides whether to discard this transaction based on sampling _hub.CaptureTransaction(new SentryTransaction(this)); diff --git a/test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs b/test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs index d6cedd9b73..3f53589e67 100644 --- a/test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs +++ b/test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs @@ -31,8 +31,7 @@ private class Fixture public Fixture() { Scope = new(); - Hub.When(hub => hub.ConfigureScope(Arg.Any>())) - .Do(callback => callback.Arg>().Invoke(Scope)); + Hub.SubstituteConfigureScope(Scope); Hub.When(hub => hub.CaptureEvent(Arg.Any(), Arg.Any())) .Do(_ => Scope.Evaluate()); @@ -162,7 +161,17 @@ public async Task InvokeAsync_LocksScope_BeforeConfiguringScope() _fixture.Hub .When(h => h.ConfigureScope(Arg.Any>())) .Do(Callback - .First(c => c.ArgAt>(0)(scope)) + .First(c => c.ArgAt>(0).Invoke(scope)) + .Then(_ => + { + Assert.True(scope.Locked); + verified = true; + })); + + _fixture.Hub + .When(h => h.ConfigureScope(Arg.Any>(), Arg.Any())) + .Do(Callback + .First(c => c.InvokeGenericConfigureScopeMethod(scope)) .Then(_ => { Assert.True(scope.Locked); @@ -182,8 +191,7 @@ public async Task InvokeAsync_OnEvaluating_HttpContextDataSet() const string expectedTraceIdentifier = "trace id"; _ = _fixture.HttpContext.TraceIdentifier.Returns(expectedTraceIdentifier); var scope = new Scope(); - _fixture.Hub.When(h => h.ConfigureScope(Arg.Any>())) - .Do(c => c.Arg>()(scope)); + _fixture.Hub.SubstituteConfigureScope(scope); var sut = _fixture.GetSut(); @@ -200,8 +208,7 @@ public async Task InvokeAsync_OnEvaluatingClonedScope_HttpContextDataSet() const string expectedTraceIdentifier = "trace id"; _ = _fixture.HttpContext.TraceIdentifier.Returns(expectedTraceIdentifier); var scope = new Scope(); - _fixture.Hub.When(h => h.ConfigureScope(Arg.Any>())) - .Do(c => c.Arg>()(scope)); + _fixture.Hub.SubstituteConfigureScope(scope); var sut = _fixture.GetSut(); @@ -664,7 +671,7 @@ public async Task InvokeAsync_InstrumenterOpenTelemetry_SavesScope() // Arrange _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; var scope = new Scope(); - _fixture.Hub.ConfigureScope(Arg.Do>(action => action.Invoke(scope))); + _fixture.Hub.SubstituteConfigureScope(scope); var sut = _fixture.GetSut(); var activity = new Activity("test").Start(); diff --git a/test/Sentry.EntityFramework.Tests/SentryQueryLoggerTests.cs b/test/Sentry.EntityFramework.Tests/SentryQueryLoggerTests.cs index ec1686fa7e..c961a309eb 100644 --- a/test/Sentry.EntityFramework.Tests/SentryQueryLoggerTests.cs +++ b/test/Sentry.EntityFramework.Tests/SentryQueryLoggerTests.cs @@ -8,7 +8,7 @@ public void Log_QueryLogger_CaptureEvent() var scope = new Scope(new SentryOptions()); var hub = Substitute.For(); hub.IsEnabled.Returns(true); - hub.ConfigureScope(Arg.Invoke(scope)); + hub.SubstituteConfigureScope(scope); var expected = new { diff --git a/test/Sentry.Extensions.Logging.Tests/SentryLoggerTests.cs b/test/Sentry.Extensions.Logging.Tests/SentryLoggerTests.cs index 5f06633293..baf89cee44 100644 --- a/test/Sentry.Extensions.Logging.Tests/SentryLoggerTests.cs +++ b/test/Sentry.Extensions.Logging.Tests/SentryLoggerTests.cs @@ -14,7 +14,7 @@ private class Fixture public Fixture() { _ = Hub.IsEnabled.Returns(true); - Hub.ConfigureScope(Arg.Invoke(Scope)); + Hub.SubstituteConfigureScope(Scope); } public SentryLogger GetSut() => new(CategoryName, Options, new MockClock(), Hub); diff --git a/test/Sentry.Log4Net.Tests/SentryAppenderTests.cs b/test/Sentry.Log4Net.Tests/SentryAppenderTests.cs index b4e5b58a89..03cd2501a3 100644 --- a/test/Sentry.Log4Net.Tests/SentryAppenderTests.cs +++ b/test/Sentry.Log4Net.Tests/SentryAppenderTests.cs @@ -16,7 +16,7 @@ private class Fixture public Fixture() { HubAccessor = () => Hub; - Hub.ConfigureScope(Arg.Invoke(Scope)); + Hub.SubstituteConfigureScope(Scope); InitAction = s => { DsnReceivedOnInit = s; diff --git a/test/Sentry.Maui.CommunityToolkit.Mvvm.Tests/MauiCommunityToolkitMvvmEventsBinderTests.cs b/test/Sentry.Maui.CommunityToolkit.Mvvm.Tests/MauiCommunityToolkitMvvmEventsBinderTests.cs index 39d4e9f1bd..1f51552842 100644 --- a/test/Sentry.Maui.CommunityToolkit.Mvvm.Tests/MauiCommunityToolkitMvvmEventsBinderTests.cs +++ b/test/Sentry.Maui.CommunityToolkit.Mvvm.Tests/MauiCommunityToolkitMvvmEventsBinderTests.cs @@ -19,11 +19,7 @@ private class Fixture public Fixture() { Hub = Substitute.For(); - Hub.When(h => h.ConfigureScope(Arg.Any>())) - .Do(c => - { - c.Arg>()(Scope); - }); + Hub.SubstituteConfigureScope(Scope); Options.Debug = true; var logger = Substitute.For(); diff --git a/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs b/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs index 2a84ec92b3..9b050b8bdb 100644 --- a/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs +++ b/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs @@ -17,11 +17,7 @@ private class Fixture public Fixture() { Hub = Substitute.For(); - Hub.When(h => h.ConfigureScope(Arg.Any>())) - .Do(c => - { - c.Arg>()(Scope); - }); + Hub.SubstituteConfigureScope(Scope); Scope.Transaction = Substitute.For(); diff --git a/test/Sentry.NLog.Tests/SentryTargetTests.cs b/test/Sentry.NLog.Tests/SentryTargetTests.cs index f0ef530a68..30fae65e90 100644 --- a/test/Sentry.NLog.Tests/SentryTargetTests.cs +++ b/test/Sentry.NLog.Tests/SentryTargetTests.cs @@ -23,7 +23,7 @@ public Fixture() Hub.IsEnabled.Returns(true); HubAccessor = () => Hub; Scope = new Scope(new SentryOptions()); - Hub.ConfigureScope(Arg.Invoke(Scope)); + Hub.SubstituteConfigureScope(Scope); } public Target GetTarget(bool asyncTarget = false) diff --git a/test/Sentry.OpenTelemetry.Tests/AspNetCoreEnricherTests.cs b/test/Sentry.OpenTelemetry.Tests/AspNetCoreEnricherTests.cs index f1106a2b10..430fd2a769 100644 --- a/test/Sentry.OpenTelemetry.Tests/AspNetCoreEnricherTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/AspNetCoreEnricherTests.cs @@ -9,7 +9,7 @@ public void Enrich_SendDefaultPii_UserOnScope() var scope = new Scope(); var options = new SentryOptions { SendDefaultPii = true }; var hub = Substitute.For(); - hub.ConfigureScope(Arg.Do>(action => action(scope))); + hub.SubstituteConfigureScope(scope); var user = new SentryUser { Id = "foo" }; var userFactory = Substitute.For(); diff --git a/test/Sentry.Serilog.Tests/SentrySinkTests.cs b/test/Sentry.Serilog.Tests/SentrySinkTests.cs index c660fe44e7..ce1011aa2b 100644 --- a/test/Sentry.Serilog.Tests/SentrySinkTests.cs +++ b/test/Sentry.Serilog.Tests/SentrySinkTests.cs @@ -14,7 +14,7 @@ public Fixture() { Hub.IsEnabled.Returns(true); HubAccessor = () => Hub; - Hub.ConfigureScope(Arg.Invoke(Scope)); + Hub.SubstituteConfigureScope(Scope); } public SentrySink GetSut() diff --git a/test/Sentry.Testing/HubSubstituteExtensions.cs b/test/Sentry.Testing/HubSubstituteExtensions.cs new file mode 100644 index 0000000000..d039e92456 --- /dev/null +++ b/test/Sentry.Testing/HubSubstituteExtensions.cs @@ -0,0 +1,27 @@ +namespace Sentry.Testing; + +public static class HubSubstituteExtensions +{ + public static void SubstituteConfigureScope( + this IHub hub, + Scope scope) + { + hub.When(h => h.ConfigureScope(Arg.Any>())) + .Do(c => c.Arg>().Invoke(scope)); + + hub.When(h => h.ConfigureScope(Arg.Any>(), Arg.Any())) + .Do(c => c.InvokeGenericConfigureScopeMethod(scope)); + } + + public static void InvokeGenericConfigureScopeMethod( + this CallInfo c, + Scope scope) + { + // If we use Arg.AnyType to look up the arguments, NSubstitute is unable to find + // them. So as a workaround, we get the arguments directly and use reflection to + // invoke the method. + var action = c[0]; + var arg = c[1]; + action.GetType().GetMethod("Invoke")!.Invoke(action, [scope, arg]); + } +} diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index b9ad168f30..82d7cdf7f7 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -256,7 +256,9 @@ namespace Sentry { void BindClient(Sentry.ISentryClient client); void ConfigureScope(System.Action configureScope); + void ConfigureScope(System.Action configureScope, TArg arg); System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope); + System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg); System.IDisposable PushScope(); System.IDisposable PushScope(TState state); void SetTag(string key, string value); @@ -846,7 +848,9 @@ namespace Sentry public static void CauseCrash(Sentry.CrashType crashType) { } public static void Close() { } public static void ConfigureScope(System.Action configureScope) { } + public static void ConfigureScope(System.Action configureScope, TArg arg) { } public static System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public static System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } public static Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public static Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } public static void EndSession(Sentry.SessionEndStatus status = 0) { } @@ -1358,7 +1362,9 @@ namespace Sentry.Extensibility [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void ConfigureScope(System.Action configureScope) { } + public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } public void Dispose() { } @@ -1407,7 +1413,9 @@ namespace Sentry.Extensibility [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } public void ConfigureScope(System.Action configureScope) { } + public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } public void EndSession(Sentry.SessionEndStatus status = 0) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index b9ad168f30..82d7cdf7f7 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -256,7 +256,9 @@ namespace Sentry { void BindClient(Sentry.ISentryClient client); void ConfigureScope(System.Action configureScope); + void ConfigureScope(System.Action configureScope, TArg arg); System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope); + System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg); System.IDisposable PushScope(); System.IDisposable PushScope(TState state); void SetTag(string key, string value); @@ -846,7 +848,9 @@ namespace Sentry public static void CauseCrash(Sentry.CrashType crashType) { } public static void Close() { } public static void ConfigureScope(System.Action configureScope) { } + public static void ConfigureScope(System.Action configureScope, TArg arg) { } public static System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public static System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } public static Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public static Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } public static void EndSession(Sentry.SessionEndStatus status = 0) { } @@ -1358,7 +1362,9 @@ namespace Sentry.Extensibility [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void ConfigureScope(System.Action configureScope) { } + public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } public void Dispose() { } @@ -1407,7 +1413,9 @@ namespace Sentry.Extensibility [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } public void ConfigureScope(System.Action configureScope) { } + public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } public void EndSession(Sentry.SessionEndStatus status = 0) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 97dac16884..701eaa25b3 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -244,7 +244,9 @@ namespace Sentry { void BindClient(Sentry.ISentryClient client); void ConfigureScope(System.Action configureScope); + void ConfigureScope(System.Action configureScope, TArg arg); System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope); + System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg); System.IDisposable PushScope(); System.IDisposable PushScope(TState state); void SetTag(string key, string value); @@ -827,7 +829,9 @@ namespace Sentry public static void CauseCrash(Sentry.CrashType crashType) { } public static void Close() { } public static void ConfigureScope(System.Action configureScope) { } + public static void ConfigureScope(System.Action configureScope, TArg arg) { } public static System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public static System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } public static Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public static Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } public static void EndSession(Sentry.SessionEndStatus status = 0) { } @@ -1339,7 +1343,9 @@ namespace Sentry.Extensibility [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void ConfigureScope(System.Action configureScope) { } + public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } public void Dispose() { } @@ -1388,7 +1394,9 @@ namespace Sentry.Extensibility [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } public void ConfigureScope(System.Action configureScope) { } + public void ConfigureScope(System.Action configureScope, TArg arg) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } + public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope, TArg arg) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } public void EndSession(Sentry.SessionEndStatus status = 0) { } diff --git a/test/Sentry.Tests/Extensibility/HubAdapterTests.cs b/test/Sentry.Tests/Extensibility/HubAdapterTests.cs index 83de560c53..824b5e08ad 100644 --- a/test/Sentry.Tests/Extensibility/HubAdapterTests.cs +++ b/test/Sentry.Tests/Extensibility/HubAdapterTests.cs @@ -162,8 +162,7 @@ private void TestAddBreadcrumbExtension( const BreadcrumbLevel level = BreadcrumbLevel.Critical; var scope = new Scope(); - Hub.When(h => h.ConfigureScope(Arg.Any>())) - .Do(c => c.Arg>()(scope)); + Hub.SubstituteConfigureScope(scope); action(message, category, type, data, level); diff --git a/test/Sentry.Tests/HubExtensionsTests.cs b/test/Sentry.Tests/HubExtensionsTests.cs index ce64c0f641..c01657890c 100644 --- a/test/Sentry.Tests/HubExtensionsTests.cs +++ b/test/Sentry.Tests/HubExtensionsTests.cs @@ -7,8 +7,7 @@ public class HubExtensionsTests public HubExtensionsTests() { - Sut.When(h => h.ConfigureScope(Arg.Any>())) - .Do(c => c.Arg>()(Scope)); + Sut.SubstituteConfigureScope(Scope); } [Fact] diff --git a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs index 7f210116d8..eb1eaf1bbf 100644 --- a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs +++ b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs @@ -582,18 +582,18 @@ public void Finish_ResetsScopeAndSetsNewPropagationContext() var hub = Substitute.For(); var transaction = new TransactionTracer(hub, "test name", "test op"); - Action capturedAction = null; - hub.ConfigureScope(Arg.Do>(action => capturedAction = action)); + Action capturedAction = null; + hub.ConfigureScope(Arg.Do>(action => capturedAction = action), Arg.Any()); // Act transaction.Finish(); // Assert - hub.Received(1).ConfigureScope(Arg.Any>()); + hub.Received(1).ConfigureScope(Arg.Any>(), Arg.Any()); capturedAction.Should().NotBeNull(); // Sanity Check var mockScope = Substitute.For(); - capturedAction(mockScope); + capturedAction(mockScope, transaction); mockScope.Received(1).ResetTransaction(transaction); mockScope.Received(1).SetPropagationContext(Arg.Any()); diff --git a/test/Sentry.Tests/SentryGraphQlHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryGraphQlHttpMessageHandlerTests.cs index a545f501d1..26b23d13b9 100644 --- a/test/Sentry.Tests/SentryGraphQlHttpMessageHandlerTests.cs +++ b/test/Sentry.Tests/SentryGraphQlHttpMessageHandlerTests.cs @@ -83,8 +83,7 @@ public void HandleResponse_AddsBreadcrumb() var scope = new Scope(); var hub = Substitute.For(); - hub.When(h => h.ConfigureScope(Arg.Any>())) - .Do(c => c.Arg>()(scope)); + hub.SubstituteConfigureScope(scope); var query = ValidQuery; var request = SentryGraphQlTestHelpers.GetRequestQuery(query, url); diff --git a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs index a2783714a9..a15727e7b4 100644 --- a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs +++ b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs @@ -214,8 +214,7 @@ public async Task SendAsync_Executed_BreadcrumbCreated() // Arrange var scope = new Scope(); var hub = Substitute.For(); - hub.When(h => h.ConfigureScope(Arg.Any>())) - .Do(c => c.Arg>()(scope)); + hub.SubstituteConfigureScope(scope); var url = "https://localhost/"; @@ -496,8 +495,7 @@ public void Send_Executed_BreadcrumbCreated() // Arrange var scope = new Scope(); var hub = Substitute.For(); - hub.When(h => h.ConfigureScope(Arg.Any>())) - .Do(c => c.Arg>()(scope)); + hub.SubstituteConfigureScope(scope); var url = "https://localhost/"; diff --git a/test/Sentry.Tests/SentrySdkTests.cs b/test/Sentry.Tests/SentrySdkTests.cs index 49ca287d59..00fcc95b82 100644 --- a/test/Sentry.Tests/SentrySdkTests.cs +++ b/test/Sentry.Tests/SentrySdkTests.cs @@ -439,6 +439,36 @@ public void ConfigureScope_Sync_CallbackNeverInvoked() Assert.False(invoked); } + [Fact] + public void ConfigureScope_SyncWithArg_CallbackNeverInvoked() + { + var invoked = false; + SentrySdk.ConfigureScope((_, _) => invoked = true, "arg"); + Assert.False(invoked); + } + + [Fact] + public void ConfigureScope_SyncWithArg_ArgIsUsed() + { + using var _ = SentrySdk.Init(o => + { + o.Dsn = ValidDsn; + o.AutoSessionTracking = false; + o.BackgroundWorker = Substitute.For(); + o.InitNativeSdks = false; + }); + + const string key = "key"; + const string arg = "arg"; + + SentrySdk.ConfigureScope((s, a) => s.SetTag(key, a), arg); + + string actual = null; + SentrySdk.ConfigureScope(s => actual = s.Tags[key]); + + Assert.Equal(arg, actual); + } + [SkippableFact] public async Task ConfigureScope_OnTask_PropagatedToCaller() { @@ -671,6 +701,48 @@ await SentrySdk.ConfigureScopeAsync(_ => Assert.False(invoked); } + [Fact] + public async Task ConfigureScope_AsyncWithArg_CallbackNeverInvoked() + { + var invoked = false; + await SentrySdk.ConfigureScopeAsync((_, _) => + { + invoked = true; + return Task.CompletedTask; + }, "arg"); + Assert.False(invoked); + } + + [Fact] + public async Task ConfigureScope_AsyncWithArg_ArgIsUsed() + { + using var _ = SentrySdk.Init(o => + { + o.Dsn = ValidDsn; + o.AutoSessionTracking = false; + o.BackgroundWorker = Substitute.For(); + o.InitNativeSdks = false; + }); + + const string key = "key"; + const string arg = "arg"; + + await SentrySdk.ConfigureScopeAsync((s, a) => + { + s.SetTag(key, a); + return Task.CompletedTask; + }, arg); + + string actual = null; + await SentrySdk.ConfigureScopeAsync(s => + { + actual = s.Tags[key]; + return Task.CompletedTask; + }); + + Assert.Equal(arg, actual); + } + [Fact] public void CaptureEvent_Instance_NoOp() => SentrySdk.CaptureEvent(new SentryEvent());