Skip to content

Commit 38a53b3

Browse files
authored
Merge b703d2e into b6702b0
2 parents b6702b0 + b703d2e commit 38a53b3

File tree

8 files changed

+221
-4
lines changed

8 files changed

+221
-4
lines changed

sentry/api/sentry.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ public final class io/sentry/DataCategory : java/lang/Enum {
347347
public static final field Default Lio/sentry/DataCategory;
348348
public static final field Error Lio/sentry/DataCategory;
349349
public static final field Feedback Lio/sentry/DataCategory;
350+
public static final field LogByte Lio/sentry/DataCategory;
350351
public static final field LogItem Lio/sentry/DataCategory;
351352
public static final field Monitor Lio/sentry/DataCategory;
352353
public static final field Profile Lio/sentry/DataCategory;
@@ -7062,6 +7063,7 @@ public final class io/sentry/util/IntegrationUtils {
70627063
public final class io/sentry/util/JsonSerializationUtils {
70637064
public fun <init> ()V
70647065
public static fun atomicIntegerArrayToList (Ljava/util/concurrent/atomic/AtomicIntegerArray;)Ljava/util/List;
7066+
public static fun byteSizeOf (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/JsonSerializable;)J
70657067
public static fun bytesFrom (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/JsonSerializable;)[B
70667068
public static fun calendarToMap (Ljava/util/Calendar;)Ljava/util/Map;
70677069
}

sentry/src/main/java/io/sentry/DataCategory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public enum DataCategory {
1010
Session("session"),
1111
Attachment("attachment"),
1212
LogItem("log_item"),
13+
LogByte("log_byte"),
1314
Monitor("monitor"),
1415
Profile("profile"),
1516
ProfileChunkUi("profile_chunk_ui"),

sentry/src/main/java/io/sentry/SentryClient.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,13 +1183,21 @@ public void captureLog(@Nullable SentryLogEvent logEvent, @Nullable IScope scope
11831183
}
11841184

11851185
if (logEvent != null) {
1186+
final @NotNull SentryLogEvent tmpLogEvent = logEvent;
11861187
logEvent = executeBeforeSendLog(logEvent);
11871188

11881189
if (logEvent == null) {
11891190
options.getLogger().log(SentryLevel.DEBUG, "Log Event was dropped by beforeSendLog");
11901191
options
11911192
.getClientReportRecorder()
11921193
.recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.LogItem);
1194+
final @NotNull long logEventNumberOfBytes =
1195+
JsonSerializationUtils.byteSizeOf(
1196+
options.getSerializer(), options.getLogger(), tmpLogEvent);
1197+
options
1198+
.getClientReportRecorder()
1199+
.recordLostEvent(
1200+
DiscardReason.BEFORE_SEND, DataCategory.LogByte, logEventNumberOfBytes);
11931201
return;
11941202
}
11951203

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import io.sentry.SentryEnvelopeItem;
77
import io.sentry.SentryItemType;
88
import io.sentry.SentryLevel;
9+
import io.sentry.SentryLogEvent;
10+
import io.sentry.SentryLogEvents;
911
import io.sentry.SentryOptions;
1012
import io.sentry.protocol.SentrySpan;
1113
import io.sentry.protocol.SentryTransaction;
@@ -98,9 +100,23 @@ public void recordLostEnvelopeItem(
98100
reason.getReason(), DataCategory.Span.getCategory(), spans.size() + 1L);
99101
executeOnDiscard(reason, DataCategory.Span, spans.size() + 1L);
100102
}
103+
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L);
104+
executeOnDiscard(reason, itemCategory, 1L);
105+
} else if (itemCategory.equals(DataCategory.LogItem)) {
106+
final @Nullable SentryLogEvents logs = envelopeItem.getLogs(options.getSerializer());
107+
if (logs != null) {
108+
final @NotNull List<SentryLogEvent> items = logs.getItems();
109+
final long count = items.size();
110+
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), count);
111+
final long logBytes = envelopeItem.getData().length;
112+
recordLostEventInternal(
113+
reason.getReason(), DataCategory.LogByte.getCategory(), logBytes);
114+
executeOnDiscard(reason, itemCategory, count);
115+
}
116+
} else {
117+
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L);
118+
executeOnDiscard(reason, itemCategory, 1L);
101119
}
102-
recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L);
103-
executeOnDiscard(reason, itemCategory, 1L);
104120
}
105121
} catch (Throwable e) {
106122
options.getLogger().log(SentryLevel.ERROR, e, "Unable to record lost envelope item.");

sentry/src/main/java/io/sentry/util/JsonSerializationUtils.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,90 @@ public final class JsonSerializationUtils {
6565
return null;
6666
}
6767
}
68+
69+
/**
70+
* Calculates the size in bytes of a serializable object when serialized to JSON without actually
71+
* storing the serialized data. This is more memory efficient than {@link #bytesFrom(ISerializer,
72+
* ILogger, JsonSerializable)} when you only need the size.
73+
*
74+
* @param serializer the serializer
75+
* @param logger the logger
76+
* @param serializable the serializable object
77+
* @return the size in bytes, or -1 if serialization fails
78+
*/
79+
public static long byteSizeOf(
80+
final @NotNull ISerializer serializer,
81+
final @NotNull ILogger logger,
82+
final @Nullable JsonSerializable serializable) {
83+
if (serializable == null) {
84+
return 0;
85+
}
86+
try {
87+
final ByteCountingWriter writer = new ByteCountingWriter();
88+
serializer.serialize(serializable, writer);
89+
return writer.getByteCount();
90+
} catch (Throwable t) {
91+
logger.log(SentryLevel.ERROR, "Could not calculate size of serializable", t);
92+
return 0;
93+
}
94+
}
95+
96+
/**
97+
* A Writer that counts the number of bytes that would be written in UTF-8 encoding without
98+
* actually storing the data.
99+
*/
100+
private static final class ByteCountingWriter extends Writer {
101+
private long byteCount = 0L;
102+
103+
@Override
104+
public void write(final char[] cbuf, final int off, final int len) {
105+
for (int i = off; i < off + len; i++) {
106+
byteCount += utf8ByteCount(cbuf[i]);
107+
}
108+
}
109+
110+
@Override
111+
public void write(final int c) {
112+
byteCount += utf8ByteCount((char) c);
113+
}
114+
115+
@Override
116+
public void write(final @NotNull String str, final int off, final int len) {
117+
for (int i = off; i < off + len; i++) {
118+
byteCount += utf8ByteCount(str.charAt(i));
119+
}
120+
}
121+
122+
@Override
123+
public void flush() {
124+
// Nothing to flush since we don't store data
125+
}
126+
127+
@Override
128+
public void close() {
129+
// Nothing to close
130+
}
131+
132+
public long getByteCount() {
133+
return byteCount;
134+
}
135+
136+
/**
137+
* Calculates the number of bytes needed to encode a character in UTF-8.
138+
*
139+
* @param c the character
140+
* @return the number of bytes (1-4)
141+
*/
142+
private static int utf8ByteCount(final char c) {
143+
if (c <= 0x7F) {
144+
return 1; // ASCII
145+
} else if (c <= 0x7FF) {
146+
return 2; // 2-byte character
147+
} else if (Character.isSurrogate(c)) {
148+
return 2; // Surrogate pair, counted as 2 bytes each (total 4 for the pair)
149+
} else {
150+
return 3; // 3-byte character
151+
}
152+
}
153+
}
68154
}

