Skip to content

Commit 5b35d8e

Browse files
Client reports now include dropped spans (#3463)
1 parent d886351 commit 5b35d8e

File tree

12 files changed

+153
-22
lines changed

12 files changed

+153
-22
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Client reports now include dropped spans ([#3463](https://github.com/getsentry/sentry-dotnet/pull/3463))
8+
39
## 4.8.1
410

511
### Fixes

src/Sentry/Http/HttpTransportBase.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,23 @@ private void ProcessEnvelopeItem(DateTimeOffset now, EnvelopeItem item, List<Env
100100

101101
if (isRateLimited)
102102
{
103-
_options.ClientReportRecorder
104-
.RecordDiscardedEvent(DiscardReason.RateLimitBackoff, item.DataCategory);
103+
var reason = DiscardReason.RateLimitBackoff;
104+
_options.ClientReportRecorder.RecordDiscardedEvent(reason, item.DataCategory);
105105

106106
_options.LogDebug(
107107
"{0}: Envelope item of type {1} was discarded because it's rate-limited.",
108108
_typeName,
109109
item.TryGetType());
110110

111+
if (item.DataCategory.Equals(DataCategory.Transaction))
112+
{
113+
if (item.Payload is JsonSerializable { Source: SentryTransaction transaction })
114+
{
115+
// Span count + 1 (transaction/root span)
116+
_options.ClientReportRecorder.RecordDiscardedEvent(reason, DataCategory.Span, transaction.Spans.Count + 1);
117+
}
118+
}
119+
111120
// Check if session update with init=true
112121
if (item.Payload is JsonSerializable
113122
{

src/Sentry/Internal/ClientReportRecorder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public ClientReportRecorder(SentryOptions options, ISystemClock? clock = default
1818
_clock = clock ?? SystemClock.Clock;
1919
}
2020

21-
public void RecordDiscardedEvent(DiscardReason reason, DataCategory category)
21+
public void RecordDiscardedEvent(DiscardReason reason, DataCategory category, int quantity = 1)
2222
{
2323
// Don't count discarded events if we're not going to be sending them.
2424
if (!_options.SendClientReports)
@@ -27,7 +27,7 @@ public void RecordDiscardedEvent(DiscardReason reason, DataCategory category)
2727
}
2828

2929
// Increment the counter for the discarded event.
30-
_discardedEvents.Increment(reason.WithCategory(category));
30+
_discardedEvents.Add(reason.WithCategory(category), quantity);
3131
}
3232

3333
public ClientReport? GenerateClientReport()

src/Sentry/Internal/DataCategory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace Sentry.Internal;
99
public static DataCategory Internal = new("internal");
1010
public static DataCategory Security = new("security");
1111
public static DataCategory Session = new("session");
12+
public static DataCategory Span = new("span");
1213
public static DataCategory Transaction = new("transaction");
1314
public static DataCategory Profile = new("profile");
1415

src/Sentry/Internal/Extensions/ClientReportExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ public static void RecordDiscardedEvents(this IClientReportRecorder recorder, Di
99
foreach (var item in envelope.Items)
1010
{
1111
recorder.RecordDiscardedEvent(reason, item.DataCategory);
12+
if (item.DataCategory.Equals(DataCategory.Transaction))
13+
{
14+
if (item.Payload is JsonSerializable { Source: SentryTransaction transaction })
15+
{
16+
// Span count + 1 (transaction/root span)
17+
recorder.RecordDiscardedEvent(reason, DataCategory.Span, transaction.Spans.Count + 1);
18+
}
19+
}
1220
}
1321
}
1422
}

src/Sentry/Internal/IClientReportRecorder.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ internal interface IClientReportRecorder
66
/// Records one count of a discarded event, with the given <paramref name="reason"/> and <paramref name="category"/>.
77
/// </summary>
88
/// <param name="reason">The reason for the event being discarded.</param>
9-
/// <param name="category">The data category of the event being discarded.</param>
10-
void RecordDiscardedEvent(DiscardReason reason, DataCategory category);
9+
/// <param name="category">The data category of the event being discardedd.</param>
10+
/// <param name="quantity">The number of items discarded (defaults to 1)</param>
11+
void RecordDiscardedEvent(DiscardReason reason, DataCategory category, int quantity = 1);
1112

1213
/// <summary>
1314
/// Generates a <see cref="ClientReport"/> containing counts of discarded events that have been recorded.

src/Sentry/Protocol/Envelopes/EnvelopeItem.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public sealed class EnvelopeItem : ISerializable, IDisposable
1515
internal const string TypeValueEvent = "event";
1616
internal const string TypeValueUserReport = "user_report";
1717
internal const string TypeValueTransaction = "transaction";
18+
internal const string TypeValueSpan = "span";
1819
internal const string TypeValueSession = "session";
1920
internal const string TypeValueCheckIn = "check_in";
2021
internal const string TypeValueAttachment = "attachment";
@@ -43,6 +44,7 @@ public sealed class EnvelopeItem : ISerializable, IDisposable
4344

4445
// These ones are equivalent
4546
TypeValueTransaction => DataCategory.Transaction,
47+
TypeValueSpan => DataCategory.Span,
4648
TypeValueSession => DataCategory.Session,
4749
TypeValueAttachment => DataCategory.Attachment,
4850
TypeValueProfile => DataCategory.Profile,

src/Sentry/SentryClient.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,11 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent
126126
// Sampling decision MUST have been made at this point
127127
Debug.Assert(transaction.IsSampled is not null, "Attempt to capture transaction without sampling decision.");
128128

129+
var spanCount = transaction.Spans.Count + 1; // 1 for each span + 1 for the transaction itself
129130
if (transaction.IsSampled is false)
130131
{
131132
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Transaction);
133+
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Span, spanCount);
132134
_options.LogDebug("Transaction dropped by sampling.");
133135
return;
134136
}
@@ -151,6 +153,7 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent
151153
if (processedTransaction == null) // Rejected transaction
152154
{
153155
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Transaction);
156+
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Span, spanCount);
154157
_options.LogInfo("Event dropped by processor {0}", processor.GetType().Name);
155158
return;
156159
}
@@ -160,6 +163,7 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent
160163
if (processedTransaction is null) // Rejected transaction
161164
{
162165
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Transaction);
166+
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Span, spanCount);
163167
_options.LogInfo("Transaction dropped by BeforeSendTransaction callback.");
164168
return;
165169
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace Sentry.Tests.Internals.Extensions;
2+
3+
public class ClientReportExtensionsTests
4+
{
5+
[Fact]
6+
public void EnvelopeContainsTransaction_ReportsDroppedSpans()
7+
{
8+
// Arrange
9+
var recorder = Substitute.For<IClientReportRecorder>();
10+
var hub = Substitute.For<IHub>();
11+
var tracer = new TransactionTracer(hub, "name", "op");
12+
var span1 = (SpanTracer)tracer.StartChild(null, tracer.SpanId, "span1");
13+
tracer.StartChild(null, span1.SpanId, "span2");
14+
tracer.StartChild(null, tracer.SpanId, "span3");
15+
var transaction = new SentryTransaction(tracer);
16+
var envelope = Envelope.FromTransaction(transaction);
17+
18+
// Act
19+
recorder.RecordDiscardedEvents(DiscardReason.EventProcessor, envelope);
20+
21+
// Assert
22+
recorder.Received(1).RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Transaction, 1);
23+
// 1 for each span + 1 for the transaction root span
24+
recorder.Received(1).RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Span, transaction.Spans.Count + 1);
25+
}
26+
}

