Skip to content

Commit

Permalink
feat: Manually capturing user feedback (#559)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucas-zimerman authored Oct 29, 2020
1 parent 038e6de commit 0c2f718
Show file tree
Hide file tree
Showing 15 changed files with 329 additions and 6 deletions.
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
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);
}
}
}
58 changes: 58 additions & 0 deletions src/Sentry/Protocol/UserFeedback.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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
{
[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);
}
}
44 changes: 40 additions & 4 deletions src/Sentry/SentryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,34 @@ 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.");
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.");
return;
}

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

/// <summary>
/// Flushes events asynchronously.
/// </summary>
Expand Down Expand Up @@ -165,18 +193,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

0 comments on commit 0c2f718

Please sign in to comment.