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

Add Client Reports feature #1556

Merged
merged 63 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
c88ade5
Add ThreadsafeCounterDictionary and tests
mattjohnsonpint Mar 25, 2022
4de2c59
Add discard enumerations
mattjohnsonpint Mar 25, 2022
0e8f28e
Count discarded queue overflows
mattjohnsonpint Mar 25, 2022
b81afb6
Add a SendClientReports option
mattjohnsonpint Mar 25, 2022
0f752c4
Separate tests
mattjohnsonpint Mar 26, 2022
16f6c85
Attach client report
mattjohnsonpint Mar 26, 2022
e192ded
Merge branch 'main' into add-client-reports
mattjohnsonpint Mar 26, 2022
af2992c
Update CHANGELOG.md
mattjohnsonpint Mar 26, 2022
9f97777
Update test
mattjohnsonpint Mar 26, 2022
4cf2dcd
Improve test
mattjohnsonpint Mar 27, 2022
7051cdc
Avoid using value tuples
mattjohnsonpint Mar 30, 2022
49c7f2f
Merge branch 'main' into add-client-reports
mattjohnsonpint Mar 31, 2022
36556b5
Merge branch 'main' into add-client-reports
mattjohnsonpint Apr 8, 2022
b75b06a
Update CHANGELOG.md
mattjohnsonpint Apr 8, 2022
5ce3558
Refactor to move discards to transport
mattjohnsonpint Apr 8, 2022
a97c258
Test client report output and fix serialization
mattjohnsonpint Apr 8, 2022
7723ae4
Add test
mattjohnsonpint Apr 8, 2022
9f94f9f
Add test
mattjohnsonpint Apr 9, 2022
dfc86b4
Count ratelimit backoffs
mattjohnsonpint Apr 9, 2022
18c7d1c
Count network failures
mattjohnsonpint Apr 9, 2022
2ad1323
Count discards from BeforeSend and EventProcessors
mattjohnsonpint Apr 9, 2022
daddee1
Count discarded sampled transactions
mattjohnsonpint Apr 9, 2022
5441fcf
count caching failures
mattjohnsonpint Apr 9, 2022
6059f5c
Sort enums
mattjohnsonpint Apr 9, 2022
06f6572
Fix broken test
mattjohnsonpint Apr 18, 2022
fdd4da2
Refactor and skip 429 network errors
mattjohnsonpint Apr 19, 2022
7548f90
Add test for rate limit discarded items
mattjohnsonpint Apr 19, 2022
b532831
Add BeforeSend test
mattjohnsonpint Apr 20, 2022
2381db8
Count discards from exception filters
mattjohnsonpint Apr 22, 2022
483785b
Add tests for discards from event processors and exception filters
mattjohnsonpint Apr 22, 2022
498cc86
Record discard for sampled out events (and test)
mattjohnsonpint Apr 22, 2022
b240b73
Merge branch 'main' into add-client-reports
mattjohnsonpint Apr 25, 2022
e26d21a
Add test for recording cache failure discard
mattjohnsonpint Apr 26, 2022
3617ff0
Refactor to split out ClientReportRecorder
mattjohnsonpint Apr 26, 2022
22f3735
Refactoring
mattjohnsonpint Apr 26, 2022
954eccc
Add tests for ClientReportRecorder
mattjohnsonpint Apr 27, 2022
d1e74bb
Minor refactoring
mattjohnsonpint Apr 27, 2022
84e8832
Add failing test
mattjohnsonpint Apr 27, 2022
70ff32a
Caching transport should implement client reports to fix test
mattjohnsonpint Apr 27, 2022
a66bb2c
Move recorder from transport to options
mattjohnsonpint Apr 27, 2022
c1ea87b
Only count when client reports enabled
mattjohnsonpint Apr 27, 2022
aa77329
Add note about self-hosted sentry version to changelog
mattjohnsonpint Apr 27, 2022
5226af1
Merge branch 'main' into add-client-reports
mattjohnsonpint Apr 27, 2022
d77f8e4
Update CHANGELOG.md
mattjohnsonpint Apr 27, 2022
1165a9d
Ensure client reports can be serialized and deserialized
mattjohnsonpint Apr 27, 2022
d6f9db3
Make caching transport write client reports to disk
mattjohnsonpint Apr 27, 2022
e9a30a5
cleanup
mattjohnsonpint Apr 27, 2022
f2d6b0c
Refactoring
mattjohnsonpint Apr 27, 2022
46940cd
Recover counts if sending client reports fails
mattjohnsonpint Apr 28, 2022
3255e1f
Merge branch 'main' into add-client-reports
mattjohnsonpint Apr 28, 2022
2eaf227
docs fixes
SimonCropp Apr 28, 2022
254b63a
Address PR feedback
mattjohnsonpint Apr 28, 2022
766c795
429 should not restore client reports
mattjohnsonpint Apr 28, 2022
0153e55
oops
mattjohnsonpint Apr 28, 2022
d4efbea
oops
mattjohnsonpint Apr 28, 2022
97ff029
break out ProcessEnvelopeItem (#1608)
SimonCropp Apr 28, 2022
2ede699
redundant this
SimonCropp Apr 28, 2022
2807e0f
add IsExternalInit (#1611)
SimonCropp Apr 29, 2022
43d1834
Add comments
mattjohnsonpint May 2, 2022
9299970
minor docs improvements to client reports (#1630)
SimonCropp May 3, 2022
8cac99e
shorten variable names (#1629)
SimonCropp May 3, 2022
80a773b
Merge branch 'main' into add-client-reports
mattjohnsonpint May 3, 2022
d2eb25b
minor
mattjohnsonpint May 3, 2022
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

## Unreleased

**Notice:** If you are using self-hosted Sentry, this version and forward requires either Sentry version >= [21.9.0](https://github.com/getsentry/relay/blob/master/CHANGELOG.md#2190), or you must manually disable sending client reports via the `SendClientReports` option.

### Features

- Collect and send Client Reports to Sentry, which contain counts of discarded events. ([#1556](https://github.com/getsentry/sentry-dotnet/pull/1556))
- Expose `ITransport` and `SentryOptions.Transport` public, to support using custom transports ([#1602](https://github.com/getsentry/sentry-dotnet/pull/1602))

### Fixes
Expand Down
12 changes: 12 additions & 0 deletions src/Sentry/Envelopes/Envelope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,17 @@ public static async Task<Envelope> DeserializeAsync(

return new Envelope(header, items);
}

/// <summary>
/// Creates a new <see cref="Envelope"/> starting from the current one and appends the <paramref name="item"/> given.
/// </summary>
/// <param name="item">The <see cref="EnvelopeItem"/> to append.</param>
/// <returns>A new <see cref="Envelope"/> with the same headers and items, including the new <paramref name="item"/>.</returns>
internal Envelope WithItem(EnvelopeItem item)
{
var items = Items.ToList();
items.Add(item);
return new Envelope(Header, items);
}
}
}
51 changes: 46 additions & 5 deletions src/Sentry/Envelopes/EnvelopeItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ namespace Sentry.Protocol.Envelopes
public sealed class EnvelopeItem : ISerializable, IDisposable
{
private const string TypeKey = "type";

private const string TypeValueEvent = "event";
private const string TypeValueUserReport = "user_report";
private const string TypeValueTransaction = "transaction";
private const string TypeValueSession = "session";
private const string TypeValueAttachment = "attachment";
private const string TypeValueClientReport = "client_report";

private const string LengthKey = "length";
private const string FileNameKey = "filename";

Expand All @@ -34,6 +37,21 @@ public sealed class EnvelopeItem : ISerializable, IDisposable
/// </summary>
public ISerializable Payload { get; }

internal DataCategory DataCategory => TryGetType() switch
{
// Yes, the "event" item type corresponds to the "error" data category
TypeValueEvent => DataCategory.Error,

// These ones are equivalent
TypeValueTransaction => DataCategory.Transaction,
TypeValueSession => DataCategory.Session,
TypeValueAttachment => DataCategory.Attachment,

// Not all envelope item types equate to data categories
// Specifically, user_report and client_report just use "default"
_ => DataCategory.Default
};

/// <summary>
/// Initializes an instance of <see cref="EnvelopeItem"/>.
/// </summary>
Expand Down Expand Up @@ -187,7 +205,7 @@ public void Serialize(Stream stream, IDiagnosticLogger? logger)
public void Dispose() => (Payload as IDisposable)?.Dispose();

/// <summary>
/// Creates an envelope item from an event.
/// Creates an <see cref="EnvelopeItem"/> from <paramref name="event"/>.
/// </summary>
public static EnvelopeItem FromEvent(SentryEvent @event)
{
Expand All @@ -200,7 +218,7 @@ public static EnvelopeItem FromEvent(SentryEvent @event)
}

/// <summary>
/// Creates an envelope item from user feedback.
/// Creates an <see cref="EnvelopeItem"/> from <paramref name="sentryUserFeedback"/>.
/// </summary>
public static EnvelopeItem FromUserFeedback(UserFeedback sentryUserFeedback)
{
Expand All @@ -213,7 +231,7 @@ public static EnvelopeItem FromUserFeedback(UserFeedback sentryUserFeedback)
}

/// <summary>
/// Creates an envelope item from transaction.
/// Creates an <see cref="EnvelopeItem"/> from <paramref name="transaction"/>.
/// </summary>
public static EnvelopeItem FromTransaction(Transaction transaction)
{
Expand All @@ -226,7 +244,7 @@ public static EnvelopeItem FromTransaction(Transaction transaction)
}

/// <summary>
/// Creates an envelope item from a session update.
/// Creates an <see cref="EnvelopeItem"/> from <paramref name="sessionUpdate"/>.
/// </summary>
public static EnvelopeItem FromSession(SessionUpdate sessionUpdate)
{
Expand All @@ -239,7 +257,7 @@ public static EnvelopeItem FromSession(SessionUpdate sessionUpdate)
}

/// <summary>
/// Creates an envelope item from attachment.
/// Creates an <see cref="EnvelopeItem"/> from <paramref name="attachment"/>.
/// </summary>
public static EnvelopeItem FromAttachment(Attachment attachment)
{
Expand All @@ -266,6 +284,19 @@ public static EnvelopeItem FromAttachment(Attachment attachment)
return new EnvelopeItem(header, new StreamSerializable(stream));
}

/// <summary>
/// Creates an <see cref="EnvelopeItem"/> from <paramref name="report"/>.
/// </summary>
internal static EnvelopeItem FromClientReport(ClientReport report)
{
var header = new Dictionary<string, object?>(1, StringComparer.Ordinal)
{
[TypeKey] = TypeValueClientReport
};

return new EnvelopeItem(header, new JsonSerializable(report));
}

private static async Task<IReadOnlyDictionary<string, object?>> DeserializeHeaderAsync(
Stream stream,
CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -343,6 +374,16 @@ private static async Task<ISerializable> DeserializePayloadAsync(
return new JsonSerializable(SessionUpdate.FromJson(json));
}

// Client Report
if (string.Equals(payloadType, TypeValueClientReport, StringComparison.OrdinalIgnoreCase))
{
var bufferLength = (int)(payloadLength ?? stream.Length);
var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false);
var json = Json.Parse(buffer);

return new JsonSerializable(ClientReport.FromJson(json));
}

// Arbitrary payload
var payloadStream = new PartialStream(stream, stream.Position, payloadLength);

Expand Down
161 changes: 107 additions & 54 deletions src/Sentry/Http/HttpTransportBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace Sentry.Http
public abstract class HttpTransportBase
{
internal const string DefaultErrorMessage = "No message";

private readonly SentryOptions _options;
private readonly ISystemClock _clock;
private readonly Func<string, string?> _getEnvironmentVariable;
Expand Down Expand Up @@ -59,83 +60,111 @@ protected HttpTransportBase(SentryOptions options,
/// </summary>
/// <param name="envelope">The envelope to process.</param>
/// <returns>The processed envelope, ready to be sent.</returns>
protected Envelope ProcessEnvelope(Envelope envelope)
protected internal Envelope ProcessEnvelope(Envelope envelope)
{
var now = _clock.GetUtcNow();

// Re-package envelope, discarding items that don't fit the rate limit
var envelopeItems = new List<EnvelopeItem>();
foreach (var envelopeItem in envelope.Items)
{
// Check if there is at least one matching category for this item that is rate-limited
var isRateLimited = CategoryLimitResets
.Any(kvp => kvp.Value > now && kvp.Key.Matches(envelopeItem));
ProcessEnvelopeItem(now, envelopeItem, envelopeItems);
}

if (isRateLimited)
{
_options.LogDebug(
"Envelope item of type {0} was discarded because it's rate-limited.",
envelopeItem.TryGetType());

// Check if session update with init=true
if (envelopeItem.Payload is JsonSerializable
{
Source: SessionUpdate {IsInitial: true} discardedSessionUpdate
})
{
_lastDiscardedSessionInitId = discardedSessionUpdate.Id.ToString();
var eventId = envelope.TryGetEventId();

_options.LogDebug(
"Discarded envelope item containing initial session update (SID: {0}).",
discardedSessionUpdate.Id);
}
var clientReport = _options.ClientReportRecorder.GenerateClientReport();
if (clientReport != null)
{
envelopeItems.Add(EnvelopeItem.FromClientReport(clientReport));
_options.LogDebug("Attached client report to envelope {0}.", eventId);
}

continue;
if (envelopeItems.Count == 0)
{
if (_options.SendClientReports)
{
_options.LogInfo("Envelope {0} was discarded because all contained items are rate-limited " +
SimonCropp marked this conversation as resolved.
Show resolved Hide resolved
"and there are no client reports to send.",
eventId);
}

// If attachment, needs to respect attachment size limit
if (string.Equals(envelopeItem.TryGetType(), "attachment", StringComparison.OrdinalIgnoreCase) &&
envelopeItem.TryGetLength() > _options.MaxAttachmentSize)
else
{
_options.LogWarning(
"Attachment '{0}' dropped because it's too large ({1} bytes).",
envelopeItem.TryGetFileName(),
envelopeItem.TryGetLength());

continue;
_options.LogInfo("Envelope {0} was discarded because all contained items are rate-limited.",
eventId);
}
}

// If it's a session update (not discarded) with init=false, check if it continues
// a session with previously dropped init and, if so, promote this update to init=true.
if (envelopeItem.Payload is JsonSerializable {Source: SessionUpdate {IsInitial: false} sessionUpdate} &&
string.Equals(sessionUpdate.Id.ToString(),
Interlocked.Exchange(ref _lastDiscardedSessionInitId, null),
StringComparison.Ordinal))
{
var modifiedEnvelopeItem = new EnvelopeItem(
envelopeItem.Header,
new JsonSerializable(new SessionUpdate(sessionUpdate, true)));
return new Envelope(envelope.Header, envelopeItems);
}

envelopeItems.Add(modifiedEnvelopeItem);
private void ProcessEnvelopeItem(DateTimeOffset now, EnvelopeItem item, List<EnvelopeItem> items)
{
// Check if there is at least one matching category for this item that is rate-limited
var isRateLimited = CategoryLimitResets
.Any(kvp => kvp.Value > now && kvp.Key.Matches(item));

_options.LogDebug(
"Promoted envelope item with session update to initial following a discarded update (SID: {0}).",
sessionUpdate.Id);
}
else
if (isRateLimited)
{
_options.ClientReportRecorder
.RecordDiscardedEvent(DiscardReason.RateLimitBackoff, item.DataCategory);

_options.LogDebug(
"Envelope item of type {0} was discarded because it's rate-limited.",
item.TryGetType());

// Check if session update with init=true
if (item.Payload is JsonSerializable
{
Source: SessionUpdate {IsInitial: true} discardedSessionUpdate
})
{
envelopeItems.Add(envelopeItem);
_lastDiscardedSessionInitId = discardedSessionUpdate.Id.ToString();

_options.LogDebug(
"Discarded envelope item containing initial session update (SID: {0}).",
discardedSessionUpdate.Id);
}

return;
}

if (envelopeItems.Count == 0)
// If attachment, needs to respect attachment size limit
if (string.Equals(item.TryGetType(), "attachment", StringComparison.OrdinalIgnoreCase) &&
item.TryGetLength() > _options.MaxAttachmentSize)
{
_options.LogInfo(
"Envelope {0} was discarded because all contained items are rate-limited.",
envelope.TryGetEventId());
// note: attachment drops are not currently counted in discarded events

_options.LogWarning(
"Attachment '{0}' dropped because it's too large ({1} bytes).",
item.TryGetFileName(),
item.TryGetLength());

return;
}

return new Envelope(envelope.Header, envelopeItems);
// If it's a session update (not discarded) with init=false, check if it continues
// a session with previously dropped init and, if so, promote this update to init=true.
if (item.Payload is JsonSerializable {Source: SessionUpdate {IsInitial: false} sessionUpdate} &&
string.Equals(sessionUpdate.Id.ToString(),
Interlocked.Exchange(ref _lastDiscardedSessionInitId, null),
StringComparison.Ordinal))
{
var modifiedEnvelopeItem = new EnvelopeItem(
item.Header,
new JsonSerializable(new SessionUpdate(sessionUpdate, true)));

items.Add(modifiedEnvelopeItem);

_options.LogDebug(
"Promoted envelope item with session update to initial following a discarded update (SID: {0}).",
sessionUpdate.Id);

return;
}

// Finally, add this item to the result
items.Add(item);
}

/// <summary>
Expand Down Expand Up @@ -289,6 +318,8 @@ private async Task HandleSuccessAsync(Envelope envelope, CancellationToken cance

private void HandleFailure(HttpResponseMessage response, Envelope envelope)
{
IncrementDiscardsForHttpFailure(response.StatusCode, envelope);

// Spare the overhead if level is not enabled
if (_options.DiagnosticLogger?.IsEnabled(SentryLevel.Error) is true && response.Content is { } content)
{
Expand Down Expand Up @@ -341,6 +372,8 @@ private void HandleFailure(HttpResponseMessage response, Envelope envelope)
private async Task HandleFailureAsync(HttpResponseMessage response, Envelope envelope,
CancellationToken cancellationToken)
{
IncrementDiscardsForHttpFailure(response.StatusCode, envelope);

// Spare the overhead if level is not enabled
if (_options.DiagnosticLogger?.IsEnabled(SentryLevel.Error) is true && response.Content is { } content)
{
Expand Down Expand Up @@ -397,6 +430,26 @@ await envelope
}
}

private void IncrementDiscardsForHttpFailure(HttpStatusCode responseStatusCode, Envelope envelope)
{
if ((int)responseStatusCode is 429 or < 400)
{
// Status == 429 or < 400 should not be counted by the client SDK
// See https://develop.sentry.dev/sdk/client-reports/#sdk-side-recommendations
return;
mattjohnsonpint marked this conversation as resolved.
Show resolved Hide resolved
}

_options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.NetworkError, envelope);

// Also restore any counts that were trying to be sent, so they are not lost.
var clientReportItems = envelope.Items.Where(x => x.TryGetType() == "client_report");
foreach (var item in clientReportItems)
{
var clientReport = (ClientReport)((JsonSerializable)item.Payload).Source;
_options.ClientReportRecorder.Load(clientReport);
}
}

private void LogFailure(string responseString, HttpStatusCode responseStatusCode, SentryId? eventId)
{
_options.Log(SentryLevel.Error,
Expand Down
3 changes: 3 additions & 0 deletions src/Sentry/Internal/BackgroundWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using Sentry.Extensibility;
using Sentry.Internal.Extensions;
using Sentry.Protocol.Envelopes;

namespace Sentry.Internal
Expand Down Expand Up @@ -59,6 +60,7 @@ public bool EnqueueEnvelope(Envelope envelope)
if (Interlocked.Increment(ref _currentItems) > _maxItems)
{
_ = Interlocked.Decrement(ref _currentItems);
_options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.QueueOverflow, envelope);
return false;
}

Expand Down Expand Up @@ -121,6 +123,7 @@ private async Task WorkerAsync()
// Dispose inside try/catch
using var _ = envelope;

// Send the envelope
var task = _transport.SendEnvelopeAsync(envelope, shutdownTimeout.Token);

_options.LogDebug(
Expand Down
Loading