Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support sending User Feedback without errors/exceptions #3981

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- 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))

Expand Down
12 changes: 9 additions & 3 deletions samples/Sentry.Samples.Console.Customized/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,18 @@ await SentrySdk.ConfigureScopeAsync(async scope =>

var eventId = SentrySdk.CaptureMessage("Some warning!", SentryLevel.Warning);

// Send an user feedback linked to the warning.
// Send feedback linked to the warning.
var timestamp = DateTime.Now.Ticks;
var user = $"user{timestamp}";
var email = $"user{timestamp}@user{timestamp}.com";

SentrySdk.CaptureUserFeedback(new UserFeedback(eventId, user, email, "this is a sample user feedback"));
SentrySdk.CaptureFeedback(new SentryFeedback(
message: "this is a sample user feedback",
contactEmail: email,
name: user,
replayId: null,
url: null,
associatedEventId: eventId
));

var error = new Exception("Attempting to send this multiple times");

Expand Down
7 changes: 7 additions & 0 deletions samples/Sentry.Samples.Maui/MainPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@
Clicked="OnNativeCrashClicked"
HorizontalOptions="Center" />

<Button
x:Name="FeedbackBtn"
Text="Submit Feedback"
SemanticProperties.Hint="Provides a form that can be used to capture open feedback from the user."
Clicked="OnFeedbackClicked"
HorizontalOptions="Center" />

</VerticalStackLayout>
</ScrollView>

Expand Down
5 changes: 5 additions & 0 deletions samples/Sentry.Samples.Maui/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,9 @@ protected override Task<HttpResponseMessage> SendAsync(
CancellationToken cancellationToken)
=> throw new Exception();
}

private async void OnFeedbackClicked(object sender, EventArgs e)
{
await Navigation.PushModalAsync(new SubmitFeedback());
}
}
25 changes: 25 additions & 0 deletions samples/Sentry.Samples.Maui/SubmitFeedback.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Sentry.Samples.Maui.SubmitFeedback">
<ContentPage.Content>
<StackLayout Padding="20">
<Label Text="Message" />
<Editor x:Name="MessageEditor" Placeholder="Enter your feedback message" />

<Label Text="Contact Email" Margin="0,20,0,0" />
<Entry x:Name="ContactEmailEntry" Placeholder="Enter your contact email" />

<Label Text="Name" Margin="0,20,0,0" />
<Entry x:Name="NameEntry" Placeholder="Enter your name" />

<Button Text="Attach Screenshot" Clicked="OnAttachScreenshotClicked" Margin="0,20,0,0" />

<StackLayout Orientation="Horizontal" HorizontalOptions="EndAndExpand" Margin="0,20,0,0" Spacing="10">
<Button Text="Cancel" Clicked="OnCancelClicked" BackgroundColor="Red" TextColor="White" />
<Button Text="Submit" Clicked="OnSubmitClicked" BackgroundColor="Green" TextColor="White" />
</StackLayout>
</StackLayout>
</ContentPage.Content>
</ContentPage>
83 changes: 83 additions & 0 deletions samples/Sentry.Samples.Maui/SubmitFeedback.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Sentry.Samples.Maui;

public partial class SubmitFeedback : ContentPage
{
[GeneratedRegex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$")]
private static partial Regex EmailPattern();

private string _screenshotPath;

private bool IsValidEmail(string email) => string.IsNullOrWhiteSpace(email) || EmailPattern().IsMatch(email);

public SubmitFeedback()
{
InitializeComponent();
}

private async void OnSubmitClicked(object sender, EventArgs e)
{
var message = MessageEditor.Text;
var contactEmail = ContactEmailEntry.Text;
var name = NameEntry.Text;

if (string.IsNullOrWhiteSpace(message))
{
await DisplayAlert("Validation Error", "Message is required.", "OK");
return;
}

if (!IsValidEmail(contactEmail))
{
await DisplayAlert("Validation Error", "Please enter a valid email address.", "OK");
return;
}

SentryHint hint = null;
if (!string.IsNullOrEmpty(_screenshotPath))
{
hint = new SentryHint();
hint.AddAttachment(_screenshotPath, AttachmentType.Default, "image/png");
}

// Handle the feedback submission logic here
var feedback = new SentryFeedback(message, contactEmail, name);
SentrySdk.CaptureFeedback(feedback, hint: hint);

await DisplayAlert("Feedback Submitted", "Thank you for your feedback!", "OK");
await Navigation.PopModalAsync();
}

private async void OnCancelClicked(object sender, EventArgs e)
{
await Navigation.PopModalAsync();
}

private async void OnAttachScreenshotClicked(object sender, EventArgs e)
{
try
{
var result = await FilePicker.PickAsync(new PickOptions
{
FileTypes = FilePickerFileType.Images,
PickerTitle = "Select a screenshot"
});

if (result != null)
{
_screenshotPath = result.FullPath;
await DisplayAlert("Screenshot Attached", "Screenshot has been attached successfully.", "OK");
}
}
catch (Exception ex)
{
await DisplayAlert("Error", $"An error occurred while selecting the screenshot: {ex.Message}", "OK");
}
}
}
8 changes: 8 additions & 0 deletions src/Sentry/Extensibility/DisabledHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ public bool CaptureEnvelope(Envelope envelope)
/// </summary>
public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null, SentryHint? hint = null) => SentryId.Empty;

