Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -4,6 +4,7 @@

### Fixes

- Report the number of dropped spans in client reports when a batched span envelope item is discarded, instead of a quantity of 1 (e.g. for Flutter span streaming) ([#6265](https://github.com/getsentry/sentry-java/pull/6265))
- Measure Session Replay's 1h recording cap and its touch-move debounce on a monotonic clock, so a device time change no longer stops a healthy recording early, keeps a finished one alive, or suppresses gesture events ([#6090](https://github.com/getsentry/sentry-java/pull/6090))

## 8.61.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ public void recordLostEnvelopeItem(
recordLostEventInternal(
reason.getReason(), DataCategory.TraceMetricByte.getCategory(), metricBytes);
executeOnDiscard(reason, itemCategory, count);
} else if (itemCategory.equals(DataCategory.Span)) {
// Batched span items (span streaming) aren't created by this SDK yet, but hybrid SDKs
// like sentry-dart send them through it via InternalSentrySdk.captureEnvelope.
final long count = itemCountFromHeader(envelopeItem);
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), count);
executeOnDiscard(reason, itemCategory, count);
} else {
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L);
executeOnDiscard(reason, itemCategory, 1L);
Expand Down Expand Up @@ -156,9 +162,9 @@ private void recordLostEventInternal(
storage.addCount(key, countToAdd);
}

// The number of items batched into a log or metric envelope item is stored in its header, so we
// read it from there instead of deserializing the payload. Deserializing on the discard path is
// expensive and, under sustained rate limiting, caused a CPU busy-loop (JAVA-662).
// The number of items batched into a log, metric or span envelope item is stored in its header,
// so we read it from there instead of deserializing the payload. Deserializing on the discard
// path is expensive and, under sustained rate limiting, caused a CPU busy-loop (JAVA-662).
private long itemCountFromHeader(final @NotNull SentryEnvelopeItem envelopeItem) {
final @Nullable Integer itemCount = envelopeItem.getHeader().getItemCount();
return itemCount != null ? itemCount : 1L;
Expand Down
59 changes: 59 additions & 0 deletions sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import org.mockito.kotlin.whenever

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

class ClientReportTest {
lateinit var opts: SentryOptions
Expand Down Expand Up @@ -503,6 +504,64 @@ class ClientReportTest {
assertEquals(1, clientReport.quantityOf(DataCategory.TraceMetric))
}

@Test
fun `recording lost span item reads count from the header`() {
val onDiscardMock = mock<SentryOptions.OnDiscardCallback>()
givenClientReportRecorder { options -> options.onDiscard = onDiscardMock }

// Batched span items reach this SDK from hybrid SDKs (e.g. Flutter span streaming).
val item =
mockEnvelopeItem(
SentryItemType.Span,
SPAN_CONTENT_TYPE,
itemCount = 500,
data = "irrelevant payload".toByteArray(),
)

clientReportRecorder.recordLostEnvelopeItem(DiscardReason.NETWORK_ERROR, item)

verify(onDiscardMock, times(1)).execute(DiscardReason.NETWORK_ERROR, DataCategory.Span, 500)

val clientReport = clientReportRecorder.resetCountsAndGenerateClientReport()
assertEquals(500, clientReport.quantityOf(DataCategory.Span))
}

@Test
fun `recording rate limited span item reads count from the header`() {
givenClientReportRecorder()

val item =
mockEnvelopeItem(SentryItemType.Span, SPAN_CONTENT_TYPE, itemCount = 7, data = ByteArray(0))

clientReportRecorder.recordLostEnvelopeItem(DiscardReason.RATELIMIT_BACKOFF, item)

val clientReport = clientReportRecorder.resetCountsAndGenerateClientReport()
val spanItem =
clientReport!!.discardedEvents!!.single {
it.category == DataCategory.Span.category &&
it.reason == DiscardReason.RATELIMIT_BACKOFF.reason
}
assertEquals(7, spanItem.quantity)
}

@Test
fun `recording lost span item without item count in header falls back to one`() {
givenClientReportRecorder()

val item =
mockEnvelopeItem(
SentryItemType.Span,
SPAN_CONTENT_TYPE,
itemCount = null,
data = ByteArray(0),
)

clientReportRecorder.recordLostEnvelopeItem(DiscardReason.NETWORK_ERROR, item)

val clientReport = clientReportRecorder.resetCountsAndGenerateClientReport()
assertEquals(1, clientReport.quantityOf(DataCategory.Span))
}

private fun mockEnvelopeItem(
type: SentryItemType,
contentType: String,
Expand Down
Loading