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

feat: Manually capturing user feedback #559

Merged
merged 5 commits into from
Oct 29, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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 @@ -2,6 +2,7 @@

## unreleased

* Add support for user feedback. (#559) @lucas-zimerman
* Fix internal url references for the new Sentry documentation. @lucas-zimerman

## 3.0.0-alpha.2
Expand Down
9 changes: 8 additions & 1 deletion samples/Sentry.Samples.Console.Customized/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,14 @@ await SentrySdk.ConfigureScopeAsync(async scope =>
SentrySdk.CaptureMessage("Fatal message!");
});

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

// Send an user 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, email, "this is a sample user feedback", user));

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

Expand Down
16 changes: 16 additions & 0 deletions samples/Sentry.Samples.Console.ManualUserFeedback/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using Sentry;

namespace Sentry.Samples.Console.ManualUserFeedback
{
public static class Program
{
static void Main()
{
using (SentrySdk.Init("https://[email protected]/5428537"))
{
SentrySdk.FlushAsync(new TimeSpan(0, 0, 30)).Wait();
lucas-zimerman marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
lucas-zimerman marked this conversation as resolved.
Show resolved Hide resolved
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Sentry\Sentry.csproj" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions src/Sentry/Extensibility/DisabledHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ public void Dispose()
{
}

/// <summary>
/// No-Op.
/// </summary>
public void CaptureUserFeedback(UserFeedback userFeedback)
{
}

/// <summary>
/// No-Op.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/Sentry/Extensibility/HubAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,13 @@ public SentryId CaptureEvent(SentryEvent evt, Scope? scope)
[EditorBrowsable(EditorBrowsableState.Never)]
public Task FlushAsync(TimeSpan timeout)
=> SentrySdk.FlushAsync(timeout);

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>
/// </summary>
[DebuggerStepThrough]
[EditorBrowsable(EditorBrowsableState.Never)]
public void CaptureUserFeedback(UserFeedback sentryUserFeedback)
=> SentrySdk.CaptureUserFeedback(sentryUserFeedback);
}
}
6 changes: 6 additions & 0 deletions src/Sentry/ISentryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public interface ISentryClient
/// <returns>The Id of the event.</returns>
SentryId CaptureEvent(SentryEvent evt, Scope? scope = null);

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

/// <summary>
/// Flushes events queued up.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null)
}
}

public void CaptureUserFeedback(UserFeedback userFeedback)
{
try
{
_ownedClient.CaptureUserFeedback(userFeedback);
}
catch (Exception e)
{
_options.DiagnosticLogger?.LogError("Failure to capture user feedback: {0}", e, userFeedback.EventId);
}
}

public async Task FlushAsync(TimeSpan timeout)
{
try
Expand Down
18 changes: 18 additions & 0 deletions src/Sentry/Protocol/Envelope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,23 @@ public static Envelope FromEvent(SentryEvent @event)

return new Envelope(header, items);
}

/// <summary>
/// Creates an envelope that contains a single user feedback.
/// </summary>
internal static Envelope FromUserFeedback(UserFeedback sentryUserFeedback)
{
var header = new Dictionary<string, object>
{
[EventIdKey] = sentryUserFeedback.EventId.ToString()
};

var items = new[]
{
EnvelopeItem.FromUserFeedback(sentryUserFeedback)
};

return new Envelope(header, items);
}
}
}
15 changes: 14 additions & 1 deletion src/Sentry/Protocol/EnvelopeItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private async Task<MemoryStream> BufferPayloadAsync(CancellationToken cancellati
{
var buffer = new MemoryStream();
await Payload.SerializeAsync(buffer, cancellationToken).ConfigureAwait(false);
buffer.Seek(0, SeekOrigin.Begin);
_ = buffer.Seek(0, SeekOrigin.Begin);

return buffer;
}
Expand Down Expand Up @@ -142,5 +142,18 @@ public static EnvelopeItem FromEvent(SentryEvent @event)

return new EnvelopeItem(header, @event);
}