/// <summary>
/// No-Op.
/// </summary>
public void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, SentryHint? hint = null)
{
}

/// <summary>
/// No-Op.
/// </summary>
Expand Down Expand Up @@ -205,6 +212,7 @@ public void Dispose()
/// <summary>
/// No-Op.
/// </summary>
[Obsolete("Use CaptureFeedback instead.")]
public void CaptureUserFeedback(UserFeedback userFeedback)
{
}
Expand Down
9 changes: 9 additions & 0 deletions src/Sentry/Extensibility/HubAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
[DebuggerStepThrough]
[EditorBrowsable(EditorBrowsableState.Never)]
public void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, SentryHint? hint = null)
=> SentrySdk.CaptureFeedback(feedback, scope, hint);

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
Expand Down Expand Up @@ -292,6 +300,7 @@ public Task FlushAsync(TimeSpan timeout)
/// </summary>
[DebuggerStepThrough]
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use CaptureFeedback instead.")]
public void CaptureUserFeedback(UserFeedback sentryUserFeedback)
=> SentrySdk.CaptureUserFeedback(sentryUserFeedback);
}
9 changes: 9 additions & 0 deletions src/Sentry/ISentryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,19 @@ public interface ISentryClient
/// <returns>The Id of the event.</returns>
SentryId CaptureEvent(SentryEvent evt, Scope? scope = null, SentryHint? hint = null);

/// <summary>
/// Captures feedback from the user.
/// </summary>
/// <param name="feedback">The feedback to send to Sentry.</param>
/// <param name="scope">An optional scope to be applied to the event.</param>
/// <param name="hint">An optional hint providing high level context for the source of the event</param>
void CaptureFeedback(SentryFeedback feedback, Scope? scope = null, SentryHint? hint = null);

/// <summary>
/// Captures a user feedback.
/// </summary>
/// <param name="userFeedback">The user feedback to send to Sentry.</param>
[Obsolete("Use CaptureFeedback instead.")]
void CaptureUserFeedback(UserFeedback userFeedback);

/// <summary>
Expand Down
18 changes: 18 additions & 0 deletions src/Sentry/Internal/Extensions/CollectionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ public static TValue GetOrCreate<TValue>(
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<TValue>(
this ConcurrentDictionary<string, object> 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<TKey, TValue>(this IDictionary<TKey, TValue> from, IDictionary<TKey, TValue> to)
where TKey : notnull
{
Expand Down
13 changes: 13 additions & 0 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -534,6 +546,7 @@ internal void CaptureHeapDump(string dumpFile)
}
#endif

[Obsolete("Use CaptureFeedback instead.")]
public void CaptureUserFeedback(UserFeedback userFeedback)
{
if (!IsEnabled)
Expand Down
96 changes: 72 additions & 24 deletions src/Sentry/Protocol/Envelopes/Envelope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<EnvelopeItem> 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);
}
}

/// <summary>
/// Creates an envelope that contains a single feedback event.
/// </summary>
public static Envelope FromFeedback(
SentryEvent @event,
IDiagnosticLogger? logger = null,
IReadOnlyCollection<SentryAttachment>? 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>
{
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)
Expand All @@ -286,6 +333,7 @@ public static Envelope FromEvent(
/// <summary>
/// Creates an envelope that contains a single user feedback.
/// </summary>
[Obsolete("Use FromFeedback instead.")]
public static Envelope FromUserFeedback(UserFeedback sentryUserFeedback)
{
var eventId = sentryUserFeedback.EventId;
Expand Down
Loading
Loading