sentry/src/test/java/io/sentry/SentryClientTest.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,10 @@ class SentryClientTest {
293293

294294
assertClientReport(
295295
fixture.sentryOptions.clientReportRecorder,
296-
listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1)),
296+
listOf(
297+
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1),
298+
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogByte.category, 109),
299+
),
297300
)
298301
}
299302

@@ -312,7 +315,10 @@ class SentryClientTest {
312315

313316
assertClientReport(
314317
fixture.sentryOptions.clientReportRecorder,
315-
listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1)),
318+
listOf(
319+
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1),
320+
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogByte.category, 109),
321+
),
316322
)
317323
}
318324

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import io.sentry.SentryEnvelope
1616
import io.sentry.SentryEnvelopeHeader
1717
import io.sentry.SentryEnvelopeItem
1818
import io.sentry.SentryEvent
19+
import io.sentry.SentryLogEvent
20+
import io.sentry.SentryLogEvents
21+
import io.sentry.SentryLogLevel
22+
import io.sentry.SentryLongDate
1923
import io.sentry.SentryOptions
2024
import io.sentry.SentryReplayEvent
2125
import io.sentry.SentryTracer
@@ -347,6 +351,37 @@ class ClientReportTest {
347351
verify(onDiscardMock, times(1)).execute(DiscardReason.BEFORE_SEND, DataCategory.Profile, 1)
348352
}
349353