/// <summary>
/// Creates an envelope item from an user feedback.
/// </summary>
public static EnvelopeItem FromUserFeedback(UserFeedback sentryUserFeedback)
{
var header = new Dictionary<string, object>
{
[TypeKey] = "user_report"
};

return new EnvelopeItem(header, sentryUserFeedback);
}
}
}
59 changes: 59 additions & 0 deletions src/Sentry/Protocol/UserFeedback.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.IO;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Sentry.Internal;
using Sentry.Protocol;
using ISerializable = Sentry.Protocol.ISerializable;

namespace Sentry
{
/// <summary>
/// Sentry User Feedback.
/// </summary>
[DataContract]
public class UserFeedback : ISerializable
{

lucas-zimerman marked this conversation as resolved.
Show resolved Hide resolved
[DataMember(Name = "event_id", EmitDefaultValue = false)]
private string _serializableEventId => EventId.ToString();

/// <summary>
/// The eventId of the event to which the user feedback is associated.
/// </summary>
public SentryId EventId { get; }

/// <summary>
/// The name of the user.
/// </summary>
[DataMember(Name = "name", EmitDefaultValue = false)]
public string? Name { get; }

/// <summary>
/// The name of the user.
/// </summary>
[DataMember(Name = "email", EmitDefaultValue = false)]
public string Email { get; }

/// <summary>
/// Comments of the user about what happened.
/// </summary>
[DataMember(Name = "comments", EmitDefaultValue = false)]
public string Comments { get; }

/// <summary>
/// Initializes an instance of <see cref="UserFeedback"/>.
/// </summary>
public UserFeedback(SentryId eventId, string email, string comments, string? name = null)
{
EventId = eventId;
Name = name;
Email = email;
Comments = comments;
}

/// <inheritdoc />
public async Task SerializeAsync(Stream stream, CancellationToken cancellationToken = default)
=> await Json.SerializeToStreamAsync(this, stream, cancellationToken).ConfigureAwait(false);
}
}
46 changes: 42 additions & 4 deletions src/Sentry/SentryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,36 @@ public SentryId CaptureEvent(SentryEvent? @event, Scope? scope = null)
}
}

/// <summary>
/// Captures a user feedback.
/// </summary>
/// <param name="userFeedback">The user feedback to send to Sentry.</param>
public void CaptureUserFeedback(UserFeedback userFeedback)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(SentryClient));
}

if (userFeedback.EventId.Equals(SentryId.Empty))
{
//Ignore the userfeedback if EventId is empty
_options.DiagnosticLogger?.LogWarning(
"User feedback dropped due to empty id.", "UserFeedback");
lucas-zimerman marked this conversation as resolved.
Show resolved Hide resolved
return;
}
else if (string.IsNullOrWhiteSpace(userFeedback.Email) ||
string.IsNullOrWhiteSpace(userFeedback.Comments))
{
//Ignore the userfeedback if a required field is null or empty.
_options.DiagnosticLogger?.LogWarning(
"User feedback discarded due to one or more required fields missing.", "UserFeedback");
return;
}

_ = CaptureEnvelope(Envelope.FromUserFeedback(userFeedback));
}

/// <summary>
/// Flushes events asynchronously.
/// </summary>
Expand Down Expand Up @@ -165,18 +195,26 @@ private SentryId DoSendEvent(SentryEvent @event, Scope? scope)
return SentryId.Empty;
}

var envelope = Envelope.FromEvent(processedEvent);
return CaptureEnvelope(Envelope.FromEvent(processedEvent)) ?
processedEvent.EventId : SentryId.Empty;
}

/// <summary>
/// Capture an envelope and queue it.
/// </summary>
/// <param name="envelope">The envelope.</param>
/// <returns>true if the enveloped was queued, false otherwise.</returns>
private bool CaptureEnvelope(Envelope envelope)
{
if (Worker.EnqueueEnvelope(envelope))
{
_options.DiagnosticLogger?.LogDebug("Envelope queued up.");
return processedEvent.EventId;
return true;
}

_options.DiagnosticLogger?.LogWarning("The attempt to queue the event failed. Items in queue: {0}",
Worker.QueuedItems);

return SentryId.Empty;
return false;
}

