From 0ab721978baeba95b9e85e6736cfd1a3b342d5ab Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 19 Feb 2025 21:52:38 +1300 Subject: [PATCH 01/20] Create SentryFeedback.cs --- src/Sentry/SentryFeedback.cs | 87 ++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/Sentry/SentryFeedback.cs diff --git a/src/Sentry/SentryFeedback.cs b/src/Sentry/SentryFeedback.cs new file mode 100644 index 0000000000..eb55f1be4f --- /dev/null +++ b/src/Sentry/SentryFeedback.cs @@ -0,0 +1,87 @@ +using Sentry.Extensibility; +using Sentry.Internal.Extensions; + +namespace Sentry; + +/// +/// Sentry User Feedback. +/// +public sealed class SentryFeedback : ISentryJsonSerializable +{ + // final String? replayId; + // final String? url; + // final SentryId? associatedEventId; + + /// + /// Message containing the user's feedback. + /// + public string Message { get; } + + /// + /// The name of the user. + /// + public string? ContactEmail { get; } + + /// + /// The name of the user. + /// + public string? Name { get; } + + /// + /// Optional ID of the Replay session associated with the feedback. + /// + public string? ReplayId { get; } + + /// + /// The name of the user. + /// + public string? Url { get; } + + /// + /// Optional ID of the event that the user feedback is associated with. + /// + public SentryId AssociatedEventId { get; } + + /// + /// Initializes an instance of . + /// + public SentryFeedback(string message, string? contactEmail, string? name, string? replayId, string? url, SentryId eventId) + { + Message = message; + ContactEmail = contactEmail; + Name = name; + ReplayId = replayId; + Url = url; + AssociatedEventId = eventId; + } + + /// + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteStartObject(); + + writer.WriteString("message", Message); + writer.WriteStringIfNotWhiteSpace("contact_email", ContactEmail); + writer.WriteStringIfNotWhiteSpace("name", Name); + writer.WriteStringIfNotWhiteSpace("replay_id", ReplayId); + writer.WriteStringIfNotWhiteSpace("url", Url); + writer.WriteSerializable("associated_event_id", AssociatedEventId, logger); + + writer.WriteEndObject(); + } + + /// + /// Parses from JSON. + /// + public static SentryFeedback FromJson(JsonElement json) + { + var message = json.GetPropertyOrNull("message")?.GetString() ?? ""; + var contactEmail = json.GetPropertyOrNull("contact_email")?.GetString(); + var name = json.GetPropertyOrNull("name")?.GetString(); + var replayId = json.GetPropertyOrNull("replay_id")?.GetString(); + var url = json.GetPropertyOrNull("url")?.GetString(); + var eventId = json.GetPropertyOrNull("associated_event_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; + + return new SentryFeedback(message, contactEmail, name, replayId, url, eventId); + } +} From 965de1260c454c76934f235e0c1e00b30e4fe6ff Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 20 Feb 2025 10:54:53 +1300 Subject: [PATCH 02/20] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index def04db62a..58cc91a9ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Support sending User Feedback without errors/exceptions ([#3981](https://github.com/getsentry/sentry-dotnet/pull/3981)) + ### Fixes - Add Azure Function UseSentry overloads for easier wire ups ([#3971](https://github.com/getsentry/sentry-dotnet/pull/3971)) From adb7c0450020ec997c00a60654d5683e530b4c09 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 20 Feb 2025 14:39:10 +1300 Subject: [PATCH 03/20] Added feedback to SentryContexts --- src/Sentry/SentryContexts.cs | 9 ++++ src/Sentry/SentryFeedback.cs | 95 +++++++++++++++++++++++++++--------- 2 files changed, 80 insertions(+), 24 deletions(-) diff --git a/src/Sentry/SentryContexts.cs b/src/Sentry/SentryContexts.cs index a9a2ed3efb..3b2da09a98 100644 --- a/src/Sentry/SentryContexts.cs +++ b/src/Sentry/SentryContexts.cs @@ -30,6 +30,11 @@ public sealed class SentryContexts : IDictionary, ISentryJsonSer /// public Device Device => _innerDictionary.GetOrCreate(Device.Type); + /// + /// Holds user feedback. + /// + public SentryFeedback Feedback => _innerDictionary.GetOrCreate(SentryFeedback.Type); + /// /// Defines the operating system. /// @@ -147,6 +152,10 @@ public static SentryContexts FromJson(JsonElement json) { result[name] = Device.FromJson(value); } + else if (string.Equals(type, SentryFeedback.Type, StringComparison.OrdinalIgnoreCase)) + { + result[name] = SentryFeedback.FromJson(value); + } else if (string.Equals(type, OperatingSystem.Type, StringComparison.OrdinalIgnoreCase)) { result[name] = OperatingSystem.FromJson(value); diff --git a/src/Sentry/SentryFeedback.cs b/src/Sentry/SentryFeedback.cs index eb55f1be4f..ec5da90c30 100644 --- a/src/Sentry/SentryFeedback.cs +++ b/src/Sentry/SentryFeedback.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal; using Sentry.Internal.Extensions; namespace Sentry; @@ -6,58 +7,53 @@ namespace Sentry; /// /// Sentry User Feedback. /// -public sealed class SentryFeedback : ISentryJsonSerializable +public sealed class SentryFeedback : ISentryJsonSerializable, ICloneable, IUpdatable { - // final String? replayId; - // final String? url; - // final SentryId? associatedEventId; + /// + /// Tells Sentry which type of context this is. + /// + internal const string Type = "feedback"; /// /// Message containing the user's feedback. /// - public string Message { get; } + public string Message { get; set; } = string.Empty; /// /// The name of the user. /// - public string? ContactEmail { get; } + public string? ContactEmail { get; set; } /// /// The name of the user. /// - public string? Name { get; } + public string? Name { get; set; } /// /// Optional ID of the Replay session associated with the feedback. /// - public string? ReplayId { get; } + public string? ReplayId { get; set; } /// /// The name of the user. /// - public string? Url { get; } + public string? Url { get; set; } /// /// Optional ID of the event that the user feedback is associated with. /// - public SentryId AssociatedEventId { get; } - - /// - /// Initializes an instance of . - /// - public SentryFeedback(string message, string? contactEmail, string? name, string? replayId, string? url, SentryId eventId) - { - Message = message; - ContactEmail = contactEmail; - Name = name; - ReplayId = replayId; - Url = url; - AssociatedEventId = eventId; - } + public SentryId AssociatedEventId { get; set; } /// public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { + if (string.IsNullOrEmpty(Message)) + { + logger?.LogWarning("Feedback message is empty - Feedback will be serialized as null"); + writer.WriteNullValue(); + return; + } + writer.WriteStartObject(); writer.WriteString("message", Message); @@ -82,6 +78,57 @@ public static SentryFeedback FromJson(JsonElement json) var url = json.GetPropertyOrNull("url")?.GetString(); var eventId = json.GetPropertyOrNull("associated_event_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; - return new SentryFeedback(message, contactEmail, name, replayId, url, eventId); + return new SentryFeedback + { + Message = message, + ContactEmail = contactEmail, + Name = name, + ReplayId = replayId, + Url = url, + AssociatedEventId = eventId + }; + } + + internal SentryFeedback Clone() => ((ICloneable)this).Clone(); + + SentryFeedback ICloneable.Clone() + => new() + { + Message = Message, + ContactEmail = ContactEmail, + Name = Name, + ReplayId = ReplayId, + Url = Url, + AssociatedEventId = AssociatedEventId + }; + + /// + /// Updates this instance with data from the properties in the , + /// unless there is already a value in the existing property. + /// + void UpdateFrom(SentryFeedback source) => ((IUpdatable)this).UpdateFrom(source); + + void IUpdatable.UpdateFrom(SentryFeedback source) + { + if (string.IsNullOrEmpty(Message)) + { + Message = source.Message; + } + ContactEmail ??= source.ContactEmail; + Name ??= source.Name; + ReplayId ??= source.ReplayId; + Url ??= source.Url; + if (AssociatedEventId == SentryId.Empty) + { + AssociatedEventId = source.AssociatedEventId; + } + } + + void IUpdatable.UpdateFrom(object source) + { + if (source is SentryFeedback runtime) + { + ((IUpdatable)this).UpdateFrom(runtime); + } } } From 486f65c3cf7ed3fffcb61940dac12586f700eaa9 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 20 Feb 2025 15:00:17 +1300 Subject: [PATCH 04/20] Verify files --- .../ApiApprovalTests.Run.DotNet8_0.verified.txt | 13 +++++++++++++ .../ApiApprovalTests.Run.DotNet9_0.verified.txt | 13 +++++++++++++ .../ApiApprovalTests.Run.Net4_8.verified.txt | 13 +++++++++++++ 3 files changed, 39 insertions(+) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 2a4a0e61a3..baed422441 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -491,6 +491,7 @@ namespace Sentry public Sentry.Protocol.Browser Browser { get; } public int Count { get; } public Sentry.Protocol.Device Device { get; } + public Sentry.SentryFeedback Feedback { get; } public Sentry.Protocol.Gpu Gpu { get; } public bool IsReadOnly { get; } public object this[string key] { get; set; } @@ -549,6 +550,18 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryEvent FromJson(System.Text.Json.JsonElement json) { } } + public sealed class SentryFeedback : Sentry.ISentryJsonSerializable + { + public SentryFeedback() { } + public Sentry.SentryId AssociatedEventId { get; set; } + public string? ContactEmail { get; set; } + public string Message { get; set; } + public string? Name { get; set; } + public string? ReplayId { get; set; } + public string? Url { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryFeedback FromJson(System.Text.Json.JsonElement json) { } + } public class SentryGraphQLHttpMessageHandler : Sentry.SentryMessageHandler { public SentryGraphQLHttpMessageHandler(System.Net.Http.HttpMessageHandler? innerHandler = null, Sentry.IHub? hub = null) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 2a4a0e61a3..baed422441 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -491,6 +491,7 @@ namespace Sentry public Sentry.Protocol.Browser Browser { get; } public int Count { get; } public Sentry.Protocol.Device Device { get; } + public Sentry.SentryFeedback Feedback { get; } public Sentry.Protocol.Gpu Gpu { get; } public bool IsReadOnly { get; } public object this[string key] { get; set; } @@ -549,6 +550,18 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryEvent FromJson(System.Text.Json.JsonElement json) { } } + public sealed class SentryFeedback : Sentry.ISentryJsonSerializable + { + public SentryFeedback() { } + public Sentry.SentryId AssociatedEventId { get; set; } + public string? ContactEmail { get; set; } + public string Message { get; set; } + public string? Name { get; set; } + public string? ReplayId { get; set; } + public string? Url { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryFeedback FromJson(System.Text.Json.JsonElement json) { } + } public class SentryGraphQLHttpMessageHandler : Sentry.SentryMessageHandler { public SentryGraphQLHttpMessageHandler(System.Net.Http.HttpMessageHandler? innerHandler = null, Sentry.IHub? hub = null) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 2730a34f6d..bdbd610d64 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -479,6 +479,7 @@ namespace Sentry public Sentry.Protocol.Browser Browser { get; } public int Count { get; } public Sentry.Protocol.Device Device { get; } + public Sentry.SentryFeedback Feedback { get; } public Sentry.Protocol.Gpu Gpu { get; } public bool IsReadOnly { get; } public object this[string key] { get; set; } @@ -537,6 +538,18 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryEvent FromJson(System.Text.Json.JsonElement json) { } } + public sealed class SentryFeedback : Sentry.ISentryJsonSerializable + { + public SentryFeedback() { } + public Sentry.SentryId AssociatedEventId { get; set; } + public string? ContactEmail { get; set; } + public string Message { get; set; } + public string? Name { get; set; } + public string? ReplayId { get; set; } + public string? Url { get; set; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.SentryFeedback FromJson(System.Text.Json.JsonElement json) { } + } public class SentryGraphQLHttpMessageHandler : Sentry.SentryMessageHandler { public SentryGraphQLHttpMessageHandler(System.Net.Http.HttpMessageHandler? innerHandler = null, Sentry.IHub? hub = null) { } From 48c364ca1952a35bdf37ff72338de61d1bfd844e Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 20 Feb 2025 02:48:06 +0000 Subject: [PATCH 05/20] Format code --- src/Sentry/SentryFeedback.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry/SentryFeedback.cs b/src/Sentry/SentryFeedback.cs index ec5da90c30..3370742391 100644 --- a/src/Sentry/SentryFeedback.cs +++ b/src/Sentry/SentryFeedback.cs @@ -106,7 +106,7 @@ SentryFeedback ICloneable.Clone() /// Updates this instance with data from the properties in the , /// unless there is already a value in the existing property. /// - void UpdateFrom(SentryFeedback source) => ((IUpdatable)this).UpdateFrom(source); + private void UpdateFrom(SentryFeedback source) => ((IUpdatable)this).UpdateFrom(source); void IUpdatable.UpdateFrom(SentryFeedback source) { From 8abc13e35697d9b7b7fb673e0bc989fef2df5222 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 21 Feb 2025 13:42:53 +1300 Subject: [PATCH 06/20] Implemented envelopes and client/hub API --- src/Sentry/Extensibility/DisabledHub.cs | 7 ++ src/Sentry/Extensibility/HubAdapter.cs | 8 ++ src/Sentry/ISentryClient.cs | 8 ++ .../Extensions/CollectionsExtensions.cs | 18 ++++ src/Sentry/Internal/Hub.cs | 12 +++ src/Sentry/Protocol/Envelopes/Envelope.cs | 95 ++++++++++++++----- src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 14 +++ src/Sentry/SentryClient.cs | 36 +++++++ src/Sentry/SentryContexts.cs | 14 ++- src/Sentry/SentryFeedback.cs | 69 ++++---------- src/Sentry/SentrySdk.cs | 7 ++ 11 files changed, 212 insertions(+), 76 deletions(-) diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 3ed44e3a09..a4592f7401 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, Scope? scope = null, SentryHint? hint = null) + { + } + /// /// No-Op. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 5a4f01b62e..fdbaf661eb 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, Scope? scope = null, SentryHint? hint = null) + => SentrySdk.CaptureFeedback(feedback, scope, hint); + /// /// Forwards the call to . /// diff --git a/src/Sentry/ISentryClient.cs b/src/Sentry/ISentryClient.cs index e3e12fe191..c40196852f 100644 --- a/src/Sentry/ISentryClient.cs +++ b/src/Sentry/ISentryClient.cs @@ -28,6 +28,14 @@ public interface ISentryClient /// The Id of the event. SentryId CaptureEvent(SentryEvent evt, Scope? scope = null, SentryHint? hint = null); + /// + /// Captures feedback from the user. + /// + /// The feedback to send to Sentry. + /// An optional scope to be applied to the event. + /// An optional hint providing high level context for the source of the event + void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, SentryHint? hint = null); + /// /// Captures a user feedback. /// diff --git a/src/Sentry/Internal/Extensions/CollectionsExtensions.cs b/src/Sentry/Internal/Extensions/CollectionsExtensions.cs index 19277a632a..8d7b6f7bdf 100644 --- a/src/Sentry/Internal/Extensions/CollectionsExtensions.cs +++ b/src/Sentry/Internal/Extensions/CollectionsExtensions.cs @@ -17,6 +17,24 @@ public static TValue GetOrCreate( throw new($"Expected a type of {typeof(TValue)} to exist for the key '{key}'. Instead found a {value.GetType()}. The likely cause of this is that the value for '{key}' has been incorrectly set to an instance of a different type."); } + public static TValue? TryGetValue( + this ConcurrentDictionary dictionary, + string key) + where TValue : class + { + if (!dictionary.TryGetValue(key, out var value)) + { + return null; + } + + if (value is TValue casted) + { + return casted; + } + + throw new($"Expected a type of {typeof(TValue)} to exist for the key '{key}'. Instead found a {value.GetType()}. The likely cause of this is that the value for '{key}' has been incorrectly set to an instance of a different type."); + } + public static void TryCopyTo(this IDictionary from, IDictionary to) where TKey : notnull { diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index a564ab1791..0b91fa1731 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -506,6 +506,18 @@ private SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Scope scope) } } + public void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, SentryHint? hint = null) + { + if (!IsEnabled) + { + return; + } + + scope ??= CurrentScope; + CurrentClient.CaptureFeedback(feedback, scope, hint); + scope.SessionUpdate = null; + } + #if MEMORY_DUMP_SUPPORTED internal void CaptureHeapDump(string dumpFile) { diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 29ad6a8e07..6e9ef12ba9 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -248,31 +248,78 @@ public static Envelope FromEvent( continue; } - try - { - // We pull the stream out here so we can length check - // to avoid adding an invalid attachment - var stream = attachment.Content.GetStream(); - if (stream.TryGetLength() != 0) - { - items.Add(EnvelopeItem.FromAttachment(attachment, stream)); - } - else - { - // We would normally dispose the stream when we dispose the envelope item - // But in this case, we need to explicitly dispose here or we will be leaving - // the stream open indefinitely. - stream.Dispose(); - - logger?.LogWarning("Did not add '{0}' to envelope because the stream was empty.", - attachment.FileName); - } - } - catch (Exception exception) - { - logger?.LogError(exception, "Failed to add attachment: {0}.", attachment.FileName); - } + AddEnvelopeItemFromAttachment(items, attachment, logger); + } + } + + if (sessionUpdate is not null) + { + items.Add(EnvelopeItem.FromSession(sessionUpdate)); + } + + return new Envelope(eventId, header, items); + } + + private static void AddEnvelopeItemFromAttachment(List items, SentryAttachment attachment, + IDiagnosticLogger? logger) + { + try + { + // We pull the stream out here so we can length check + // to avoid adding an invalid attachment + var stream = attachment.Content.GetStream(); + if (stream.TryGetLength() != 0) + { + items.Add(EnvelopeItem.FromAttachment(attachment, stream)); } + else + { + // We would normally dispose the stream when we dispose the envelope item + // But in this case, we need to explicitly dispose here or we will be leaving + // the stream open indefinitely. + stream.Dispose(); + + logger?.LogWarning("Did not add '{0}' to envelope because the stream was empty.", + attachment.FileName); + } + } + catch (Exception exception) + { + logger?.LogError(exception, "Failed to add attachment: {0}.", attachment.FileName); + } + } + + /// + /// Creates an envelope that contains a single feedback event. + /// + public static Envelope FromFeedback( + SentryEvent @event, + IDiagnosticLogger? logger = null, + IReadOnlyCollection? attachments = null, + SessionUpdate? sessionUpdate = null) + { + if (@event.Contexts.Feedback == null) + { + throw new ArgumentException("Unable to create envelope - the event does not contain any feedback."); + } + + var eventId = @event.EventId; + var header = CreateHeader(eventId, @event.DynamicSamplingContext); + + var items = new List + { + EnvelopeItem.FromFeedback(@event) + }; + + if (attachments is { Count: > 0 }) + { + if (attachments.Count > 1) + { + logger?.LogWarning("Feedback can only contain one attachment. Discarding {0} additional attachments.", + attachments.Count - 1); + } + + AddEnvelopeItemFromAttachment(items, attachments.First(), logger); } if (sessionUpdate is not null) diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 9954de43d6..f34f5261be 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -13,6 +13,7 @@ public sealed class EnvelopeItem : ISerializable, IDisposable private const string TypeKey = "type"; internal const string TypeValueEvent = "event"; + internal const string TypeValueFeedback = "feedback"; internal const string TypeValueUserReport = "user_report"; internal const string TypeValueTransaction = "transaction"; internal const string TypeValueSpan = "span"; @@ -216,6 +217,19 @@ public static EnvelopeItem FromEvent(SentryEvent @event) return new EnvelopeItem(header, new JsonSerializable(@event)); } + /// + /// Creates an from a feedback . + /// + public static EnvelopeItem FromFeedback(SentryEvent @event) + { + var header = new Dictionary(1, StringComparer.Ordinal) + { + [TypeKey] = TypeValueFeedback + }; + + return new EnvelopeItem(header, new JsonSerializable(@event)); + } + /// /// Creates an from . /// diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 7d5b071dc5..9db5772edc 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -81,6 +81,42 @@ public SentryId CaptureEvent(SentryEvent? @event, Scope? scope = null, SentryHin } } + /// + public void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, SentryHint? hint = null) + { + if (string.IsNullOrEmpty(feedback.Message)) + { + // Ignore the user feedback if EventId is empty + _options.LogWarning("Feedback dropped due to empty message."); + return; + } + + scope ??= new Scope(_options); + hint ??= new SentryHint(); + hint.AddAttachmentsFromScope(scope); + + _options.LogInfo("Capturing event."); + + var evt = new SentryEvent { Level = SentryLevel.Info }; + evt.Contexts.Feedback = feedback; + // type: 'feedback', + + // Evaluate and copy before invoking the callback + scope.Evaluate(); + scope.Apply(evt); + + if (scope.Level != null && scope.Level != SentryLevel.Info) + { + // Level on scope takes precedence over the one on event + _options.LogInfo("Overriding level set on feedback event '{0}' with level set on scope '{1}'.", evt.Level, scope.Level); + evt.Level = scope.Level; + } + + var attachments = hint.Attachments.ToList(); + var envelope = Envelope.FromFeedback(evt, _options.DiagnosticLogger, attachments, scope.SessionUpdate); + CaptureEnvelope(envelope); + } + /// public void CaptureUserFeedback(UserFeedback userFeedback) { diff --git a/src/Sentry/SentryContexts.cs b/src/Sentry/SentryContexts.cs index 3b2da09a98..b0abb14227 100644 --- a/src/Sentry/SentryContexts.cs +++ b/src/Sentry/SentryContexts.cs @@ -33,7 +33,19 @@ public sealed class SentryContexts : IDictionary, ISentryJsonSer /// /// Holds user feedback. /// - public SentryFeedback Feedback => _innerDictionary.GetOrCreate(SentryFeedback.Type); + public SentryFeedback? Feedback + { + get => _innerDictionary.TryGetValue(SentryFeedback.Type); + set + { + if (value is null) + { + _innerDictionary.TryRemove(SentryFeedback.Type, out _); + return; + } + _innerDictionary[SentryFeedback.Type] = value; + } + } /// /// Defines the operating system. diff --git a/src/Sentry/SentryFeedback.cs b/src/Sentry/SentryFeedback.cs index ec5da90c30..deaad2ab3d 100644 --- a/src/Sentry/SentryFeedback.cs +++ b/src/Sentry/SentryFeedback.cs @@ -7,7 +7,7 @@ namespace Sentry; /// /// Sentry User Feedback. /// -public sealed class SentryFeedback : ISentryJsonSerializable, ICloneable, IUpdatable +public sealed class SentryFeedback : ISentryJsonSerializable, ICloneable { /// /// Tells Sentry which type of context this is. @@ -44,12 +44,25 @@ public sealed class SentryFeedback : ISentryJsonSerializable, ICloneable public SentryId AssociatedEventId { get; set; } + /// + /// Creates an instance of . + /// + public SentryFeedback(string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, SentryId associatedEventId = default) + { + Message = message; + ContactEmail = contactEmail; + Name = name; + ReplayId = replayId; + Url = url; + AssociatedEventId = associatedEventId; + } + /// public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { if (string.IsNullOrEmpty(Message)) { - logger?.LogWarning("Feedback message is empty - Feedback will be serialized as null"); + logger?.LogWarning("Feedback message is empty - serializing as null"); writer.WriteNullValue(); return; } @@ -71,64 +84,18 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) /// public static SentryFeedback FromJson(JsonElement json) { - var message = json.GetPropertyOrNull("message")?.GetString() ?? ""; + var message = json.GetPropertyOrNull("message")?.GetString() ?? ""; var contactEmail = json.GetPropertyOrNull("contact_email")?.GetString(); var name = json.GetPropertyOrNull("name")?.GetString(); var replayId = json.GetPropertyOrNull("replay_id")?.GetString(); var url = json.GetPropertyOrNull("url")?.GetString(); var eventId = json.GetPropertyOrNull("associated_event_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; - return new SentryFeedback - { - Message = message, - ContactEmail = contactEmail, - Name = name, - ReplayId = replayId, - Url = url, - AssociatedEventId = eventId - }; + return new SentryFeedback(message, contactEmail, name, replayId, url, eventId); } internal SentryFeedback Clone() => ((ICloneable)this).Clone(); SentryFeedback ICloneable.Clone() - => new() - { - Message = Message, - ContactEmail = ContactEmail, - Name = Name, - ReplayId = ReplayId, - Url = Url, - AssociatedEventId = AssociatedEventId - }; - - /// - /// Updates this instance with data from the properties in the , - /// unless there is already a value in the existing property. - /// - void UpdateFrom(SentryFeedback source) => ((IUpdatable)this).UpdateFrom(source); - - void IUpdatable.UpdateFrom(SentryFeedback source) - { - if (string.IsNullOrEmpty(Message)) - { - Message = source.Message; - } - ContactEmail ??= source.ContactEmail; - Name ??= source.Name; - ReplayId ??= source.ReplayId; - Url ??= source.Url; - if (AssociatedEventId == SentryId.Empty) - { - AssociatedEventId = source.AssociatedEventId; - } - } - - void IUpdatable.UpdateFrom(object source) - { - if (source is SentryFeedback runtime) - { - ((IUpdatable)this).UpdateFrom(runtime); - } - } + => new(Message, ContactEmail, Name, ReplayId, Url, AssociatedEventId); } diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 505bf7bb1b..9b79bd5d4d 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, Scope? scope = null, SentryHint? hint = null) + => CurrentHub.CaptureFeedback(feedback, scope, hint); + /// /// Captures a user feedback. /// From c55b742888274eb7ed4260b059e36942d9f6903e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 24 Feb 2025 22:10:19 +1300 Subject: [PATCH 07/20] CollectionExtensionsTests --- .../CollectionExtensionsTests.verify.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/Sentry.Tests/Internals/CollectionExtensionsTests.verify.cs b/test/Sentry.Tests/Internals/CollectionExtensionsTests.verify.cs index 1a8f5528e6..35d2dcc202 100644 --- a/test/Sentry.Tests/Internals/CollectionExtensionsTests.verify.cs +++ b/test/Sentry.Tests/Internals/CollectionExtensionsTests.verify.cs @@ -13,4 +13,50 @@ public Task GetOrCreate_invalid_type() private class Value { } + + [Fact] + public void TryGetValue_KeyDoesNotExist_ReturnsNull() + { + // Arrange + var dictionary = new ConcurrentDictionary(); + + // Act + var result = dictionary.TryGetValue("nonexistentKey"); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void TryGetValue_KeyExistsAndTypeMatches_ReturnsValue() + { + // Arrange + var dictionary = new ConcurrentDictionary + { + ["existingKey"] = "testValue" + }; + + // Act + var result = dictionary.TryGetValue("existingKey"); + + // Assert + result.Should().Be("testValue"); + } + + [Fact] + public void TryGetValue_KeyExistsButTypeDoesNotMatch_Throws() + { + // Arrange + var dictionary = new ConcurrentDictionary + { + ["existingKey"] = 123 + }; + + // Act + Action act = () => dictionary.TryGetValue("existingKey"); + + // Assert + act.Should().Throw() + .WithMessage("Expected a type of System.String to exist for the key 'existingKey'. Instead found a System.Int32. The likely cause of this is that the value for 'existingKey' has been incorrectly set to an instance of a different type."); + } } From 03eddf82ee290d384ae786ec976a58d28c851dfa Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 24 Feb 2025 22:18:13 +1300 Subject: [PATCH 08/20] Verify tests --- .../ApiApprovalTests.Run.DotNet8_0.verified.txt | 11 +++++++++-- .../ApiApprovalTests.Run.DotNet9_0.verified.txt | 11 +++++++++-- .../ApiApprovalTests.Run.Net4_8.verified.txt | 11 +++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index baed422441..e842626dbb 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -237,6 +237,7 @@ namespace Sentry Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null); bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null); + void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null); void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint); @@ -461,6 +462,7 @@ namespace Sentry public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, 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) { } @@ -491,7 +493,7 @@ namespace Sentry public Sentry.Protocol.Browser Browser { get; } public int Count { get; } public Sentry.Protocol.Device Device { get; } - public Sentry.SentryFeedback Feedback { get; } + public Sentry.SentryFeedback? Feedback { get; set; } public Sentry.Protocol.Gpu Gpu { get; } public bool IsReadOnly { get; } public object this[string key] { get; set; } @@ -552,7 +554,7 @@ namespace Sentry } public sealed class SentryFeedback : Sentry.ISentryJsonSerializable { - public SentryFeedback() { } + public SentryFeedback(string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId associatedEventId = default) { } public Sentry.SentryId AssociatedEventId { get; set; } public string? ContactEmail { get; set; } public string Message { get; set; } @@ -816,6 +818,7 @@ 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 void CaptureFeedback(Sentry.SentryFeedback feedback, 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) { } public static void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } @@ -1321,6 +1324,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } 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 CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } @@ -1365,6 +1369,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope, Sentry.SentryHint? hint = null) { } 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 CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } @@ -1809,6 +1814,7 @@ namespace Sentry.Protocol.Envelopes public static System.Threading.Tasks.Task DeserializeAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default) { } public static Sentry.Protocol.Envelopes.Envelope FromCheckIn(Sentry.SentryCheckIn checkIn) { } public static Sentry.Protocol.Envelopes.Envelope FromEvent(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } + public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { } public static Sentry.Protocol.Envelopes.Envelope FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } @@ -1828,6 +1834,7 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.EnvelopeItem FromAttachment(Sentry.SentryAttachment attachment) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromCheckIn(Sentry.SentryCheckIn checkIn) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromEvent(Sentry.SentryEvent @event) { } + public static Sentry.Protocol.Envelopes.EnvelopeItem FromFeedback(Sentry.SentryEvent @event) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromTransaction(Sentry.SentryTransaction transaction) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index baed422441..e842626dbb 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -237,6 +237,7 @@ namespace Sentry Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null); bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null); + void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null); void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint); @@ -461,6 +462,7 @@ namespace Sentry public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, 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) { } @@ -491,7 +493,7 @@ namespace Sentry public Sentry.Protocol.Browser Browser { get; } public int Count { get; } public Sentry.Protocol.Device Device { get; } - public Sentry.SentryFeedback Feedback { get; } + public Sentry.SentryFeedback? Feedback { get; set; } public Sentry.Protocol.Gpu Gpu { get; } public bool IsReadOnly { get; } public object this[string key] { get; set; } @@ -552,7 +554,7 @@ namespace Sentry } public sealed class SentryFeedback : Sentry.ISentryJsonSerializable { - public SentryFeedback() { } + public SentryFeedback(string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId associatedEventId = default) { } public Sentry.SentryId AssociatedEventId { get; set; } public string? ContactEmail { get; set; } public string Message { get; set; } @@ -816,6 +818,7 @@ 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 void CaptureFeedback(Sentry.SentryFeedback feedback, 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) { } public static void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } @@ -1321,6 +1324,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } 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 CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } @@ -1365,6 +1369,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope, Sentry.SentryHint? hint = null) { } 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 CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } @@ -1809,6 +1814,7 @@ namespace Sentry.Protocol.Envelopes public static System.Threading.Tasks.Task DeserializeAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default) { } public static Sentry.Protocol.Envelopes.Envelope FromCheckIn(Sentry.SentryCheckIn checkIn) { } public static Sentry.Protocol.Envelopes.Envelope FromEvent(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } + public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { } public static Sentry.Protocol.Envelopes.Envelope FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } @@ -1828,6 +1834,7 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.EnvelopeItem FromAttachment(Sentry.SentryAttachment attachment) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromCheckIn(Sentry.SentryCheckIn checkIn) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromEvent(Sentry.SentryEvent @event) { } + public static Sentry.Protocol.Envelopes.EnvelopeItem FromFeedback(Sentry.SentryEvent @event) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromTransaction(Sentry.SentryTransaction transaction) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index bdbd610d64..3b75fc5455 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -225,6 +225,7 @@ namespace Sentry Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null); bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null); + void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null); void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint); @@ -449,6 +450,7 @@ namespace Sentry public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { } public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.SentryHint? hint = null) { } + public void CaptureFeedback(Sentry.SentryFeedback feedback, Sentry.Scope? scope = null, 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) { } @@ -479,7 +481,7 @@ namespace Sentry public Sentry.Protocol.Browser Browser { get; } public int Count { get; } public Sentry.Protocol.Device Device { get; } - public Sentry.SentryFeedback Feedback { get; } + public Sentry.SentryFeedback? Feedback { get; set; } public Sentry.Protocol.Gpu Gpu { get; } public bool IsReadOnly { get; } public object this[string key] { get; set; } @@ -540,7 +542,7 @@ namespace Sentry } public sealed class SentryFeedback : Sentry.ISentryJsonSerializable { - public SentryFeedback() { } + public SentryFeedback(string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, Sentry.SentryId associatedEventId = default) { } public Sentry.SentryId AssociatedEventId { get; set; } public string? ContactEmail { get; set; } public string Message { get; set; } @@ -797,6 +799,7 @@ 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 void CaptureFeedback(Sentry.SentryFeedback feedback, 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) { } public static void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } @@ -1302,6 +1305,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } 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 CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } @@ -1346,6 +1350,7 @@ namespace Sentry.Extensibility public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope, Sentry.SentryHint? hint = null) { } 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 CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } @@ -1791,6 +1796,7 @@ namespace Sentry.Protocol.Envelopes public static System.Threading.Tasks.Task DeserializeAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default) { } public static Sentry.Protocol.Envelopes.Envelope FromCheckIn(Sentry.SentryCheckIn checkIn) { } public static Sentry.Protocol.Envelopes.Envelope FromEvent(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } + public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { } public static Sentry.Protocol.Envelopes.Envelope FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } @@ -1810,6 +1816,7 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.EnvelopeItem FromAttachment(Sentry.SentryAttachment attachment) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromCheckIn(Sentry.SentryCheckIn checkIn) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromEvent(Sentry.SentryEvent @event) { } + public static Sentry.Protocol.Envelopes.EnvelopeItem FromFeedback(Sentry.SentryEvent @event) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromTransaction(Sentry.SentryTransaction transaction) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } From f92258e06c6fa21af84c08faa48da1b27c36aeb3 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 24 Feb 2025 23:18:46 +1300 Subject: [PATCH 09/20] Update HubTests.cs --- test/Sentry.Tests/HubTests.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index b00cc89e7f..eb90a0b2be 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1575,6 +1575,27 @@ public void CaptureEvent_HubEnabled(bool enabled) _fixture.Client.Received(enabled ? 1 : 0).CaptureEvent(Arg.Any(), Arg.Any(), Arg.Any()); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CaptureFeedback_HubEnabled(bool enabled) + { + // Arrange + var hub = _fixture.GetSut(); + if (!enabled) + { + hub.Dispose(); + } + + var feedback = new SentryFeedback("Test feedback"); + + // Act + hub.CaptureFeedback(feedback); + + // Assert + _fixture.Client.Received(enabled ? 1 : 0).CaptureFeedback(Arg.Any(), Arg.Any(), Arg.Any()); + } + [Theory] [InlineData(true)] [InlineData(false)] From b3d1c7d64331a12aecddd0e3d13e2fbd8b17aedb Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 25 Feb 2025 12:38:19 +1300 Subject: [PATCH 10/20] EnvelopeTests --- src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 2 +- .../Protocol/Envelopes/EnvelopeTests.cs | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index f34f5261be..877ea2bd7d 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -10,7 +10,7 @@ namespace Sentry.Protocol.Envelopes; /// public sealed class EnvelopeItem : ISerializable, IDisposable { - private const string TypeKey = "type"; + internal const string TypeKey = "type"; internal const string TypeValueEvent = "event"; internal const string TypeValueFeedback = "feedback"; diff --git a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs index 93f28b7379..8346111d42 100644 --- a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs +++ b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs @@ -819,6 +819,90 @@ public async Task Roundtrip_WithUserFeedback_Success() envelopeRoundtrip.Should().BeEquivalentTo(envelope); } + [Fact] + public async Task Roundtrip_WithFeedback_Success() + { + // Arrange + var feedback = new SentryFeedback( + "Everything is great!", + "foo@bar.com", + "Someone Nice", + "fake-replay-id", + "https://www.example.com", + SentryId.Create() + ); + var evt = new SentryEvent { Level = SentryLevel.Info, + Contexts = + { + Feedback = feedback + } + }; + + using var envelope = Envelope.FromFeedback(evt); + + using var stream = new MemoryStream(); + + // Act + await envelope.SerializeAsync(stream, _testOutputLogger); + stream.Seek(0, SeekOrigin.Begin); + + using var envelopeRoundtrip = await Envelope.DeserializeAsync(stream); + + // Assert + envelopeRoundtrip.Should().BeEquivalentTo(envelope); + } + + [Fact] + public void FromFeedback_NoFeedbackContext_Throws() + { + // Arrange + var evt = new SentryEvent { Level = SentryLevel.Info }; + + // Act + Action act = () => Envelope.FromFeedback(evt); + + // Assert + act.Should().Throw() + .WithMessage("Unable to create envelope - the event does not contain any feedback."); + } + + [Fact] + public void FromFeedback_MultipleAttachments_LogsWarning() + { + // Arrange + var feedback = new SentryFeedback( + "Everything is great!", + "foo@bar.com", + "Someone Nice", + "fake-replay-id", + "https://www.example.com", + SentryId.Create() + ); + var evt = new SentryEvent { Level = SentryLevel.Info, + Contexts = + { + Feedback = feedback + } + }; + var logger = Substitute.For(); + logger.IsEnabled(Arg.Any()).Returns(true); + + List attachments = [ + AttachmentHelper.FakeAttachment("file1.txt"), AttachmentHelper.FakeAttachment("file2.txt") + ]; + + // Act + using var envelope = Envelope.FromFeedback(evt, logger, attachments); + + // Assert + logger.Received(1).Log( + SentryLevel.Warning, + Arg.Is(m => m.Contains("Feedback can only contain one attachment")), + null, + Arg.Any()); + envelope.Items.Should().ContainSingle(item => item.Header[EnvelopeItem.TypeKey].ToString() == EnvelopeItem.TypeValueAttachment); + } + [Fact] public async Task Roundtrip_WithSession_Success() { From a70f78c0b17c2d6d0aee8e2ecc96add9864fd11b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 25 Feb 2025 13:22:34 +1300 Subject: [PATCH 11/20] SentryClient tests --- src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 2 +- src/Sentry/SentryClient.cs | 1 - .../Protocol/Envelopes/EnvelopeTests.cs | 2 +- test/Sentry.Tests/SentryClientTests.cs | 88 +++++++++++++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 877ea2bd7d..f34f5261be 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -10,7 +10,7 @@ namespace Sentry.Protocol.Envelopes; /// public sealed class EnvelopeItem : ISerializable, IDisposable { - internal const string TypeKey = "type"; + private const string TypeKey = "type"; internal const string TypeValueEvent = "event"; internal const string TypeValueFeedback = "feedback"; diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 9db5772edc..661bddd293 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -86,7 +86,6 @@ public void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, Sentry { if (string.IsNullOrEmpty(feedback.Message)) { - // Ignore the user feedback if EventId is empty _options.LogWarning("Feedback dropped due to empty message."); return; } diff --git a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs index 8346111d42..e1025fe66d 100644 --- a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs +++ b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs @@ -900,7 +900,7 @@ public void FromFeedback_MultipleAttachments_LogsWarning() Arg.Is(m => m.Contains("Feedback can only contain one attachment")), null, Arg.Any()); - envelope.Items.Should().ContainSingle(item => item.Header[EnvelopeItem.TypeKey].ToString() == EnvelopeItem.TypeValueAttachment); + envelope.Items.Should().ContainSingle(item => item.TryGetType() == EnvelopeItem.TypeValueAttachment); } [Fact] diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index dccaf26882..acfa680b3e 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -836,6 +836,7 @@ public void CaptureUserFeedback_EventIdEmpty_FeedbackIgnored() //Assert _ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); } + [Fact] public void Dispose_should_only_flush() { @@ -849,6 +850,93 @@ public void Dispose_should_only_flush() client.CaptureEvent(new SentryEvent { Message = "Test" }); } + [Fact] + public void CaptureFeedback_DisposedClient_DoesNotThrow() + { + // Arrange + var feedback = new SentryFeedback("Everything is great!"); + + var sut = _fixture.GetSut(); + sut.Dispose(); + + // Act / Assert + sut.CaptureFeedback(feedback); + } + + [Fact] + public void CaptureFeedback_NoMessage_FeedbackIgnored() + { + //Arrange + var sut = _fixture.GetSut(); + var feedback = new SentryFeedback(string.Empty); + + //Act + sut.CaptureFeedback(feedback); + + //Assert + _ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + } + + [Fact] + public void CaptureFeedback_ValidUserFeedback_FeedbackRegistered() + { + //Arrange + var sut = _fixture.GetSut(); + var feedback = new SentryFeedback("Everything is great!"); + + //Act + sut.CaptureFeedback(feedback); + + //Assert + _ = sut.Worker.Received(1).EnqueueEnvelope(Arg.Any()); + } + + [Fact] + public void CaptureFeedback_WithScope_ScopeCopiedToEvent() + { + //Arrange + const string expectedBreadcrumb = "test"; + var scope = new Scope(_fixture.SentryOptions); + scope.AddBreadcrumb(expectedBreadcrumb); + scope.Level = SentryLevel.Warning; + var feedback = new SentryFeedback("Everything is great!"); + var sut = _fixture.GetSut(); + + Envelope envelope = null; + sut.Worker.When(w => w.EnqueueEnvelope(Arg.Any())) + .Do(callback => envelope = callback.Arg()); + + //Act + sut.CaptureFeedback(feedback, scope); + + //Assert + _ = sut.Worker.Received(1).EnqueueEnvelope(Arg.Any()); + envelope.Should().NotBeNull(); + envelope.Items.Should().Contain(item => item.TryGetType() == EnvelopeItem.TypeValueFeedback); + var item = envelope.Items.First(x => x.TryGetType() == EnvelopeItem.TypeValueFeedback); + var @event = (SentryEvent)((JsonSerializable)item.Payload).Source; + @event.Level.Should().Be(scope.Level); + Assert.Equal(scope.Breadcrumbs, @event.Breadcrumbs); + } + + [Fact] + public void CaptureFeedback_WithHint_HasHintAttachment() + { + //Arrange + var sut = _fixture.GetSut(); + var feedback = new SentryFeedback("Everything is great!"); + var hint = new SentryHint(); + hint.Attachments.Add(AttachmentHelper.FakeAttachment("foo.txt")); + + //Act + sut.CaptureFeedback(feedback, null, hint); + + //Assert + _ = sut.Worker.Received(1).EnqueueEnvelope(Arg.Any()); + sut.Worker.Received(1).EnqueueEnvelope(Arg.Is(envelope => + envelope.Items.Count(item => item.TryGetType() == "attachment") == 1)); + } + [Fact] public void CaptureUserFeedback_DisposedClient_DoesNotThrow() { From db8503909f07da90bfab1acbcbb1682b3327d531 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 25 Feb 2025 00:38:42 +0000 Subject: [PATCH 12/20] Format code --- test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs index e1025fe66d..f945448389 100644 --- a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs +++ b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs @@ -831,7 +831,9 @@ public async Task Roundtrip_WithFeedback_Success() "https://www.example.com", SentryId.Create() ); - var evt = new SentryEvent { Level = SentryLevel.Info, + var evt = new SentryEvent + { + Level = SentryLevel.Info, Contexts = { Feedback = feedback @@ -878,7 +880,9 @@ public void FromFeedback_MultipleAttachments_LogsWarning() "https://www.example.com", SentryId.Create() ); - var evt = new SentryEvent { Level = SentryLevel.Info, + var evt = new SentryEvent + { + Level = SentryLevel.Info, Contexts = { Feedback = feedback From 327b1faec4c52ad88d3200836afaf8e13e124a4f Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 25 Feb 2025 13:42:13 +1300 Subject: [PATCH 13/20] Marked UserFeedback APIs obsolete --- CHANGELOG.md | 2 +- src/Sentry/Extensibility/DisabledHub.cs | 1 + src/Sentry/Extensibility/HubAdapter.cs | 1 + src/Sentry/ISentryClient.cs | 1 + src/Sentry/Internal/Hub.cs | 1 + src/Sentry/Protocol/Envelopes/Envelope.cs | 1 + src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 3 +++ src/Sentry/SentryClient.cs | 1 + src/Sentry/SentryClientExtensions.cs | 1 + src/Sentry/SentrySdk.cs | 2 ++ src/Sentry/UserFeedback.cs | 1 + .../ApiApprovalTests.Run.DotNet8_0.verified.txt | 10 ++++++++++ .../ApiApprovalTests.Run.DotNet9_0.verified.txt | 10 ++++++++++ .../ApiApprovalTests.Run.Net4_8.verified.txt | 10 ++++++++++ test/Sentry.Tests/HubTests.cs | 2 ++ test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs | 2 ++ test/Sentry.Tests/Protocol/UserFeedbackTests.cs | 2 ++ test/Sentry.Tests/SentryClientExtensionsTests.cs | 4 ++++ test/Sentry.Tests/SentryClientTests.cs | 8 ++++++++ 19 files changed, 62 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a51a01a5e..b259ee224c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Support sending User Feedback without errors/exceptions ([#3981](https://github.com/getsentry/sentry-dotnet/pull/3981)) +- User Feedback can now be captured without errors/exceptions. Note that these APIs replace the older UserFeedback APIs, which have now been marked as obsolete (and will be removed in a future major version bump) ([#3981](https://github.com/getsentry/sentry-dotnet/pull/3981)) - Serilog scope properties are now sent with Sentry events ([#3976](https://github.com/getsentry/sentry-dotnet/pull/3976)) - The sample seed used for sampling decisions is now propagated, for use in downstream custom trace samplers ([#3951](https://github.com/getsentry/sentry-dotnet/pull/3951)) diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index a4592f7401..6c18af6e45 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -212,6 +212,7 @@ public void Dispose() /// /// No-Op. /// + [Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(UserFeedback userFeedback) { } diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index fdbaf661eb..61d715d341 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -300,6 +300,7 @@ public Task FlushAsync(TimeSpan timeout) /// [DebuggerStepThrough] [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(UserFeedback sentryUserFeedback) => SentrySdk.CaptureUserFeedback(sentryUserFeedback); } diff --git a/src/Sentry/ISentryClient.cs b/src/Sentry/ISentryClient.cs index c40196852f..3841539cc2 100644 --- a/src/Sentry/ISentryClient.cs +++ b/src/Sentry/ISentryClient.cs @@ -40,6 +40,7 @@ public interface ISentryClient /// Captures a user feedback. /// /// The user feedback to send to Sentry. + [Obsolete("Use CaptureFeedback instead.")] void CaptureUserFeedback(UserFeedback userFeedback); /// diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 0b91fa1731..e857b0bbee 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -546,6 +546,7 @@ internal void CaptureHeapDump(string dumpFile) } #endif + [Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(UserFeedback userFeedback) { if (!IsEnabled) diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 6e9ef12ba9..b62dc82c98 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -333,6 +333,7 @@ public static Envelope FromFeedback( /// /// Creates an envelope that contains a single user feedback. /// + [Obsolete("Use FromFeedback instead.")] public static Envelope FromUserFeedback(UserFeedback sentryUserFeedback) { var eventId = sentryUserFeedback.EventId; diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index f34f5261be..7c721db581 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -233,6 +233,7 @@ public static EnvelopeItem FromFeedback(SentryEvent @event) /// /// Creates an from . /// + [Obsolete("Use FromFeedback instead.")] public static EnvelopeItem FromUserFeedback(UserFeedback sentryUserFeedback) { var header = new Dictionary(1, StringComparer.Ordinal) @@ -406,9 +407,11 @@ private static async Task DeserializePayloadAsync( // User report if (string.Equals(payloadType, TypeValueUserReport, StringComparison.OrdinalIgnoreCase)) { +#pragma warning disable CS0618 // Type or member is obsolete var bufferLength = (int)(payloadLength ?? stream.Length); var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); var userFeedback = Json.Parse(buffer, UserFeedback.FromJson); +#pragma warning restore CS0618 // Type or member is obsolete return new JsonSerializable(userFeedback); } diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 661bddd293..8358e131a1 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -117,6 +117,7 @@ public void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, Sentry } /// + [Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(UserFeedback userFeedback) { if (userFeedback.EventId.Equals(SentryId.Empty)) diff --git a/src/Sentry/SentryClientExtensions.cs b/src/Sentry/SentryClientExtensions.cs index 686a732229..7b89c91d4e 100644 --- a/src/Sentry/SentryClientExtensions.cs +++ b/src/Sentry/SentryClientExtensions.cs @@ -48,6 +48,7 @@ public static SentryId CaptureMessage(this ISentryClient client, string message, /// The user email. /// The user comments. /// The optional username. + [Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(this ISentryClient client, SentryId eventId, string email, string comments, string? name = null) { diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 9b79bd5d4d..7b97f675a8 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -493,6 +493,7 @@ public static void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, /// /// The user feedback to send to Sentry. [DebuggerStepThrough] + [Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(UserFeedback userFeedback) => CurrentHub.CaptureUserFeedback(userFeedback); @@ -504,6 +505,7 @@ public static void CaptureUserFeedback(UserFeedback userFeedback) /// The user comments. /// The optional username. [DebuggerStepThrough] + [Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(SentryId eventId, string email, string comments, string? name = null) => CurrentHub.CaptureUserFeedback(new UserFeedback(eventId, name, email, comments)); diff --git a/src/Sentry/UserFeedback.cs b/src/Sentry/UserFeedback.cs index 3de5773d69..0094a6ade4 100644 --- a/src/Sentry/UserFeedback.cs +++ b/src/Sentry/UserFeedback.cs @@ -6,6 +6,7 @@ namespace Sentry; /// /// Sentry User Feedback. /// +[Obsolete("Use SentryFeedback instead.")] public sealed class UserFeedback : ISentryJsonSerializable { /// diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index e842626dbb..f15da465e0 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -241,6 +241,7 @@ namespace Sentry void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint); + [System.Obsolete("Use CaptureFeedback instead.")] void CaptureUserFeedback(Sentry.UserFeedback userFeedback); System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout); } @@ -466,6 +467,7 @@ namespace Sentry public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } @@ -474,6 +476,7 @@ namespace Sentry { public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } + [System.Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(this Sentry.ISentryClient client, Sentry.SentryId eventId, string email, string comments, string? name = null) { } public static void Flush(this Sentry.ISentryClient client) { } public static void Flush(this Sentry.ISentryClient client, System.TimeSpan timeout) { } @@ -824,7 +827,9 @@ namespace Sentry public static void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } + [System.Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(Sentry.SentryId eventId, string email, string comments, string? name = null) { } [System.Obsolete("WARNING: This method deliberately causes a crash, and should not be used in a rea" + "l application.")] @@ -1236,6 +1241,7 @@ namespace Sentry public Sentry.ISpan StartChild(string operation) { } public void UnsetTag(string key) { } } + [System.Obsolete("Use SentryFeedback instead.")] public sealed class UserFeedback : Sentry.ISentryJsonSerializable { public UserFeedback(Sentry.SentryId eventId, string? name, string? email, string? comments) { } @@ -1328,6 +1334,7 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void ConfigureScope(System.Action configureScope) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1373,6 +1380,7 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } public void ConfigureScope(System.Action configureScope) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1817,6 +1825,7 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { } + [System.Obsolete("Use FromFeedback instead.")] public static Sentry.Protocol.Envelopes.Envelope FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable @@ -1837,6 +1846,7 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.EnvelopeItem FromFeedback(Sentry.SentryEvent @event) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromTransaction(Sentry.SentryTransaction transaction) { } + [System.Obsolete("Use FromFeedback instead.")] public static Sentry.Protocol.Envelopes.EnvelopeItem FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public interface ISerializable diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index e842626dbb..f15da465e0 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -241,6 +241,7 @@ namespace Sentry void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint); + [System.Obsolete("Use CaptureFeedback instead.")] void CaptureUserFeedback(Sentry.UserFeedback userFeedback); System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout); } @@ -466,6 +467,7 @@ namespace Sentry public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } @@ -474,6 +476,7 @@ namespace Sentry { public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } + [System.Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(this Sentry.ISentryClient client, Sentry.SentryId eventId, string email, string comments, string? name = null) { } public static void Flush(this Sentry.ISentryClient client) { } public static void Flush(this Sentry.ISentryClient client, System.TimeSpan timeout) { } @@ -824,7 +827,9 @@ namespace Sentry public static void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } + [System.Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(Sentry.SentryId eventId, string email, string comments, string? name = null) { } [System.Obsolete("WARNING: This method deliberately causes a crash, and should not be used in a rea" + "l application.")] @@ -1236,6 +1241,7 @@ namespace Sentry public Sentry.ISpan StartChild(string operation) { } public void UnsetTag(string key) { } } + [System.Obsolete("Use SentryFeedback instead.")] public sealed class UserFeedback : Sentry.ISentryJsonSerializable { public UserFeedback(Sentry.SentryId eventId, string? name, string? email, string? comments) { } @@ -1328,6 +1334,7 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void ConfigureScope(System.Action configureScope) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1373,6 +1380,7 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } public void ConfigureScope(System.Action configureScope) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1817,6 +1825,7 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { } + [System.Obsolete("Use FromFeedback instead.")] public static Sentry.Protocol.Envelopes.Envelope FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable @@ -1837,6 +1846,7 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.EnvelopeItem FromFeedback(Sentry.SentryEvent @event) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromTransaction(Sentry.SentryTransaction transaction) { } + [System.Obsolete("Use FromFeedback instead.")] public static Sentry.Protocol.Envelopes.EnvelopeItem FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public interface ISerializable diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 3b75fc5455..7c657fd299 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -229,6 +229,7 @@ namespace Sentry void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint); + [System.Obsolete("Use CaptureFeedback instead.")] void CaptureUserFeedback(Sentry.UserFeedback userFeedback); System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout); } @@ -454,6 +455,7 @@ namespace Sentry public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } @@ -462,6 +464,7 @@ namespace Sentry { public static Sentry.SentryId CaptureException(this Sentry.ISentryClient client, System.Exception ex) { } public static Sentry.SentryId CaptureMessage(this Sentry.ISentryClient client, string message, Sentry.SentryLevel level = 1) { } + [System.Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(this Sentry.ISentryClient client, Sentry.SentryId eventId, string email, string comments, string? name = null) { } public static void Flush(this Sentry.ISentryClient client) { } public static void Flush(this Sentry.ISentryClient client, System.TimeSpan timeout) { } @@ -805,7 +808,9 @@ namespace Sentry public static void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction) { } public static void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } + [System.Obsolete("Use CaptureFeedback instead.")] public static void CaptureUserFeedback(Sentry.SentryId eventId, string email, string comments, string? name = null) { } [System.Obsolete("WARNING: This method deliberately causes a crash, and should not be used in a rea" + "l application.")] @@ -1217,6 +1222,7 @@ namespace Sentry public Sentry.ISpan StartChild(string operation) { } public void UnsetTag(string key) { } } + [System.Obsolete("Use SentryFeedback instead.")] public sealed class UserFeedback : Sentry.ISentryJsonSerializable { public UserFeedback(Sentry.SentryId eventId, string? name, string? email, string? comments) { } @@ -1309,6 +1315,7 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void ConfigureScope(System.Action configureScope) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1354,6 +1361,7 @@ namespace Sentry.Extensibility public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } public void CaptureTransaction(Sentry.SentryTransaction transaction, Sentry.Scope? scope, Sentry.SentryHint? hint) { } + [System.Obsolete("Use CaptureFeedback instead.")] public void CaptureUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } public void ConfigureScope(System.Action configureScope) { } public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } @@ -1799,6 +1807,7 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { } public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { } + [System.Obsolete("Use FromFeedback instead.")] public static Sentry.Protocol.Envelopes.Envelope FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable @@ -1819,6 +1828,7 @@ namespace Sentry.Protocol.Envelopes public static Sentry.Protocol.Envelopes.EnvelopeItem FromFeedback(Sentry.SentryEvent @event) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromSession(Sentry.SessionUpdate sessionUpdate) { } public static Sentry.Protocol.Envelopes.EnvelopeItem FromTransaction(Sentry.SentryTransaction transaction) { } + [System.Obsolete("Use FromFeedback instead.")] public static Sentry.Protocol.Envelopes.EnvelopeItem FromUserFeedback(Sentry.UserFeedback sentryUserFeedback) { } } public interface ISerializable diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index eb90a0b2be..d22326b119 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1601,6 +1601,7 @@ public void CaptureFeedback_HubEnabled(bool enabled) [InlineData(false)] public void CaptureUserFeedback_HubEnabled(bool enabled) { +#pragma warning disable CS0618 // Type or member is obsolete // Arrange var hub = _fixture.GetSut(); if (!enabled) @@ -1615,6 +1616,7 @@ public void CaptureUserFeedback_HubEnabled(bool enabled) // Assert _fixture.Client.Received(enabled ? 1 : 0).CaptureUserFeedback(Arg.Any()); +#pragma warning restore CS0618 // Type or member is obsolete } [Theory] diff --git a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs index e1025fe66d..f2b06e3a4b 100644 --- a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs +++ b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs @@ -798,6 +798,7 @@ public async Task Roundtrip_WithEvent_WithSession_Success() [Fact] public async Task Roundtrip_WithUserFeedback_Success() { +#pragma warning disable CS0618 // Type or member is obsolete // Arrange var feedback = new UserFeedback( SentryId.Create(), @@ -817,6 +818,7 @@ public async Task Roundtrip_WithUserFeedback_Success() // Assert envelopeRoundtrip.Should().BeEquivalentTo(envelope); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] diff --git a/test/Sentry.Tests/Protocol/UserFeedbackTests.cs b/test/Sentry.Tests/Protocol/UserFeedbackTests.cs index 2e12f56b0a..5358773067 100644 --- a/test/Sentry.Tests/Protocol/UserFeedbackTests.cs +++ b/test/Sentry.Tests/Protocol/UserFeedbackTests.cs @@ -12,6 +12,7 @@ public UserFeedbackTests(ITestOutputHelper output) [Fact] public void Serialization_SentryUserFeedbacks_Success() { +#pragma warning disable CS0618 // Type or member is obsolete // Arrange var eventId = new SentryId(Guid.Parse("acbe351c61494e7b807fd7e82a435ffc")); var userFeedback = new UserFeedback(eventId, "myName", "myEmail@service.com", "my comment"); @@ -30,5 +31,6 @@ public void Serialization_SentryUserFeedbacks_Success() } """, actual); +#pragma warning restore CS0618 // Type or member is obsolete } } diff --git a/test/Sentry.Tests/SentryClientExtensionsTests.cs b/test/Sentry.Tests/SentryClientExtensionsTests.cs index 9210c7c62f..cb73d048ba 100644 --- a/test/Sentry.Tests/SentryClientExtensionsTests.cs +++ b/test/Sentry.Tests/SentryClientExtensionsTests.cs @@ -81,18 +81,22 @@ public void CaptureMessage_NullMessage_DoesNotCapturesEventWithMessage() [Fact] public void CaptureUserFeedback_EnabledClient_CapturesUserFeedback() { +#pragma warning disable CS0618 // Type or member is obsolete _ = _sut.IsEnabled.Returns(true); _sut.CaptureUserFeedback(Guid.Parse("1ec19311a7c048818de80b18dcc43eaa"), "email@email.com", "comments"); _sut.Received(1).CaptureUserFeedback(Arg.Any()); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] public void CaptureUserFeedback_DisabledClient_DoesNotCaptureUserFeedback() { +#pragma warning disable CS0618 // Type or member is obsolete _ = _sut.IsEnabled.Returns(false); _sut.CaptureUserFeedback(Guid.Parse("1ec19311a7c048818de80b18dcc43eea"), "email@email.com", "comments"); _sut.DidNotReceive().CaptureUserFeedback(Arg.Any()); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index acfa680b3e..6b85c74de9 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -799,6 +799,7 @@ public void CaptureEvent_DisposedClient_DoesNotThrow() [Fact] public void CaptureUserFeedback_EventIdEmpty_IgnoreUserFeedback() { +#pragma warning disable CS0618 // Type or member is obsolete //Arrange var sut = _fixture.GetSut(); @@ -808,11 +809,13 @@ public void CaptureUserFeedback_EventIdEmpty_IgnoreUserFeedback() //Assert _ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] public void CaptureUserFeedback_ValidUserFeedback_FeedbackRegistered() { +#pragma warning disable CS0618 // Type or member is obsolete //Arrange var sut = _fixture.GetSut(); @@ -822,11 +825,13 @@ public void CaptureUserFeedback_ValidUserFeedback_FeedbackRegistered() //Assert _ = sut.Worker.Received(1).EnqueueEnvelope(Arg.Any()); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] public void CaptureUserFeedback_EventIdEmpty_FeedbackIgnored() { +#pragma warning disable CS0618 // Type or member is obsolete //Arrange var sut = _fixture.GetSut(); @@ -835,6 +840,7 @@ public void CaptureUserFeedback_EventIdEmpty_FeedbackIgnored() //Assert _ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] @@ -940,9 +946,11 @@ public void CaptureFeedback_WithHint_HasHintAttachment() [Fact] public void CaptureUserFeedback_DisposedClient_DoesNotThrow() { +#pragma warning disable CS0618 // Type or member is obsolete var sut = _fixture.GetSut(); sut.Dispose(); sut.CaptureUserFeedback(new UserFeedback(SentryId.Empty, "name", "email", "comment")); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] From 1ecedf7cd2d3317475c83ab4a17bf9781d7f7fcb Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 25 Feb 2025 15:45:17 +1300 Subject: [PATCH 14/20] Added to Maui Sample --- samples/Sentry.Samples.Maui/MainPage.xaml | 7 +++ samples/Sentry.Samples.Maui/MainPage.xaml.cs | 5 ++ .../Sentry.Samples.Maui/SubmitFeedback.xaml | 23 ++++++++ .../SubmitFeedback.xaml.cs | 52 +++++++++++++++++++ src/Sentry/SentryFeedback.cs | 8 +-- 5 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 samples/Sentry.Samples.Maui/SubmitFeedback.xaml create mode 100644 samples/Sentry.Samples.Maui/SubmitFeedback.xaml.cs diff --git a/samples/Sentry.Samples.Maui/MainPage.xaml b/samples/Sentry.Samples.Maui/MainPage.xaml index d1519f610c..672b9d2347 100644 --- a/samples/Sentry.Samples.Maui/MainPage.xaml +++ b/samples/Sentry.Samples.Maui/MainPage.xaml @@ -70,6 +70,13 @@ Clicked="OnNativeCrashClicked" HorizontalOptions="Center" /> + - public SentryId AssociatedEventId { get; set; } + public SentryId? AssociatedEventId { get; set; } /// /// Creates an instance of . /// - public SentryFeedback(string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, SentryId associatedEventId = default) + public SentryFeedback(string message, string? contactEmail = null, string? name = null, string? replayId = null, string? url = null, SentryId? associatedEventId = null) { Message = message; ContactEmail = contactEmail; @@ -74,7 +74,7 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteStringIfNotWhiteSpace("name", Name); writer.WriteStringIfNotWhiteSpace("replay_id", ReplayId); writer.WriteStringIfNotWhiteSpace("url", Url); - writer.WriteSerializable("associated_event_id", AssociatedEventId, logger); + writer.WriteSerializableIfNotNull("associated_event_id", AssociatedEventId, logger); writer.WriteEndObject(); } @@ -89,7 +89,7 @@ public static SentryFeedback FromJson(JsonElement json) var name = json.GetPropertyOrNull("name")?.GetString(); var replayId = json.GetPropertyOrNull("replay_id")?.GetString(); var url = json.GetPropertyOrNull("url")?.GetString(); - var eventId = json.GetPropertyOrNull("associated_event_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; + var eventId = json.GetPropertyOrNull("associated_event_id")?.Pipe(SentryId.FromJson); return new SentryFeedback(message, contactEmail, name, replayId, url, eventId); } From bd50e63e6301802281b97e940fe7561eba6d5661 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 25 Feb 2025 15:55:06 +1300 Subject: [PATCH 15/20] Allow attachments with feedback in Maui Sample --- .../Sentry.Samples.Maui/SubmitFeedback.xaml | 2 + .../SubmitFeedback.xaml.cs | 38 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/samples/Sentry.Samples.Maui/SubmitFeedback.xaml b/samples/Sentry.Samples.Maui/SubmitFeedback.xaml index 9acc680cba..f51c08ace5 100644 --- a/samples/Sentry.Samples.Maui/SubmitFeedback.xaml +++ b/samples/Sentry.Samples.Maui/SubmitFeedback.xaml @@ -14,6 +14,8 @@