354+
@Test
355+
fun `recording lost client report counts log entries`() {
356+
val onDiscardMock = mock<SentryOptions.OnDiscardCallback>()
357+
givenClientReportRecorder { options -> options.onDiscard = onDiscardMock }
358+
359+
val envelope =
360+
testHelper.newEnvelope(
361+
SentryEnvelopeItem.fromLogs(
362+
opts.serializer,
363+
SentryLogEvents(
364+
listOf(
365+
SentryLogEvent(SentryId(), SentryLongDate(1), "log message 1", SentryLogLevel.ERROR),
366+
SentryLogEvent(SentryId(), SentryLongDate(2), "log message 2", SentryLogLevel.WARN),
367+
)
368+
),
369+
)
370+
)
371+
372+
clientReportRecorder.recordLostEnvelopeItem(DiscardReason.NETWORK_ERROR, envelope.items.first())
373+
374+
verify(onDiscardMock, times(1)).execute(DiscardReason.NETWORK_ERROR, DataCategory.LogItem, 2)
375+
376+
val clientReport = clientReportRecorder.resetCountsAndGenerateClientReport()
377+
val logItem =
378+
clientReport!!.discardedEvents!!.first { it.category == DataCategory.LogItem.category }
379+
assertEquals(2, logItem.quantity)
380+
val logByte =
381+
clientReport!!.discardedEvents!!.first { it.category == DataCategory.LogByte.category }
382+
assertEquals(226, logByte.quantity)
383+
}
384+
350385
private fun givenClientReportRecorder(
351386
callback: Sentry.OptionsConfiguration<SentryOptions>? = null
352387
) {

sentry/src/test/java/io/sentry/util/JsonSerializationUtilsTest.kt

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,28 @@ package io.sentry.util
33
import io.sentry.ILogger
44
import io.sentry.JsonSerializable
55
import io.sentry.JsonSerializer
6+
import io.sentry.ObjectWriter
7+
import io.sentry.SentryLogEvent
8+
import io.sentry.SentryLogLevel
9+
import io.sentry.SentryOptions
10+
import io.sentry.protocol.SentryId
611
import java.io.Writer
712
import java.util.Calendar
813
import java.util.concurrent.atomic.AtomicIntegerArray
914
import kotlin.test.Test
1015
import kotlin.test.assertContentEquals
1116
import kotlin.test.assertEquals
1217
import kotlin.test.assertNull
18+
import kotlin.test.assertTrue
1319
import org.mockito.invocation.InvocationOnMock
1420
import org.mockito.kotlin.any
1521
import org.mockito.kotlin.mock
1622

1723
class JsonSerializationUtilsTest {
24+
25+
private val serializer = JsonSerializer(SentryOptions())
26+
private val logger: ILogger = mock()
27+
1828
@Test
1929
fun `serializes calendar to map`() {
2030
val calendar = Calendar.getInstance()
@@ -74,4 +84,57 @@ class JsonSerializationUtilsTest {
7484

7585
assertNull(actualBytes, "Mocker error should be captured and null returned.")
7686
}
87+
88+
@Test
89+
fun `byteSizeOf returns same size as bytesFrom for ASCII`() {
90+
val logEvent = SentryLogEvent(SentryId(), 1234567890.0, "Hello ASCII", SentryLogLevel.INFO)
91+
92+
val actualBytes = JsonSerializationUtils.bytesFrom(serializer, logger, logEvent)
93+
val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, logEvent)
94+
95+
assertEquals(
96+
(actualBytes?.size ?: -1).toLong(),
97+
byteSize,
98+
"byteSizeOf should match actual byte array length",
99+
)
100+
assertTrue(byteSize > 0, "byteSize should be positive")
101+
}
102+
103+
@Test
104+
fun `byteSizeOf returns same size as bytesFrom for UTF-8 characters`() {
105+
// Mix of 1-byte, 2-byte, 3-byte and 4-byte UTF-8 characters
106+
val logEvent =
107+
SentryLogEvent(SentryId(), 1234567890.0, "Hello 世界 café 🎉 🚀", SentryLogLevel.WARN)
108+
109+
val actualBytes = JsonSerializationUtils.bytesFrom(serializer, logger, logEvent)
110+
val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, logEvent)
111+
112+
assertEquals(
113+
(actualBytes?.size ?: -1).toLong(),
114+
byteSize,
115+
"byteSizeOf should match actual byte array length for UTF-8",
116+
)
117+
assertTrue(byteSize > 0, "byteSize should be positive")
118+
}
119+
120+
@Test
121+
fun `byteSizeOf returns 0 on serialization error`() {
122+
val serializable =
123+
object : JsonSerializable {
124+
override fun serialize(writer: ObjectWriter, logger: ILogger) {
125+
throw RuntimeException("Serialization error")
126+
}
127+
}
128+
129+
val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, serializable)
130+
131+
assertEquals(0, byteSize, "byteSizeOf should return 0 on error")
132+
}
133+
134+
@Test
135+
fun `byteSizeOf returns 0 on null serializable`() {
136+
val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, null)
137+
138+
assertEquals(0, byteSize, "byteSizeOf should return 0 on null serializable")
139+
}
77140
}

0 commit comments

Comments
 (0)