Skip to content

Commit 779a6ad

Browse files
committed
fix(client-reports): Count spans in batched span envelope items
Prior to this commit, `ClientReportRecorder` didn't have any explicit logic for dealing with span envelopes because sentry-java didn't implement them. That meant that any span envelopes created by hybrid SDKs (in particular sentry-dart) and dropped by sentry-java would be shunted to the recorder's fallback branch and wrongly record a single dropped span per dropped envelope, rather than the actual number. This commit fixes that by adding the missing span envelope logic. Now sentry-java will report the actual span count, rather than 1. Refs [sentry-docs#19854](getsentry/sentry-docs#19854)
1 parent e9f7568 commit 779a6ad

2 files changed

Lines changed: 68 additions & 3 deletions

File tree

‎sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ public void recordLostEnvelopeItem(
113113
recordLostEventInternal(
114114
reason.getReason(), DataCategory.TraceMetricByte.getCategory(), metricBytes);
115115
executeOnDiscard(reason, itemCategory, count);
116+
} else if (itemCategory.equals(DataCategory.Span)) {
117+
// Batched span items (span streaming) aren't created by this SDK yet, but hybrid SDKs
118+
// like sentry-dart send them through it via InternalSentrySdk.captureEnvelope.
119+
final long count = itemCountFromHeader(envelopeItem);
120+
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), count);
121+
executeOnDiscard(reason, itemCategory, count);
116122
} else {
117123
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L);
118124
executeOnDiscard(reason, itemCategory, 1L);
@@ -156,9 +162,9 @@ private void recordLostEventInternal(
156162
storage.addCount(key, countToAdd);
157163
}
158164

159-
// The number of items batched into a log or metric envelope item is stored in its header, so we
160-
// read it from there instead of deserializing the payload. Deserializing on the discard path is
161-
// expensive and, under sustained rate limiting, caused a CPU busy-loop (JAVA-662).
165+
// The number of items batched into a log, metric or span envelope item is stored in its header,
166+
// so we read it from there instead of deserializing the payload. Deserializing on the discard
167+
// path is expensive and, under sustained rate limiting, caused a CPU busy-loop (JAVA-662).
162168
private long itemCountFromHeader(final @NotNull SentryEnvelopeItem envelopeItem) {
163169
final @Nullable Integer itemCount = envelopeItem.getHeader().getItemCount();
164170
return itemCount != null ? itemCount : 1L;

‎sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import org.mockito.kotlin.whenever
5959

6060
private const val LOG_CONTENT_TYPE = "application/vnd.sentry.items.log+json"
6161
private const val METRIC_CONTENT_TYPE = "application/vnd.sentry.items.trace-metric+json"
62+
private const val SPAN_CONTENT_TYPE = "application/vnd.sentry.items.span.v2+json"
6263

6364
class ClientReportTest {
6465
lateinit var opts: SentryOptions
@@ -503,6 +504,64 @@ class ClientReportTest {
503504
assertEquals(1, clientReport.quantityOf(DataCategory.TraceMetric))
504505
}
505506

507+
@Test
508+
fun `recording lost span item reads count from the header`() {
509+
val onDiscardMock = mock<SentryOptions.OnDiscardCallback>()
510+
givenClientReportRecorder { options -> options.onDiscard = onDiscardMock }
511+
512+
// Batched span items reach this SDK from hybrid SDKs (e.g. Flutter span streaming).
513+
val item =
514+
mockEnvelopeItem(
515+
SentryItemType.Span,
516+
SPAN_CONTENT_TYPE,
517+
itemCount = 500,
518+
data = "irrelevant payload".toByteArray(),
519+
)
520+
521+
clientReportRecorder.recordLostEnvelopeItem(DiscardReason.NETWORK_ERROR, item)
522+
523+
verify(onDiscardMock, times(1)).execute(DiscardReason.NETWORK_ERROR, DataCategory.Span, 500)
524+
525+
val clientReport = clientReportRecorder.resetCountsAndGenerateClientReport()
526+
assertEquals(500, clientReport.quantityOf(DataCategory.Span))
527+
}
528+
529+
@Test
530+
fun `recording rate limited span item reads count from the header`() {
531+
givenClientReportRecorder()
532+
533+
val item =
534+
mockEnvelopeItem(SentryItemType.Span, SPAN_CONTENT_TYPE, itemCount = 7, data = ByteArray(0))
535+
536+
clientReportRecorder.recordLostEnvelopeItem(DiscardReason.RATELIMIT_BACKOFF, item)
537+
538+
val clientReport = clientReportRecorder.resetCountsAndGenerateClientReport()
539+
val spanItem =
540+
clientReport!!.discardedEvents!!.single {
541+
it.category == DataCategory.Span.category &&
542+
it.reason == DiscardReason.RATELIMIT_BACKOFF.reason
543+
}
544+
assertEquals(7, spanItem.quantity)
545+
}
546+
547+
@Test
548+
fun `recording lost span item without item count in header falls back to one`() {
549+
givenClientReportRecorder()
550+
551+
val item =
552+
mockEnvelopeItem(
553+
SentryItemType.Span,
554+
SPAN_CONTENT_TYPE,
555+
itemCount = null,
556+
data = ByteArray(0),
557+
)
558+
559+
clientReportRecorder.recordLostEnvelopeItem(DiscardReason.NETWORK_ERROR, item)
560+
561+
val clientReport = clientReportRecorder.resetCountsAndGenerateClientReport()
562+
assertEquals(1, clientReport.quantityOf(DataCategory.Span))
563+
}
564+
506565
private fun mockEnvelopeItem(
507566
type: SentryItemType,
508567
contentType: String,

0 commit comments

Comments
 (0)