Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0ab7219
Create SentryFeedback.cs
jamescrosswell Feb 19, 2025
965de12
Update CHANGELOG.md
jamescrosswell Feb 19, 2025
adb7c04
Added feedback to SentryContexts
jamescrosswell Feb 20, 2025
486f65c
Verify files
jamescrosswell Feb 20, 2025
dd5e7eb
Merge branch 'main' into feedback
jamescrosswell Feb 20, 2025
48c364c
Format code
getsentry-bot Feb 20, 2025
8abc13e
Implemented envelopes and client/hub API
jamescrosswell Feb 21, 2025
56f45e6
Merge branches 'feedback' and 'feedback' of github.com:getsentry/sent…
jamescrosswell Feb 21, 2025
c55b742
CollectionExtensionsTests
jamescrosswell Feb 24, 2025
03eddf8
Verify tests
jamescrosswell Feb 24, 2025
f92258e
Update HubTests.cs
jamescrosswell Feb 24, 2025
b3d1c7d
EnvelopeTests
jamescrosswell Feb 24, 2025
a70f78c
SentryClient tests
jamescrosswell Feb 25, 2025
db85039
Format code
getsentry-bot Feb 25, 2025
327b1fa
Marked UserFeedback APIs obsolete
jamescrosswell Feb 25, 2025
81ae494
Merge branch 'feedback' of github.com:getsentry/sentry-dotnet into fe…
jamescrosswell Feb 25, 2025
1ecedf7
Added to Maui Sample
jamescrosswell Feb 25, 2025
bd50e63
Allow attachments with feedback in Maui Sample
jamescrosswell Feb 25, 2025
750bf37
Verify tests
jamescrosswell Feb 25, 2025
e21588f
Format code
getsentry-bot Feb 25, 2025
79dd55b
Updated Console.Customized sample
jamescrosswell Feb 25, 2025
498dd04
Merge branch 'feedback' of github.com:getsentry/sentry-dotnet into fe…
jamescrosswell Feb 25, 2025
39ca97f
Added Feedback to ASP.NET Core MVC sample
jamescrosswell Feb 26, 2025
68a9976
Merge branch 'main' into feedback
jamescrosswell Feb 26, 2025
d618ae9
Review feedback
jamescrosswell Feb 28, 2025
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 @@ -5,6 +5,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))
- Users can now register their own MAUI controls for breadcrumb creation ([#3997](https://github.com/getsentry/sentry-dotnet/pull/3997))
- 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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Data;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Samples.AspNetCore.Mvc.Models;
using Sentry.Ben.BlockingDetector;
using Sentry.Samples.AspNetCore.Mvc.Models;

Expand Down Expand Up @@ -176,4 +177,33 @@ public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

[HttpGet("[controller]/feedback")]
public IActionResult Feedback()
{
return View();
}

[HttpPost]
public async Task<IActionResult> SubmitFeedback(FeedbackModel feedback)
{
if (!ModelState.IsValid)
{
return View("Feedback", feedback);
}

var sentryFeedback = new SentryFeedback(feedback.Message!, feedback.ContactEmail, feedback.Name);
var hint = new SentryHint();

if (feedback.Screenshot is { Length: > 0 })
{
await using var memoryStream = new MemoryStream();
await feedback.Screenshot.CopyToAsync(memoryStream);
hint.AddAttachment(memoryStream.ToArray(), feedback.Screenshot.FileName, AttachmentType.Default, "image/png");
}

SentrySdk.CaptureFeedback(sentryFeedback, null, hint);
ViewBag.Message = "Feedback submitted successfully!";
return View("Index");
}
}
16 changes: 16 additions & 0 deletions samples/Sentry.Samples.AspNetCore.Mvc/Models/FeedbackModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;

namespace Samples.AspNetCore.Mvc.Models;

public class FeedbackModel
{
[Required]
public string? Message { get; set; }

[EmailAddress]
public string? ContactEmail { get; set; }

public string? Name { get; set; }

public IFormFile? Screenshot { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@
<ProjectReference Include="..\..\src\Sentry.AspNetCore\Sentry.AspNetCore.csproj" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="Views\Home\Feedback.cshtml" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@model Samples.AspNetCore.Mvc.Models.FeedbackModel

@{
ViewData["Title"] = "Submit Feedback";
}

<h2>Submit Feedback</h2>

@if (ViewBag.Message != null)
{
<div class="alert alert-success">@ViewBag.Message</div>
}

<form asp-action="SubmitFeedback" method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="Message"></label>
<textarea asp-for="Message" class="form-control"></textarea>
<span asp-validation-for="Message" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ContactEmail"></label>
<input asp-for="ContactEmail" class="form-control" />
<span asp-validation-for="ContactEmail" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Name"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Screenshot">Attach Screenshot</label>
<input asp-for="Screenshot" type="file" class="form-control" />
<span asp-validation-for="Screenshot" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Action">Action Throws</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Feedback">Submit Feedback</a>
</li>
</ul>
</div>
</div>
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" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, we can use this to create a built-in Widget like @armcknight built for iOS

Or use bindings to use the native one. But with a MAUI implementation means we have it also on Windows and macOS


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