private SentryEvent? BeforeSend(SentryEvent? @event)
Expand Down
16 changes: 16 additions & 0 deletions src/Sentry/SentryClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,21 @@ public static SentryId CaptureMessage(
}
);
}

/// <summary>
/// Captures a user feedback.
/// </summary>
/// <param name="client"></param>
/// <param name="eventId">The event Id.</param>
/// <param name="email">The user email.</param>
/// <param name="comments">The user comments.</param>
/// <param name="name">The optional username.</param>
public static void CaptureUserFeedback(this ISentryClient client, SentryId eventId, string email, string comments, string? name = null)
{
if (client.IsEnabled)
{
client.CaptureUserFeedback(new UserFeedback(eventId, email, comments, name));
}
}
}
}
19 changes: 19 additions & 0 deletions src/Sentry/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,5 +283,24 @@ public static SentryId CaptureException(Exception exception)
[DebuggerStepThrough]
public static SentryId CaptureMessage(string message, SentryLevel level = SentryLevel.Info)
=> _hub.CaptureMessage(message, level);

/// <summary>
/// Captures a user feedback.
/// </summary>
/// <param name="userFeedback">The user feedback to send to Sentry.</param>
[DebuggerStepThrough]
public static void CaptureUserFeedback(UserFeedback userFeedback)
=> _hub.CaptureUserFeedback(userFeedback);

/// <summary>
/// Captures a user feedback.
/// </summary>
/// <param name="eventId">The event Id.</param>
/// <param name="email">The user email.</param>
/// <param name="comments">The user comments.</param>
/// <param name="name">The optional username.</param>
[DebuggerStepThrough]
public static void CaptureUserFeedback(SentryId eventId, string email, string comments, string? name = null)
=> _hub.CaptureUserFeedback(new UserFeedback(eventId, email, comments, name));
}
}
29 changes: 29 additions & 0 deletions test/Sentry.Tests/Protocol/UserFeedbackTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Sentry.Protocol;
using Xunit;

namespace Sentry.Tests.Protocol
{
public class UserFeedbackTests
{
[Fact]
public async Task Serialization_SentryUserFeedbacks_Success()
{
// Arrange
var eventId = new SentryId(Guid.Parse("acbe351c61494e7b807fd7e82a435ffc"));
var userFeedback = new UserFeedback(eventId, "[email protected]", "my comment", "myName");
using var stream = new MemoryStream();

// Act
await userFeedback.SerializeAsync(stream, default);
var serializedContent = Encoding.UTF8.GetString(stream.ToArray());

// Assert
var assertExpected = "{\"event_id\":\"acbe351c61494e7b807fd7e82a435ffc\",\"name\":\"myName\",\"email\":\"[email protected]\",\"comments\":\"my comment\"}";
Assert.Equal(assertExpected, serializedContent);
}
}
}
17 changes: 17 additions & 0 deletions test/Sentry.Tests/SentryClientExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,22 @@ public void CaptureMessage_NullMessage_DoesNotCapturesEventWithMessage()
_ = _sut.DidNotReceive().CaptureEvent(Arg.Any<SentryEvent>());
Assert.Equal(default, id);
}

[Fact]
public void CaptureUserFeedback_EnabledClient_CapturesUserFeedback()
{
_ = _sut.IsEnabled.Returns(true);
_sut.CaptureUserFeedback(Guid.Parse("1ec19311a7c048818de80b18dcc43eaa"), "[email protected]", "comments");
_sut.Received(1).CaptureUserFeedback(Arg.Any<UserFeedback>());
}

[Fact]
public void CaptureUserFeedback_DisabledClient_DoesNotCaptureUserFeedback()
{
_ = _sut.IsEnabled.Returns(false);
_sut.CaptureUserFeedback(Guid.Parse("1ec19311a7c048818de80b18dcc43eea"), "[email protected]", "comments");

_sut.DidNotReceive().CaptureUserFeedback(Arg.Any<UserFeedback>());
}
}
}
Loading