diff --git a/src/Sentry/HubExtensions.cs b/src/Sentry/HubExtensions.cs index 061378afb5..8874741448 100644 --- a/src/Sentry/HubExtensions.cs +++ b/src/Sentry/HubExtensions.cs @@ -2,6 +2,7 @@ using Sentry.Infrastructure; using Sentry.Internal; using Sentry.Internal.Extensions; +using Sentry.Protocol; namespace Sentry; @@ -268,8 +269,17 @@ public LockedScope(IHub hub) public void Dispose() => _scope.Dispose(); } - internal static SentryId CaptureExceptionInternal(this IHub hub, Exception ex) => - hub.CaptureEvent(new SentryEvent(ex)); + internal static SentryId CaptureExceptionInternal(this IHub hub, Exception ex) + { + // Integrations always call `SetSentryMechanism` before calling this method (e.g. WinUI forwards the + // platform's Handled value), so this fallback is defensive only. In practice, it never executes. + // Terminal is left unset (not false) so a missing SetSentryMechanism call still reads as a crash. + if (!ex.Data.Contains(Mechanism.HandledKey)) + { + ex.Data[Mechanism.HandledKey] = false; + } + return hub.CaptureEvent(new SentryEvent(ex)); + } /// /// Captures the exception with a configurable scope callback. @@ -281,6 +291,30 @@ internal static SentryId CaptureExceptionInternal(this IHub hub, Exception ex) = public static SentryId CaptureException(this IHub hub, Exception ex, Action configureScope) => hub.CaptureEvent(new SentryEvent(ex), configureScope); + /// + /// Captures the exception with a configurable scope callback, explicitly marking it as handled or unhandled + /// and whether it terminated the application. + /// + /// The Sentry hub. + /// The exception. + /// Whether the exception was handled. Recorded on the exception, overriding any flag + /// previously set on it, including one set via . + /// Whether the app crashed. Only used when is + /// false. If true, the session ends as crashed and the active transaction is aborted. + /// The callback to configure the scope. + /// The Id of the event + public static SentryId CaptureException(this IHub hub, Exception ex, bool handled, bool terminal, + Action configureScope) + { + if (!hub.IsEnabled) + { + return SentryId.Empty; + } + + ex.RecordMechanismFlags(handled, terminal); + return hub.CaptureEvent(new SentryEvent(ex), configureScope); + } + /// /// Captures feedback from the user. /// diff --git a/src/Sentry/Internal/MainExceptionProcessor.cs b/src/Sentry/Internal/MainExceptionProcessor.cs index 3a5d524df4..8f4899763c 100644 --- a/src/Sentry/Internal/MainExceptionProcessor.cs +++ b/src/Sentry/Internal/MainExceptionProcessor.cs @@ -174,17 +174,12 @@ private static Mechanism GetMechanism(Exception exception, int id, int? parentId mechanism.Handled = handled; exception.Data.Remove(Mechanism.HandledKey); } - else if (exception.StackTrace != null) - { - // The exception was thrown, but it was caught by the user, not an integration. - // Thus, we can mark it as handled. - mechanism.Handled = true; - } else { - // The exception was never thrown. It was just constructed and then captured. - // Thus, it is neither handled nor unhandled. - mechanism.Handled = null; + // https://getsentry.github.io/relay/relay_event_schema/protocol/struct.Mechanism.html#structfield.handled + // "Exceptions captured using capture_exception (called from user code) are handled=true as the user + // explicitly captured the exception (and therefore kind of handled it)." + mechanism.Handled = true; } if (exception.Data[Mechanism.MechanismKey] is string mechanismType) diff --git a/src/Sentry/SentryClientExtensions.cs b/src/Sentry/SentryClientExtensions.cs index abcf1dc099..a9d0d205a8 100644 --- a/src/Sentry/SentryClientExtensions.cs +++ b/src/Sentry/SentryClientExtensions.cs @@ -1,5 +1,6 @@ using Sentry.Extensibility; using Sentry.Internal; +using Sentry.Protocol; namespace Sentry; @@ -18,6 +19,29 @@ public static class SentryClientExtensions public static SentryId CaptureException(this ISentryClient client, Exception ex) => client.IsEnabled ? client.CaptureEvent(new SentryEvent(ex)) : SentryId.Empty; + /// + /// Captures the exception, explicitly marking it as handled or unhandled. + /// + /// The Sentry client. + /// The exception. + /// Whether the exception was handled. Recorded on the exception, overriding any flag + /// previously set on it, including one set via . + /// Whether the app crashed. Only used when is + /// false. If true, the session ends as crashed, aborting the active transaction on + /// clients. + /// The Id of the event + public static SentryId CaptureException(this ISentryClient client, Exception ex, bool handled, + bool terminal = false) + { + if (!client.IsEnabled) + { + return SentryId.Empty; + } + + ex.RecordMechanismFlags(handled, terminal); + return client.CaptureEvent(new SentryEvent(ex)); + } + /// /// Captures a message. /// diff --git a/src/Sentry/SentryExceptionExtensions.cs b/src/Sentry/SentryExceptionExtensions.cs index 822ad8c856..da78c8fea3 100644 --- a/src/Sentry/SentryExceptionExtensions.cs +++ b/src/Sentry/SentryExceptionExtensions.cs @@ -65,4 +65,26 @@ public static void SetSentryMechanism(this Exception ex, string type, string? de ex.Data[Mechanism.TerminalKey] = terminal; } } + + /// + /// Records the mechanism flags for an explicit user capture, fully overriding any previously set values. + /// + /// + /// Terminal is only meaningful when the exception is unhandled, so it is removed when + /// is true rather than left over from an earlier + /// call. Mirrors that method's null-removes semantics. + /// + internal static void RecordMechanismFlags(this Exception ex, bool handled, bool terminal) + { + ex.Data[Mechanism.HandledKey] = handled; + + if (handled) + { + ex.Data.Remove(Mechanism.TerminalKey); + } + else + { + ex.Data[Mechanism.TerminalKey] = terminal; + } + } } diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index fcc557ec75..3405cb1069 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -524,6 +524,19 @@ public static SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Action CurrentHub.CaptureException(exception); + /// + /// Captures the exception, explicitly marking it as handled or unhandled. + /// + /// The exception. + /// Whether the exception was handled. Recorded on the exception, overriding any flag + /// previously set on it, including one set via . + /// Whether the app crashed. Only used when is + /// false. If true, the session ends as crashed and the active transaction is aborted. + /// The Id of the event. + [DebuggerStepThrough] + public static SentryId CaptureException(Exception exception, bool handled, bool terminal = false) + => CurrentHub.CaptureException(exception, handled, terminal); + /// /// Captures the exception with a configurable scope. /// @@ -537,6 +550,25 @@ public static SentryId CaptureException(Exception exception) public static SentryId CaptureException(Exception exception, Action configureScope) => CurrentHub.CaptureException(exception, configureScope); + /// + /// Captures the exception with a configurable scope, explicitly marking it as handled or unhandled and + /// whether it terminated the application. + /// + /// + /// This allows modifying a scope without affecting other events. + /// + /// The exception. + /// Whether the exception was handled. Recorded on the exception, overriding any flag + /// previously set on it, including one set via . + /// Whether the app crashed. Only used when is + /// false. If true, the session ends as crashed and the active transaction is aborted. + /// The callback to configure the scope. + /// The Id of the event. + [DebuggerStepThrough] + public static SentryId CaptureException(Exception exception, bool handled, bool terminal, + Action configureScope) + => CurrentHub.CaptureException(exception, handled, terminal, configureScope); + /// /// Captures the message. /// diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt index 290207f39e..3c95d6b346 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet10_0.verified.txt @@ -13,6 +13,7 @@ Value: my exception, Mechanism: { Type: generic, + Handled: true, Synthetic: false, IsExceptionGroup: false, Data: { diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt index 290207f39e..3c95d6b346 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet8_0.verified.txt @@ -13,6 +13,7 @@ Value: my exception, Mechanism: { Type: generic, + Handled: true, Synthetic: false, IsExceptionGroup: false, Data: { diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt index 290207f39e..3c95d6b346 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.DotNet9_0.verified.txt @@ -13,6 +13,7 @@ Value: my exception, Mechanism: { Type: generic, + Handled: true, Synthetic: false, IsExceptionGroup: false, Data: { diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt index 51fc328f6a..96a1009270 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsEfAsync.Net4_8.verified.txt @@ -13,6 +13,7 @@ Value: my exception, Mechanism: { Type: generic, + Handled: true, Synthetic: false, IsExceptionGroup: false, Data: { diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt index fbf3184173..b86c6fec54 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.RecordsSqlAsync.verified.txt @@ -13,6 +13,7 @@ Value: my exception, Mechanism: { Type: generic, + Handled: true, Synthetic: false, IsExceptionGroup: false, Data: { diff --git a/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt b/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt index 42af21ba1b..b807582bf8 100644 --- a/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt +++ b/test/Sentry.EntityFramework.Tests/IntegrationTests.Simple.verified.txt @@ -13,6 +13,7 @@ Value: my exception, Mechanism: { Type: generic, + Handled: true, Synthetic: false, IsExceptionGroup: false, Data: { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index b9fb58dba4..1b849bc057 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -148,6 +148,7 @@ namespace Sentry public static void AddBreadcrumb(this Sentry.IHub hub, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void AddBreadcrumb(this Sentry.IHub hub, Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static Sentry.SentryId CaptureException(this Sentry.IHub hub, System.Exception ex, System.Action configureScope) { } + public static Sentry.SentryId CaptureException(this Sentry.IHub hub, System.Exception ex, bool handled, bool terminal, System.Action configureScope) { } public static Sentry.SentryId CaptureFeedback(this Sentry.IHub hub, Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.IHub hub, string message, System.Action configureScope, Sentry.SentryLevel level = 1) { } public static void LockScope(this Sentry.IHub hub) { } @@ -491,6 +492,7 @@ namespace Sentry public static class SentryClientExtensions { public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } + public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex, bool handled, bool terminal = false) { } public static Sentry.SentryId CaptureFeedback(this Sentry.ISentryClient client, Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(this Sentry.ISentryClient client, string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId? associatedEventId = default, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } @@ -978,6 +980,8 @@ namespace Sentry public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } public static Sentry.SentryId CaptureException(System.Exception exception) { } public static Sentry.SentryId CaptureException(System.Exception exception, System.Action configureScope) { } + public static Sentry.SentryId CaptureException(System.Exception exception, bool handled, bool terminal = false) { } + public static Sentry.SentryId CaptureException(System.Exception exception, bool handled, bool terminal, System.Action configureScope) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, out Sentry.CaptureFeedbackResult result, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index b9fb58dba4..1b849bc057 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -148,6 +148,7 @@ namespace Sentry public static void AddBreadcrumb(this Sentry.IHub hub, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void AddBreadcrumb(this Sentry.IHub hub, Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static Sentry.SentryId CaptureException(this Sentry.IHub hub, System.Exception ex, System.Action configureScope) { } + public static Sentry.SentryId CaptureException(this Sentry.IHub hub, System.Exception ex, bool handled, bool terminal, System.Action configureScope) { } public static Sentry.SentryId CaptureFeedback(this Sentry.IHub hub, Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.IHub hub, string message, System.Action configureScope, Sentry.SentryLevel level = 1) { } public static void LockScope(this Sentry.IHub hub) { } @@ -491,6 +492,7 @@ namespace Sentry public static class SentryClientExtensions { public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } + public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex, bool handled, bool terminal = false) { } public static Sentry.SentryId CaptureFeedback(this Sentry.ISentryClient client, Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(this Sentry.ISentryClient client, string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId? associatedEventId = default, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } @@ -978,6 +980,8 @@ namespace Sentry public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } public static Sentry.SentryId CaptureException(System.Exception exception) { } public static Sentry.SentryId CaptureException(System.Exception exception, System.Action configureScope) { } + public static Sentry.SentryId CaptureException(System.Exception exception, bool handled, bool terminal = false) { } + public static Sentry.SentryId CaptureException(System.Exception exception, bool handled, bool terminal, System.Action configureScope) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, out Sentry.CaptureFeedbackResult result, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index b9fb58dba4..1b849bc057 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -148,6 +148,7 @@ namespace Sentry public static void AddBreadcrumb(this Sentry.IHub hub, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void AddBreadcrumb(this Sentry.IHub hub, Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static Sentry.SentryId CaptureException(this Sentry.IHub hub, System.Exception ex, System.Action configureScope) { } + public static Sentry.SentryId CaptureException(this Sentry.IHub hub, System.Exception ex, bool handled, bool terminal, System.Action configureScope) { } public static Sentry.SentryId CaptureFeedback(this Sentry.IHub hub, Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.IHub hub, string message, System.Action configureScope, Sentry.SentryLevel level = 1) { } public static void LockScope(this Sentry.IHub hub) { } @@ -491,6 +492,7 @@ namespace Sentry public static class SentryClientExtensions { public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } + public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex, bool handled, bool terminal = false) { } public static Sentry.SentryId CaptureFeedback(this Sentry.ISentryClient client, Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(this Sentry.ISentryClient client, string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId? associatedEventId = default, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } @@ -978,6 +980,8 @@ namespace Sentry public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } public static Sentry.SentryId CaptureException(System.Exception exception) { } public static Sentry.SentryId CaptureException(System.Exception exception, System.Action configureScope) { } + public static Sentry.SentryId CaptureException(System.Exception exception, bool handled, bool terminal = false) { } + public static Sentry.SentryId CaptureException(System.Exception exception, bool handled, bool terminal, System.Action configureScope) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, out Sentry.CaptureFeedbackResult result, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 2e103ee02d..2cf30af33f 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -136,6 +136,7 @@ namespace Sentry public static void AddBreadcrumb(this Sentry.IHub hub, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void AddBreadcrumb(this Sentry.IHub hub, Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static Sentry.SentryId CaptureException(this Sentry.IHub hub, System.Exception ex, System.Action configureScope) { } + public static Sentry.SentryId CaptureException(this Sentry.IHub hub, System.Exception ex, bool handled, bool terminal, System.Action configureScope) { } public static Sentry.SentryId CaptureFeedback(this Sentry.IHub hub, Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.IHub hub, string message, System.Action configureScope, Sentry.SentryLevel level = 1) { } public static void LockScope(this Sentry.IHub hub) { } @@ -479,6 +480,7 @@ namespace Sentry public static class SentryClientExtensions { public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } + public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex, bool handled, bool terminal = false) { } public static Sentry.SentryId CaptureFeedback(this Sentry.ISentryClient client, Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(this Sentry.ISentryClient client, string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId? associatedEventId = default, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } @@ -959,6 +961,8 @@ namespace Sentry public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } public static Sentry.SentryId CaptureException(System.Exception exception) { } public static Sentry.SentryId CaptureException(System.Exception exception, System.Action configureScope) { } + public static Sentry.SentryId CaptureException(System.Exception exception, bool handled, bool terminal = false) { } + public static Sentry.SentryId CaptureException(System.Exception exception, bool handled, bool terminal, System.Action configureScope) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public static Sentry.SentryId CaptureFeedback(Sentry.SentryFeedback feedback, out Sentry.CaptureFeedbackResult result, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } diff --git a/test/Sentry.Tests/HubExtensionsTests.cs b/test/Sentry.Tests/HubExtensionsTests.cs index 83295c2134..661bde2766 100644 --- a/test/Sentry.Tests/HubExtensionsTests.cs +++ b/test/Sentry.Tests/HubExtensionsTests.cs @@ -56,6 +56,116 @@ public void UnlockScope_UnlocksScope() Assert.False(Scope.Locked); } + [Fact] + public void CaptureException_ScopeCallback_NoHandledArgument_DoesNotSetHandledFlag() + { + // Arrange + _ = Sut.IsEnabled.Returns(true); + var ex = new Exception(); + + // Act + _ = Sut.CaptureException(ex, _ => { }); + + // Assert + Assert.False(ex.Data.Contains(Mechanism.HandledKey)); + _ = Sut.Received(1).CaptureEvent(Arg.Any(), Arg.Any>()); + } + + [Fact] + public void CaptureException_ScopeCallback_NoHandledArgument_PresetFlagIsPreserved() + { + // Arrange + _ = Sut.IsEnabled.Returns(true); + var ex = new Exception(); + ex.SetSentryMechanism("SomeMechanism", handled: false); + + // Act + _ = Sut.CaptureException(ex, _ => { }); + + // Assert + Assert.Equal(false, ex.Data[Mechanism.HandledKey]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CaptureException_ScopeCallback_ExplicitHandled_RecordsFlagOnException(bool handled) + { + // Arrange + _ = Sut.IsEnabled.Returns(true); + var ex = new Exception(); + + // Act + _ = Sut.CaptureException(ex, handled, terminal: false, _ => { }); + + // Assert + Assert.Equal(handled, ex.Data[Mechanism.HandledKey]); + _ = Sut.Received(1).CaptureEvent(Arg.Any(), Arg.Any>()); + } + + [Fact] + public void CaptureException_ScopeCallback_ExplicitHandled_OverridesFlagSetBySetSentryMechanism() + { + // Arrange + _ = Sut.IsEnabled.Returns(true); + var ex = new Exception(); + ex.SetSentryMechanism("SomeMechanism", handled: true); + + // Act + _ = Sut.CaptureException(ex, handled: false, terminal: false, _ => { }); + + // Assert + Assert.Equal(false, ex.Data[Mechanism.HandledKey]); + } + + [Fact] + public void CaptureException_ScopeCallback_ExplicitHandled_DisabledHub_DoesNotRecordFlagOnException() + { + // Arrange + _ = Sut.IsEnabled.Returns(false); + var ex = new Exception(); + + // Act + var id = Sut.CaptureException(ex, handled: false, terminal: false, _ => { }); + + // Assert + Assert.Equal(SentryId.Empty, id); + Assert.False(ex.Data.Contains(Mechanism.HandledKey)); + _ = Sut.DidNotReceive().CaptureEvent(Arg.Any(), Arg.Any>()); + } + + [Fact] + public void CaptureExceptionInternal_NoPresetFlag_DefaultsToUnhandled() + { + // Arrange + var ex = new Exception(); + + // Act + _ = Sut.CaptureExceptionInternal(ex); + + // Assert + Assert.Equal(false, ex.Data[Mechanism.HandledKey]); + _ = Sut.Received(1).CaptureEvent(Arg.Any()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CaptureExceptionInternal_PresetFlag_IsNotOverwritten(bool handled) + { + // Arrange + // E.g. WinUIUnhandledExceptionIntegration forwards the platform's Handled value + // via SetSentryMechanism before capturing. + var ex = new Exception(); + ex.SetSentryMechanism("SomeMechanism", handled: handled); + + // Act + _ = Sut.CaptureExceptionInternal(ex); + + // Assert + Assert.Equal(handled, ex.Data[Mechanism.HandledKey]); + } + [Fact] public void AddBreadcrumb_MinimalArguments_CreatesBreadcrumb() { @@ -160,4 +270,38 @@ public void GetTraceIdAndSpanId_WithoutIds_ShouldBeUnreachable() traceId.Should().Be(SentryId.Empty); spanId.Should().BeNull(); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CaptureException_ScopeCallback_ExplicitTerminal_RecordsTerminalFlagOnException(bool terminal) + { + // Arrange + _ = Sut.IsEnabled.Returns(true); + var ex = new Exception(); + + // Act + _ = Sut.CaptureException(ex, handled: false, terminal, _ => { }); + + // Assert + Assert.Equal(false, ex.Data[Mechanism.HandledKey]); + Assert.Equal(terminal, ex.Data[Mechanism.TerminalKey]); + _ = Sut.Received(1).CaptureEvent(Arg.Any(), Arg.Any>()); + } + + [Fact] + public void CaptureException_ScopeCallback_ExplicitTerminal_DisabledHub_DoesNotRecordFlagsOnException() + { + // Arrange + _ = Sut.IsEnabled.Returns(false); + var ex = new Exception(); + + // Act + var id = Sut.CaptureException(ex, handled: false, terminal: true, _ => { }); + + // Assert + Assert.Equal(SentryId.Empty, id); + Assert.False(ex.Data.Contains(Mechanism.HandledKey)); + Assert.False(ex.Data.Contains(Mechanism.TerminalKey)); + } } diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index eea49f6250..fcc1bf8f54 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -545,6 +545,59 @@ public void CaptureEvent_TerminalUnhandledException_AbortsActiveTransaction() transaction.IsFinished.Should().BeTrue(); } + [Fact] + public void CaptureException_Terminal_AbortsActiveTransaction() + { + // Arrange + _fixture.Options.TracesSampleRate = 1.0; + var hub = _fixture.GetSut(); + + var transaction = hub.StartTransaction("test", "operation"); + hub.ConfigureScope(scope => scope.Transaction = transaction); + + // Act + hub.CaptureException(new Exception("test"), handled: false, terminal: true, _ => { }); + + // Assert + transaction.Status.Should().Be(SpanStatus.Aborted); + transaction.IsFinished.Should().BeTrue(); + } + + [Fact] + public void CaptureException_TerminalWithoutScopeCallback_AbortsActiveTransaction() + { + // Arrange + _fixture.Options.TracesSampleRate = 1.0; + var hub = _fixture.GetSut(); + + var transaction = hub.StartTransaction("test", "operation"); + hub.ConfigureScope(scope => scope.Transaction = transaction); + + // Act + hub.CaptureException(new Exception("test"), handled: false, terminal: true); + + // Assert + transaction.Status.Should().Be(SpanStatus.Aborted); + transaction.IsFinished.Should().BeTrue(); + } + + [Fact] + public void CaptureException_NonTerminal_LeavesActiveTransactionRunning() + { + // Arrange + _fixture.Options.TracesSampleRate = 1.0; + var hub = _fixture.GetSut(); + + var transaction = hub.StartTransaction("test", "operation"); + hub.ConfigureScope(scope => scope.Transaction = transaction); + + // Act + hub.CaptureException(new Exception("test"), handled: false, terminal: false, _ => { }); + + // Assert + transaction.IsFinished.Should().BeFalse(); + } + [Fact] public void CaptureEvent_TerminalUnhandledException_DoesNotAbortOpenTelemetryTransaction() { diff --git a/test/Sentry.Tests/Internals/MainExceptionProcessorTests.CreateSentryException_Aggregate.verified.txt b/test/Sentry.Tests/Internals/MainExceptionProcessorTests.CreateSentryException_Aggregate.verified.txt index 520f33f3fa..824a7e3709 100644 --- a/test/Sentry.Tests/Internals/MainExceptionProcessorTests.CreateSentryException_Aggregate.verified.txt +++ b/test/Sentry.Tests/Internals/MainExceptionProcessorTests.CreateSentryException_Aggregate.verified.txt @@ -5,6 +5,7 @@ Mechanism: { Type: chained, Source: InnerExceptions[1], + Handled: true, Synthetic: false, IsExceptionGroup: false, ExceptionId: 2, @@ -20,6 +21,7 @@ Mechanism: { Type: chained, Source: InnerExceptions[0], + Handled: true, Synthetic: false, IsExceptionGroup: false, ExceptionId: 1, diff --git a/test/Sentry.Tests/Internals/MainExceptionProcessorTests.cs b/test/Sentry.Tests/Internals/MainExceptionProcessorTests.cs index 14151c21a8..9a65da4dbf 100644 --- a/test/Sentry.Tests/Internals/MainExceptionProcessorTests.cs +++ b/test/Sentry.Tests/Internals/MainExceptionProcessorTests.cs @@ -56,8 +56,9 @@ public void Process_ExceptionWithout_Handled() sut.Process(exp, evt); + // No integration set the flag, so this was captured by user code, which counts as handled. Assert.NotNull(evt.SentryExceptions); - Assert.Single(evt.SentryExceptions, p => p.Mechanism?.Handled == null); + Assert.Single(evt.SentryExceptions, p => p.Mechanism?.Handled == true); } [Fact] diff --git a/test/Sentry.Tests/SentryClientExtensionsTests.cs b/test/Sentry.Tests/SentryClientExtensionsTests.cs index 637e05078f..b9996b7656 100644 --- a/test/Sentry.Tests/SentryClientExtensionsTests.cs +++ b/test/Sentry.Tests/SentryClientExtensionsTests.cs @@ -22,6 +22,98 @@ public void CaptureException_EnabledClient_CapturesEvent() _ = _sut.Received(1).CaptureEvent(Arg.Any()); } + [Fact] + public void CaptureException_NoHandledArgument_DoesNotSetHandledFlag() + { + // Arrange + _ = _sut.IsEnabled.Returns(true); + var ex = new Exception(); + + // Act + _ = _sut.CaptureException(ex); + + // Assert + Assert.False(ex.Data.Contains(Mechanism.HandledKey)); + } + + [Fact] + public void CaptureException_NoHandledArgument_PresetFlagIsPreserved() + { + // Arrange + _ = _sut.IsEnabled.Returns(true); + var ex = new Exception(); + ex.SetSentryMechanism("SomeMechanism", handled: false); + + // Act + _ = _sut.CaptureException(ex); + + // Assert + Assert.Equal(false, ex.Data[Mechanism.HandledKey]); + } + + [Fact] + public void CaptureException_DisabledClient_NoHandledArgument_DoesNotMutateException() + { + // Arrange + _ = _sut.IsEnabled.Returns(false); + var ex = new Exception(); + + // Act + var id = _sut.CaptureException(ex); + + // Assert + _ = _sut.DidNotReceive().CaptureEvent(Arg.Any()); + Assert.Equal(default, id); + Assert.False(ex.Data.Contains(Mechanism.HandledKey)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CaptureException_ExplicitHandled_RecordsFlagOnException(bool handled) + { + // Arrange + _ = _sut.IsEnabled.Returns(true); + var ex = new Exception(); + + // Act + _ = _sut.CaptureException(ex, handled); + + // Assert + Assert.Equal(handled, ex.Data[Mechanism.HandledKey]); + } + + [Fact] + public void CaptureException_ExplicitHandled_OverridesFlagSetBySetSentryMechanism() + { + // Arrange + _ = _sut.IsEnabled.Returns(true); + var ex = new Exception(); + ex.SetSentryMechanism("SomeMechanism", handled: false); + + // Act + _ = _sut.CaptureException(ex, handled: true); + + // Assert + Assert.Equal(true, ex.Data[Mechanism.HandledKey]); + } + + [Fact] + public void CaptureException_DisabledClient_ExplicitHandled_DoesNotMutateException() + { + // Arrange + _ = _sut.IsEnabled.Returns(false); + var ex = new Exception(); + + // Act + var id = _sut.CaptureException(ex, handled: false); + + // Assert + _ = _sut.DidNotReceive().CaptureEvent(Arg.Any()); + Assert.Equal(default, id); + Assert.False(ex.Data.Contains(Mechanism.HandledKey)); + } + [Fact] public void CaptureMessage_DisabledClient_DoesNotCaptureEvent() { @@ -117,4 +209,67 @@ public async Task Flush_WithTimeoutSpecified_UsesThatTimeout() await _sut.Received(1).FlushAsync(timeout); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CaptureException_ExplicitTerminal_RecordsTerminalFlagOnException(bool terminal) + { + // Arrange + _ = _sut.IsEnabled.Returns(true); + var ex = new Exception(); + + // Act + _ = _sut.CaptureException(ex, handled: false, terminal: terminal); + + // Assert + Assert.Equal(false, ex.Data[Mechanism.HandledKey]); + Assert.Equal(terminal, ex.Data[Mechanism.TerminalKey]); + } + + [Fact] + public void CaptureException_NoTerminalArgument_DefaultsToNonTerminal() + { + // Arrange + _ = _sut.IsEnabled.Returns(true); + var ex = new Exception(); + + // Act + _ = _sut.CaptureException(ex, handled: false); + + // Assert + Assert.Equal(false, ex.Data[Mechanism.TerminalKey]); + } + + [Fact] + public void CaptureException_HandledTrue_ClearsPresetTerminalFlag() + { + // Arrange + _ = _sut.IsEnabled.Returns(true); + var ex = new Exception(); + ex.SetSentryMechanism("SomeMechanism", handled: false, terminal: true); + + // Act + _ = _sut.CaptureException(ex, handled: true); + + // Assert + Assert.Equal(true, ex.Data[Mechanism.HandledKey]); + Assert.False(ex.Data.Contains(Mechanism.TerminalKey)); + } + + [Fact] + public void CaptureException_ExplicitTerminal_DisabledClient_DoesNotRecordFlagsOnException() + { + // Arrange + _ = _sut.IsEnabled.Returns(false); + var ex = new Exception(); + + // Act + var id = _sut.CaptureException(ex, handled: false, terminal: true); + + // Assert + Assert.Equal(default, id); + Assert.False(ex.Data.Contains(Mechanism.HandledKey)); + Assert.False(ex.Data.Contains(Mechanism.TerminalKey)); + } } diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index b8121a4ff2..8329dad9a1 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -2095,4 +2095,64 @@ public void CaptureEvent_ActiveSessionAndNonTerminalUnhandledException_SessionMa // Assert _fixture.SessionManager.Received().MarkSessionAsUnhandled(); } + + [Fact] + public void CaptureException_Handled_ReportsError() + { + // Arrange + var client = _fixture.GetSut(); + + // Act + client.CaptureException(new Exception(), handled: true); + + // Assert + _fixture.SessionManager.Received(1).ReportError(); + _fixture.SessionManager.DidNotReceive().EndSession(SessionEndStatus.Crashed); + _fixture.SessionManager.DidNotReceive().MarkSessionAsUnhandled(); + } + + [Fact] + public void CaptureException_UnhandledNonTerminal_MarksSessionUnhandledWithoutEndingIt() + { + // Arrange + var client = _fixture.GetSut(); + + // Act + client.CaptureException(new Exception(), handled: false, terminal: false); + + // Assert + _fixture.SessionManager.Received(1).MarkSessionAsUnhandled(); + _fixture.SessionManager.DidNotReceive().EndSession(SessionEndStatus.Crashed); + } + + [Fact] + public void CaptureException_UnhandledTerminal_EndsSessionAsCrashed() + { + // Arrange + var client = _fixture.GetSut(); + + // Act + client.CaptureException(new Exception(), handled: false, terminal: true); + + // Assert + _fixture.SessionManager.Received(1).EndSession(SessionEndStatus.Crashed); + _fixture.SessionManager.DidNotReceive().MarkSessionAsUnhandled(); + } + + [Fact] + public void CaptureException_UnhandledWithoutTerminalArgument_DoesNotEndSessionAsCrashed() + { + // This is the behaviour change the terminal parameter exists for: a manual unhandled capture + // must not dent the crash-free-sessions rate when nothing actually crashed. + + // Arrange + var client = _fixture.GetSut(); + + // Act + client.CaptureException(new Exception(), handled: false); + + // Assert + _fixture.SessionManager.Received(1).MarkSessionAsUnhandled(); + _fixture.SessionManager.DidNotReceive().EndSession(SessionEndStatus.Crashed); + } } diff --git a/test/Sentry.Tests/SentrySdkTests.cs b/test/Sentry.Tests/SentrySdkTests.cs index 45750ebd93..e9b98ac2e3 100644 --- a/test/Sentry.Tests/SentrySdkTests.cs +++ b/test/Sentry.Tests/SentrySdkTests.cs @@ -672,6 +672,210 @@ public void CaptureException_WithConfiguredScope_ScopeCallbackGetsInvoked() Assert.True(scopeCallbackWasInvoked); } + [Fact] + public void CaptureException_NoHandledArgument_NeverThrown_DefaultsToHandled() + { + SentryEvent captured = null; + using var _ = SentrySdk.Init(o => + { + o.Dsn = ValidDsn; + o.AutoSessionTracking = false; + o.BackgroundWorker = Substitute.For(); + o.InitNativeSdks = false; + o.SetBeforeSend(e => + { + captured = e; + return e; + }); + }); + + SentrySdk.CaptureException(new Exception("test")); + + // The plain overload writes no flag; MainExceptionProcessor infers handled for a never-thrown exception. + Assert.NotNull(captured); + Assert.True(Assert.Single(captured.SentryExceptions!).Mechanism!.Handled); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CaptureException_ExplicitHandled_SetsMechanismHandledOnEvent(bool handled) + { + SentryEvent captured = null; + using var _ = SentrySdk.Init(o => + { + o.Dsn = ValidDsn; + o.AutoSessionTracking = false; + o.BackgroundWorker = Substitute.For(); + o.InitNativeSdks = false; + o.SetBeforeSend(e => + { + captured = e; + return e; + }); + }); + + SentrySdk.CaptureException(new Exception("test"), handled); + + Assert.NotNull(captured); + Assert.Equal(handled, Assert.Single(captured.SentryExceptions!).Mechanism!.Handled); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CaptureException_WithConfiguredScope_ExplicitHandled_SetsMechanismHandledOnEvent(bool handled) + { + SentryEvent captured = null; + using var _ = SentrySdk.Init(o => + { + o.Dsn = ValidDsn; + o.AutoSessionTracking = false; + o.BackgroundWorker = Substitute.For(); + o.InitNativeSdks = false; + o.SetBeforeSend(e => + { + captured = e; + return e; + }); + }); + + SentrySdk.CaptureException(new Exception("test"), handled, terminal: false, s => s.SetTag("scope-callback", "ran")); + + Assert.NotNull(captured); + Assert.Equal(handled, Assert.Single(captured.SentryExceptions!).Mechanism!.Handled); + Assert.Equal("ran", captured.Tags["scope-callback"]); + } + + [Fact] + public void CaptureException_ExplicitHandled_OverridesFlagSetBySetSentryMechanism() + { + SentryEvent captured = null; + using var _ = SentrySdk.Init(o => + { + o.Dsn = ValidDsn; + o.AutoSessionTracking = false; + o.BackgroundWorker = Substitute.For(); + o.InitNativeSdks = false; + o.SetBeforeSend(e => + { + captured = e; + return e; + }); + }); + + var ex = new Exception("test"); + ex.SetSentryMechanism("SomeMechanism", handled: false); + + SentrySdk.CaptureException(ex, handled: true); + + Assert.NotNull(captured); + Assert.True(Assert.Single(captured.SentryExceptions!).Mechanism!.Handled); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CaptureException_ExplicitTerminal_SetsMechanismTerminalOnEvent(bool terminal) + { + SentryEvent captured = null; + using var _ = SentrySdk.Init(o => + { + o.Dsn = ValidDsn; + o.AutoSessionTracking = false; + o.BackgroundWorker = Substitute.For(); + o.InitNativeSdks = false; + o.SetBeforeSend(e => + { + captured = e; + return e; + }); + }); + + SentrySdk.CaptureException(new Exception("test"), handled: false, terminal: terminal); + + Assert.NotNull(captured); + var mechanism = Assert.Single(captured.SentryExceptions!).Mechanism!; + Assert.False(mechanism.Handled); + Assert.Equal(terminal, mechanism.Terminal); + } + + [Fact] + public void CaptureException_NoTerminalArgument_SetsMechanismTerminalToFalse() + { + SentryEvent captured = null; + using var _ = SentrySdk.Init(o => + { + o.Dsn = ValidDsn; + o.AutoSessionTracking = false; + o.BackgroundWorker = Substitute.For(); + o.InitNativeSdks = false; + o.SetBeforeSend(e => + { + captured = e; + return e; + }); + }); + + SentrySdk.CaptureException(new Exception("test"), handled: false); + + Assert.NotNull(captured); + Assert.False(Assert.Single(captured.SentryExceptions!).Mechanism!.Terminal); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CaptureException_WithConfiguredScope_ExplicitTerminal_SetsMechanismTerminalOnEvent(bool terminal) + { + SentryEvent captured = null; + using var _ = SentrySdk.Init(o => + { + o.Dsn = ValidDsn; + o.AutoSessionTracking = false; + o.BackgroundWorker = Substitute.For(); + o.InitNativeSdks = false; + o.SetBeforeSend(e => + { + captured = e; + return e; + }); + }); + + SentrySdk.CaptureException(new Exception("test"), handled: false, terminal, _ => { }); + + Assert.NotNull(captured); + Assert.Equal(terminal, Assert.Single(captured.SentryExceptions!).Mechanism!.Terminal); + } + + [Fact] + public void CaptureException_HandledTrue_LeavesMechanismTerminalUnset() + { + SentryEvent captured = null; + using var _ = SentrySdk.Init(o => + { + o.Dsn = ValidDsn; + o.AutoSessionTracking = false; + o.BackgroundWorker = Substitute.For(); + o.InitNativeSdks = false; + o.SetBeforeSend(e => + { + captured = e; + return e; + }); + }); + + var ex = new Exception("test"); + ex.SetSentryMechanism("SomeMechanism", handled: false, terminal: true); + + SentrySdk.CaptureException(ex, handled: true); + + Assert.NotNull(captured); + var mechanism = Assert.Single(captured.SentryExceptions!).Mechanism!; + Assert.True(mechanism.Handled); + Assert.Null(mechanism.Terminal); + } + [Fact] public void CaptureMessage_WithConfiguredScope_ScopeCallbackGetsInvoked() {