test/Sentry.Tests/Internals/Http/HttpTransportTests.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ public async Task CreateRequest_Content_IncludesEvent()
793793
}
794794

795795
[Fact]
796-
public void ProcessEnvelope_ShouldNotAttachClientReportWhenOptionDisabled()
796+
public void ProcessEnvelope_SendClientReportsDisabled_ShouldNotAttachClientReport()
797797
{
798798
var options = new SentryOptions
799799
{
@@ -813,4 +813,37 @@ public void ProcessEnvelope_ShouldNotAttachClientReportWhenOptionDisabled()
813813
Assert.Single(processedEnvelope.Items);
814814
Assert.Equal("event", processedEnvelope.Items[0].TryGetType());
815815
}
816+
817+
[Fact]
818+
public void ProcessEnvelope_SendClientReportsEnabled_ShouldReportTransactionsAndSpans()
819+
{
820+
// Arrange
821+
var options = new SentryOptions
822+
{
823+
SendClientReports = true,
824+
ClientReportRecorder = Substitute.For<IClientReportRecorder>()
825+
};
826+
827+
var httpTransport = Substitute.For<HttpTransportBase>(options, null, null);
828+
var transactionCategory = new RateLimitCategory(EnvelopeItem.TypeValueTransaction);
829+
httpTransport.CategoryLimitResets[transactionCategory] = DateTimeOffset.UtcNow.AddMonths(1);
830+
831+
var hub = Substitute.For<IHub>();
832+
var tracer = new TransactionTracer(hub, "name", "op");
833+
var span1 = (SpanTracer)tracer.StartChild(null, tracer.SpanId, "span1");
834+
tracer.StartChild(null, span1.SpanId, "span2");
835+
tracer.StartChild(null, tracer.SpanId, "span3");
836+
var transaction = new SentryTransaction(tracer);
837+
var envelope = Envelope.FromTransaction(transaction);
838+
839+
// Act
840+
var processedEnvelope = httpTransport.ProcessEnvelope(envelope);
841+
842+
// Assert
843+
processedEnvelope.Items.Should().BeEmpty();
844+
options.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.RateLimitBackoff, DataCategory.Transaction, 1);
845+
// 1 for each span + 1 for the transaction root span
846+
var expectedDiscardedSpanCount = transaction.Spans.Count + 1;
847+
options.ClientReportRecorder.Received(1).RecordDiscardedEvent(DiscardReason.RateLimitBackoff, DataCategory.Span, expectedDiscardedSpanCount);
848+
}
816849
}

0 commit comments

Comments
 (0)