diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b7ee7380..eb8c957329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Added `CaptureFeedback` overload with `configureScope` parameter ([#4073](https://github.com/getsentry/sentry-dotnet/pull/4073)) - Custom SessionReplay masks in MAUI Android apps ([#4121](https://github.com/getsentry/sentry-dotnet/pull/4121)) ### Fixes diff --git a/samples/Sentry.Samples.AspNetCore.Mvc/Controllers/HomeController.cs b/samples/Sentry.Samples.AspNetCore.Mvc/Controllers/HomeController.cs index ef3a27275b..e39d037fbe 100644 --- a/samples/Sentry.Samples.AspNetCore.Mvc/Controllers/HomeController.cs +++ b/samples/Sentry.Samples.AspNetCore.Mvc/Controllers/HomeController.cs @@ -202,7 +202,7 @@ public async Task SubmitFeedback(FeedbackModel feedback) hint.AddAttachment(memoryStream.ToArray(), feedback.Screenshot.FileName, AttachmentType.Default, "image/png"); } - SentrySdk.CaptureFeedback(sentryFeedback, null, hint); + SentrySdk.CaptureFeedback(sentryFeedback, hint: hint); ViewBag.Message = "Feedback submitted successfully!"; return View("Index"); } diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 6c18af6e45..a5d7541dde 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -147,6 +147,13 @@ public bool CaptureEnvelope(Envelope envelope) /// public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null, SentryHint? hint = null) => SentryId.Empty; + /// + /// No-Op. + /// + public void CaptureFeedback(SentryFeedback feedback, Action configureScope, SentryHint? hint = null) + { + } + /// /// No-Op. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 61d715d341..b21eb369ee 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -222,6 +222,14 @@ public SentryId CaptureEvent(SentryEvent evt, Scope? scope) public SentryId CaptureEvent(SentryEvent evt, Scope? scope, SentryHint? hint = null) => SentrySdk.CaptureEvent(evt, scope, hint); + /// + /// Forwards the call to . + /// + [DebuggerStepThrough] + [EditorBrowsable(EditorBrowsableState.Never)] + public void CaptureFeedback(SentryFeedback feedback, Action configureScope, SentryHint? hint = null) + => SentrySdk.CaptureFeedback(feedback, configureScope, hint); + /// /// Forwards the call to . /// diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index f7a609805b..abf722c89d 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -116,4 +116,12 @@ public TransactionContext ContinueTrace( /// The callback to configure the scope. /// public SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Action configureScope); + + /// + /// Captures feedback from the user. + /// + /// The feedback to send to Sentry. + /// Callback method to configure the scope. + /// An optional hint providing high level context for the source of the event, including attachments + public void CaptureFeedback(SentryFeedback feedback, Action configureScope, SentryHint? hint = null); } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index e7234b6356..cc9cbd3ef3 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -506,6 +506,26 @@ private SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Scope scope) } } + public void CaptureFeedback(SentryFeedback feedback, Action configureScope, SentryHint? hint = null) + { + if (!IsEnabled) + { + return; + } + + try + { + var clonedScope = CurrentScope.Clone(); + configureScope(clonedScope); + + CaptureFeedback(feedback, clonedScope, hint); + } + catch (Exception e) + { + _options.LogError(e, "Failure to capture feedback"); + } + } + public void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, SentryHint? hint = null) { if (!IsEnabled) @@ -513,8 +533,15 @@ public void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, Sentry return; } - scope ??= CurrentScope; - CurrentClient.CaptureFeedback(feedback, scope, hint); + try + { + scope ??= CurrentScope; + CurrentClient.CaptureFeedback(feedback, scope, hint); + } + catch (Exception e) + { + _options.LogError(e, "Failure to capture feedback"); + } } #if MEMORY_DUMP_SUPPORTED diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 401a0fa6f0..0670f31729 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -481,6 +481,13 @@ public static SentryId CaptureMessage(string message, SentryLevel level = Sentry public static SentryId CaptureMessage(string message, Action configureScope, SentryLevel level = SentryLevel.Info) => CurrentHub.CaptureMessage(message, configureScope, level); + /// + /// Captures feedback from the user. + /// + [DebuggerStepThrough] + public static void CaptureFeedback(SentryFeedback feedback, Action configureScope, SentryHint? hint = null) + => CurrentHub.CaptureFeedback(feedback, configureScope, hint); + /// /// Captures feedback from the user. /// diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 93d72d716c..925a97874c 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -212,6 +212,7 @@ namespace Sentry void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); + void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null); Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null); Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null); void EndSession(Sentry.SessionEndStatus status = 0); @@ -824,6 +825,7 @@ namespace Sentry public static Sentry.SentryId CaptureException(System.Exception exception) { } public static Sentry.SentryId CaptureException(System.Exception exception, System.Action configureScope) { } public static void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public static void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public static void CaptureFeedback(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(string message, Sentry.SentryLevel level = 1) { } public static Sentry.SentryId CaptureMessage(string message, System.Action configureScope, Sentry.SentryLevel level = 1) { } @@ -1334,6 +1336,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } @@ -1380,6 +1383,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } public Sentry.SentryId CaptureException(System.Exception exception) { } public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 93d72d716c..925a97874c 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -212,6 +212,7 @@ namespace Sentry void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); + void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null); Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null); Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null); void EndSession(Sentry.SessionEndStatus status = 0); @@ -824,6 +825,7 @@ namespace Sentry public static Sentry.SentryId CaptureException(System.Exception exception) { } public static Sentry.SentryId CaptureException(System.Exception exception, System.Action configureScope) { } public static void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public static void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public static void CaptureFeedback(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(string message, Sentry.SentryLevel level = 1) { } public static Sentry.SentryId CaptureMessage(string message, System.Action configureScope, Sentry.SentryLevel level = 1) { } @@ -1334,6 +1336,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } @@ -1380,6 +1383,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } public Sentry.SentryId CaptureException(System.Exception exception) { } public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 75fa5ad89d..583b80f350 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -200,6 +200,7 @@ namespace Sentry void BindException(System.Exception exception, Sentry.ISpan span); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); + void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null); Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null); Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null); void EndSession(Sentry.SessionEndStatus status = 0); @@ -805,6 +806,7 @@ namespace Sentry public static Sentry.SentryId CaptureException(System.Exception exception) { } public static Sentry.SentryId CaptureException(System.Exception exception, System.Action configureScope) { } public static void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public static void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public static void CaptureFeedback(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(string message, Sentry.SentryLevel level = 1) { } public static Sentry.SentryId CaptureMessage(string message, System.Action configureScope, Sentry.SentryLevel level = 1) { } @@ -1315,6 +1317,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } @@ -1361,6 +1364,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope) { } public Sentry.SentryId CaptureException(System.Exception exception) { } public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, System.Action configureScope, Sentry.SentryHint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } diff --git a/test/Sentry.Tests/Extensibility/HubAdapterTests.cs b/test/Sentry.Tests/Extensibility/HubAdapterTests.cs index cee0344ed2..83de560c53 100644 --- a/test/Sentry.Tests/Extensibility/HubAdapterTests.cs +++ b/test/Sentry.Tests/Extensibility/HubAdapterTests.cs @@ -28,6 +28,23 @@ public void CaptureEvent_WithScope_MockInvoked() Hub.Received(1).CaptureEvent(expectedEvent, expectedScope); } + [Fact] + public void CaptureFeedback_MockInvoked() + { + var expected = new SentryFeedback("foo"); + HubAdapter.Instance.CaptureFeedback(expected); + Hub.Received(1).CaptureFeedback(expected); + } + + [Fact] + public void CaptureFeedback_WithScope_MockInvoked() + { + var expectedEvent = new SentryFeedback("foo"); + var expectedScope = new Scope(); + HubAdapter.Instance.CaptureFeedback(expectedEvent, expectedScope); + Hub.Received(1).CaptureFeedback(expectedEvent, expectedScope); + } + [Fact] public void CaptureException_MockInvoked() { diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index f0c141617e..88d330f950 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1596,6 +1596,27 @@ public void CaptureFeedback_HubEnabled(bool enabled) _fixture.Client.Received(enabled ? 1 : 0).CaptureFeedback(Arg.Any(), Arg.Any(), Arg.Any()); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CaptureFeedback_ConfigureScope_ScopeApplied(bool enabled) + { + // Arrange + var hub = _fixture.GetSut(); + if (!enabled) + { + hub.Dispose(); + } + + var feedback = new SentryFeedback("Test feedback"); + + // Act + hub.CaptureFeedback(feedback, s => s.SetTag("foo", "bar")); + + // Assert + _fixture.Client.Received(enabled ? 1 : 0).CaptureFeedback(Arg.Any(), Arg.Is(s => s.Tags["foo"] == "bar"), Arg.Any()); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/test/Sentry.Tests/SentrySdkTests.cs b/test/Sentry.Tests/SentrySdkTests.cs index f88876e914..ed99d7a746 100644 --- a/test/Sentry.Tests/SentrySdkTests.cs +++ b/test/Sentry.Tests/SentrySdkTests.cs @@ -554,6 +554,23 @@ public void CaptureEvent_WithConfiguredScope_ScopeCallbackGetsInvoked() Assert.True(scopeCallbackWasInvoked); } + [Fact] + public void CaptureFeedback_WithConfiguredScope_ScopeCallbackGetsInvoked() + { + using var _ = SentrySdk.Init(o => + { + o.Dsn = ValidDsn; + o.AutoSessionTracking = false; + o.BackgroundWorker = Substitute.For(); + o.InitNativeSdks = false; + }); + + var scopeCallbackWasInvoked = false; + SentrySdk.CaptureFeedback(new SentryFeedback("Foo"), _ => scopeCallbackWasInvoked = true); + + Assert.True(scopeCallbackWasInvoked); + } + [Fact] public void CaptureException_WithConfiguredScope_ScopeCallbackGetsInvoked() {