Skip to content
Merged
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 @@ -9,6 +9,7 @@
- [Config] Trim whitespace on properties path ([#4880](https://github.com/getsentry/sentry-java/pull/4880))
- Only set `DefaultReplayBreadcrumbConverter` if replay is available ([#4888](https://github.com/getsentry/sentry-java/pull/4888))
- Session Replay: Cache connection status instead of using blocking calls ([#4891](https://github.com/getsentry/sentry-java/pull/4891))
- Fix log count in client reports ([#4869](https://github.com/getsentry/sentry-java/pull/4869))

### Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.sentry.SentryEnvelopeItem;
import io.sentry.SentryItemType;
import io.sentry.SentryLevel;
import io.sentry.SentryLogEvents;
import io.sentry.SentryOptions;
import io.sentry.protocol.SentrySpan;
import io.sentry.protocol.SentryTransaction;
Expand Down Expand Up @@ -98,9 +99,19 @@ public void recordLostEnvelopeItem(
reason.getReason(), DataCategory.Span.getCategory(), spans.size() + 1L);
executeOnDiscard(reason, DataCategory.Span, spans.size() + 1L);
}
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L);
executeOnDiscard(reason, itemCategory, 1L);
} else if (itemCategory.equals(DataCategory.LogItem)) {
final @Nullable SentryLogEvents logs = envelopeItem.getLogs(options.getSerializer());
if (logs != null) {
final long count = logs.getItems().size();
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), count);
executeOnDiscard(reason, itemCategory, count);
}
Comment on lines +104 to +110
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: LogItem deserialization failures do not record lost items, leading to inaccurate client reports, unlike Transaction items.
Severity: CRITICAL | Confidence: 0.95

🔍 Detailed Analysis

When a LogItem envelope item fails to deserialize via envelopeItem.getLogs(options.getSerializer()) (either returning null or throwing an exception), the code responsible for recording lost items is skipped. This occurs because the recordLostEventInternal and executeOnDiscard calls are conditionally placed inside an if (logs != null) block. Consequently, client reports will not accurately reflect all lost LogItem events, specifically those that are malformed and fail deserialization. This behavior is inconsistent with how Transaction items are handled, where a lost item is recorded regardless of deserialization success.

💡 Suggested Fix

Move the recordLostEventInternal and executeOnDiscard calls for LogItem outside the if (logs != null) block. This ensures that a lost item is recorded even if LogItem deserialization fails, aligning its behavior with Transaction item handling.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java#L104-L110

Potential issue: When a `LogItem` envelope item fails to deserialize via
`envelopeItem.getLogs(options.getSerializer())` (either returning `null` or throwing an
exception), the code responsible for recording lost items is skipped. This occurs
because the `recordLostEventInternal` and `executeOnDiscard` calls are conditionally
placed inside an `if (logs != null)` block. Consequently, client reports will not
accurately reflect all lost `LogItem` events, specifically those that are malformed and
fail deserialization. This behavior is inconsistent with how `Transaction` items are
handled, where a lost item is recorded regardless of deserialization success.

Did we get this right? 👍 / 👎 to inform future reviews.

} else {
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L);
executeOnDiscard(reason, itemCategory, 1L);
}
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L);
executeOnDiscard(reason, itemCategory, 1L);
}
} catch (Throwable e) {
options.getLogger().log(SentryLevel.ERROR, e, "Unable to record lost envelope item.");
Expand Down
32 changes: 32 additions & 0 deletions sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import io.sentry.SentryEnvelope
import io.sentry.SentryEnvelopeHeader
import io.sentry.SentryEnvelopeItem
import io.sentry.SentryEvent
import io.sentry.SentryLogEvent
import io.sentry.SentryLogEvents
import io.sentry.SentryLogLevel
import io.sentry.SentryLongDate
import io.sentry.SentryOptions
import io.sentry.SentryReplayEvent
import io.sentry.SentryTracer
Expand Down Expand Up @@ -347,6 +351,34 @@ class ClientReportTest {
verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Profile, 1)
}

@Test
fun `recording lost client report counts log entries`() {
val onDiscardMock = mock<SentryOptions.OnDiscardCallback>()
givenClientReportRecorder { options -> options.onDiscard = onDiscardMock }

val envelope =
testHelper.newEnvelope(
SentryEnvelopeItem.fromLogs(
opts.serializer,
SentryLogEvents(
listOf(
SentryLogEvent(SentryId(), SentryLongDate(1), "log message 1", SentryLogLevel.ERROR),
SentryLogEvent(SentryId(), SentryLongDate(2), "log message 2", SentryLogLevel.WARN),
)
),
)
)

clientReportRecorder.recordLostEnvelopeItem(DiscardReason.NETWORK_ERROR, envelope.items.first())

verify(onDiscardMock, times(1)).execute(DiscardReason.NETWORK_ERROR, DataCategory.LogItem, 2)

val clientReport = clientReportRecorder.resetCountsAndGenerateClientReport()
val logItem =
clientReport!!.discardedEvents!!.first { it.category == DataCategory.LogItem.category }
assertEquals(2, logItem.quantity)
}

private fun givenClientReportRecorder(
callback: Sentry.OptionsConfiguration<SentryOptions>? = null
) {
Expand Down
Loading