From d2a38ccc2b0092051e9d387165ba7f38032f850b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 3 Nov 2025 13:35:31 +0100 Subject: [PATCH 01/12] Fix log count in client reports --- .../clientreport/ClientReportRecorder.java | 15 +++++++++-- .../sentry/clientreport/ClientReportTest.kt | 27 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index 4e2383eb107..7b415ce48c9 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -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; @@ -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); + } + } 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."); diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt index 2b34fd839b0..eae216239aa 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -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 @@ -347,6 +351,29 @@ 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() + 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) + } + private fun givenClientReportRecorder( callback: Sentry.OptionsConfiguration? = null ) { From 3bd2cf0e1d48489ad7e94fe945ea4bdcdff2adc0 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 3 Nov 2025 15:29:02 +0100 Subject: [PATCH 02/12] add assertion to test --- .../src/test/java/io/sentry/clientreport/ClientReportTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt index eae216239aa..30cfece6267 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -372,6 +372,10 @@ class ClientReportTest { 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( From 6c5011fa67a08d31bc52ef7fb37ba60ab03000c8 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 5 Nov 2025 13:32:00 +0100 Subject: [PATCH 03/12] Detect oversized events --- sentry/api/sentry.api | 14 + .../src/main/java/io/sentry/DataCategory.java | 1 + .../src/main/java/io/sentry/SentryClient.java | 12 + .../main/java/io/sentry/SentryOptions.java | 65 +++ .../clientreport/ClientReportRecorder.java | 7 +- .../sentry/util/EventSizeLimitingUtils.java | 198 ++++++++ .../sentry/util/JsonSerializationUtils.java | 86 ++++ .../io/sentry/EventSizeLimitingUtilsTest.kt | 460 ++++++++++++++++++ .../test/java/io/sentry/SentryClientTest.kt | 10 +- .../sentry/clientreport/ClientReportTest.kt | 6 +- .../sentry/util/JsonSerializationUtilsTest.kt | 63 +++ 11 files changed, 918 insertions(+), 4 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java create mode 100644 sentry/src/test/java/io/sentry/EventSizeLimitingUtilsTest.kt diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 2211f053912..a70967d7d84 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -347,6 +347,7 @@ public final class io/sentry/DataCategory : java/lang/Enum { public static final field Default Lio/sentry/DataCategory; public static final field Error Lio/sentry/DataCategory; public static final field Feedback Lio/sentry/DataCategory; + public static final field LogByte Lio/sentry/DataCategory; public static final field LogItem Lio/sentry/DataCategory; public static final field Monitor Lio/sentry/DataCategory; public static final field Profile Lio/sentry/DataCategory; @@ -3384,6 +3385,7 @@ public class io/sentry/SentryOptions { public fun getMaxTraceFileSize ()J public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader; public fun getOnDiscard ()Lio/sentry/SentryOptions$OnDiscardCallback; + public fun getOnOversizedError ()Lio/sentry/SentryOptions$OnOversizedErrorCallback; public fun getOpenTelemetryMode ()Lio/sentry/SentryOpenTelemetryMode; public fun getOptionsObservers ()Ljava/util/List; public fun getOutboxPath ()Ljava/lang/String; @@ -3434,6 +3436,7 @@ public class io/sentry/SentryOptions { public fun isEnableAutoSessionTracking ()Z public fun isEnableBackpressureHandling ()Z public fun isEnableDeduplication ()Z + public fun isEnableEventSizeLimiting ()Z public fun isEnableExternalConfiguration ()Z public fun isEnablePrettySerializationOutput ()Z public fun isEnableScopePersistence ()Z @@ -3490,6 +3493,7 @@ public class io/sentry/SentryOptions { public fun setEnableAutoSessionTracking (Z)V public fun setEnableBackpressureHandling (Z)V public fun setEnableDeduplication (Z)V + public fun setEnableEventSizeLimiting (Z)V public fun setEnableExternalConfiguration (Z)V public fun setEnablePrettySerializationOutput (Z)V public fun setEnableScopePersistence (Z)V @@ -3531,6 +3535,7 @@ public class io/sentry/SentryOptions { public fun setMaxTraceFileSize (J)V public fun setModulesLoader (Lio/sentry/internal/modules/IModulesLoader;)V public fun setOnDiscard (Lio/sentry/SentryOptions$OnDiscardCallback;)V + public fun setOnOversizedError (Lio/sentry/SentryOptions$OnOversizedErrorCallback;)V public fun setOpenTelemetryMode (Lio/sentry/SentryOpenTelemetryMode;)V public fun setPrintUncaughtStackTrace (Z)V public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V @@ -3640,6 +3645,10 @@ public abstract interface class io/sentry/SentryOptions$OnDiscardCallback { public abstract fun execute (Lio/sentry/clientreport/DiscardReason;Lio/sentry/DataCategory;Ljava/lang/Long;)V } +public abstract interface class io/sentry/SentryOptions$OnOversizedErrorCallback { + public abstract fun execute (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; +} + public abstract interface class io/sentry/SentryOptions$ProfilesSamplerCallback { public abstract fun sample (Lio/sentry/SamplingContext;)Ljava/lang/Double; } @@ -6996,6 +7005,10 @@ public final class io/sentry/util/EventProcessorUtils { public static fun unwrap (Ljava/util/List;)Ljava/util/List; } +public final class io/sentry/util/EventSizeLimitingUtils { + public static fun limitEventSize (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/SentryOptions;)Lio/sentry/SentryEvent; +} + public final class io/sentry/util/ExceptionUtils { public fun ()V public static fun findRootCause (Ljava/lang/Throwable;)Ljava/lang/Throwable; @@ -7062,6 +7075,7 @@ public final class io/sentry/util/IntegrationUtils { public final class io/sentry/util/JsonSerializationUtils { public fun ()V public static fun atomicIntegerArrayToList (Ljava/util/concurrent/atomic/AtomicIntegerArray;)Ljava/util/List; + public static fun byteSizeOf (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/JsonSerializable;)J public static fun bytesFrom (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/JsonSerializable;)[B public static fun calendarToMap (Ljava/util/Calendar;)Ljava/util/Map; } diff --git a/sentry/src/main/java/io/sentry/DataCategory.java b/sentry/src/main/java/io/sentry/DataCategory.java index 226deef9a69..ca60f8bc5d4 100644 --- a/sentry/src/main/java/io/sentry/DataCategory.java +++ b/sentry/src/main/java/io/sentry/DataCategory.java @@ -10,6 +10,7 @@ public enum DataCategory { Session("session"), Attachment("attachment"), LogItem("log_item"), + LogByte("log_byte"), Monitor("monitor"), Profile("profile"), ProfileChunkUi("profile_chunk_ui"), diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index bfcf4e780be..58e0c6b698e 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -163,6 +163,10 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul } } + if (event != null) { + event = EventSizeLimitingUtils.limitEventSize(event, hint, options); + } + if (event == null) { return SentryId.EMPTY_ID; } @@ -1183,6 +1187,7 @@ public void captureLog(@Nullable SentryLogEvent logEvent, @Nullable IScope scope } if (logEvent != null) { + final @NotNull SentryLogEvent tmpLogEvent = logEvent; logEvent = executeBeforeSendLog(logEvent); if (logEvent == null) { @@ -1190,6 +1195,13 @@ public void captureLog(@Nullable SentryLogEvent logEvent, @Nullable IScope scope options .getClientReportRecorder() .recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.LogItem); + final @NotNull long logEventNumberOfBytes = + JsonSerializationUtils.byteSizeOf( + options.getSerializer(), options.getLogger(), tmpLogEvent); + options + .getClientReportRecorder() + .recordLostEvent( + DiscardReason.BEFORE_SEND, DataCategory.LogByte, logEventNumberOfBytes); return; } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 3a81b268b25..ec081f9eb98 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -346,6 +346,18 @@ public class SentryOptions { */ private boolean enableDeduplication = true; + /** + * Enables event size limiting with {@link EventSizeLimitingEventProcessor}. When enabled, events + * exceeding 1MB will have breadcrumbs and stack frames reduced to stay under the limit. + */ + private boolean enableEventSizeLimiting = false; + + /** + * Callback invoked when an oversized event is detected. This allows custom handling of oversized + * events before the automatic reduction steps are applied. + */ + private @Nullable OnOversizedErrorCallback onOversizedError; + /** Maximum number of spans that can be atteched to single transaction. */ private int maxSpans = 1000; @@ -1716,6 +1728,44 @@ public void setEnableDeduplication(final boolean enableDeduplication) { this.enableDeduplication = enableDeduplication; } + /** + * Returns if event size limiting is enabled. + * + * @return true if event size limiting is enabled, false otherwise + */ + public boolean isEnableEventSizeLimiting() { + return enableEventSizeLimiting; + } + + /** + * Enables or disables event size limiting. When enabled, events exceeding 1MB will have + * breadcrumbs and stack frames reduced to stay under the limit. + * + * @param enableEventSizeLimiting true to enable, false to disable + */ + public void setEnableEventSizeLimiting(final boolean enableEventSizeLimiting) { + this.enableEventSizeLimiting = enableEventSizeLimiting; + } + + /** + * Returns the onOversizedError callback. + * + * @return the onOversizedError callback or null if not set + */ + public @Nullable OnOversizedErrorCallback getOnOversizedError() { + return onOversizedError; + } + + /** + * Sets the onOversizedError callback. This callback is invoked when an oversized event is + * detected, before the automatic reduction steps are applied. + * + * @param onOversizedError the onOversizedError callback + */ + public void setOnOversizedError(@Nullable OnOversizedErrorCallback onOversizedError) { + this.onOversizedError = onOversizedError; + } + /** * Returns if tracing should be enabled. If tracing is disabled, starting transactions returns * {@link NoOpTransaction}. @@ -3100,6 +3150,21 @@ public interface BeforeBreadcrumbCallback { Breadcrumb execute(@NotNull Breadcrumb breadcrumb, @NotNull Hint hint); } + /** The OnOversizedError callback */ + public interface OnOversizedErrorCallback { + + /** + * Called when an oversized event is detected. This callback allows custom handling of oversized + * events before automatic reduction steps are applied. + * + * @param event the oversized event + * @param hint the hints + * @return the modified event (should ideally be reduced in size) + */ + @NotNull + SentryEvent execute(@NotNull SentryEvent event, @NotNull Hint hint); + } + /** The OnDiscard callback */ public interface OnDiscardCallback { diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index 7b415ce48c9..7c872a0cd19 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -6,6 +6,7 @@ import io.sentry.SentryEnvelopeItem; import io.sentry.SentryItemType; import io.sentry.SentryLevel; +import io.sentry.SentryLogEvent; import io.sentry.SentryLogEvents; import io.sentry.SentryOptions; import io.sentry.protocol.SentrySpan; @@ -104,8 +105,12 @@ public void recordLostEnvelopeItem( } else if (itemCategory.equals(DataCategory.LogItem)) { final @Nullable SentryLogEvents logs = envelopeItem.getLogs(options.getSerializer()); if (logs != null) { - final long count = logs.getItems().size(); + final @NotNull List items = logs.getItems(); + final long count = items.size(); recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), count); + final long logBytes = envelopeItem.getData().length; + recordLostEventInternal( + reason.getReason(), DataCategory.LogByte.getCategory(), logBytes); executeOnDiscard(reason, itemCategory, count); } } else { diff --git a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java new file mode 100644 index 00000000000..83187432c76 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java @@ -0,0 +1,198 @@ +package io.sentry.util; + +import io.sentry.Breadcrumb; +import io.sentry.Hint; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; +import io.sentry.protocol.SentryException; +import io.sentry.protocol.SentryStackFrame; +import io.sentry.protocol.SentryStackTrace; +import io.sentry.protocol.SentryThread; +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Utility class that limits event size to 1MB by incrementally dropping fields when the event + * exceeds the limit. This runs after beforeSend and right before sending the event. + * + *

Fields are reduced in order of least importance: + * + *

    + *
  1. All breadcrumbs + *
  2. Exception stack frames (keep 250 frames from start and 250 frames from end, removing + * middle) + *
+ * + *

Note: Extras, tags, threads, request data, debug meta, and contexts are preserved. + */ +@ApiStatus.Internal +public final class EventSizeLimitingUtils { + + private static final long MAX_EVENT_SIZE_BYTES = 1024 * 1024; // 1MB + private static final int FRAMES_PER_SIDE = 250; // Keep 250 frames from start and 250 from end + + private EventSizeLimitingUtils() {} + + /** + * Limits the size of an event by incrementally dropping fields when it exceeds the limit. + * + * @param event the event to limit + * @param hint the hint + * @param options the SentryOptions + * @return the potentially reduced event + */ + public static @Nullable SentryEvent limitEventSize( + final @NotNull SentryEvent event, + final @NotNull Hint hint, + final @NotNull SentryOptions options) { + if (!options.isEnableEventSizeLimiting()) { + return event; + } + + if (!isTooLarge(event, options)) { + return event; + } + + long eventSize = byteSizeOf(event, options); + options + .getLogger() + .log( + SentryLevel.INFO, + "Event size (%d bytes) exceeds %d bytes limit. Reducing size by dropping fields.", + eventSize, + MAX_EVENT_SIZE_BYTES); + + SentryEvent reducedEvent = event; + + // Step 0: Invoke custom callback if defined + final SentryOptions.OnOversizedErrorCallback onOversizedError = options.getOnOversizedError(); + if (onOversizedError != null) { + try { + reducedEvent = onOversizedError.execute(reducedEvent, hint); + if (!isTooLarge(reducedEvent, options)) { + return reducedEvent; + } + } catch (Exception e) { + options + .getLogger() + .log( + SentryLevel.ERROR, + "The onOversizedError callback threw an exception. It will be ignored and automatic reduction will continue.", + e); + // Continue with automatic reduction if callback fails + reducedEvent = event; + } + } + + // Step 1: Remove all breadcrumbs + reducedEvent = removeAllBreadcrumbs(reducedEvent, options); + if (!isTooLarge(reducedEvent, options)) { + return reducedEvent; + } + + // Step 2: Truncate stack frames (keep 250 from start and 250 from end) + reducedEvent = truncateStackFrames(reducedEvent, options); + if (isTooLarge(reducedEvent, options)) { + long finalEventSize = byteSizeOf(reducedEvent, options); + options + .getLogger() + .log( + SentryLevel.WARNING, + "Event size (%d bytes) still exceeds limit after reducing all fields. Event may be rejected by server.", + finalEventSize); + } + + return reducedEvent; + } + + /** + * Checks if the event exceeds the size limit. + * + * @param event the event to check + * @param options the SentryOptions + * @return true if the event exceeds the size limit + */ + private static boolean isTooLarge( + final @NotNull SentryEvent event, final @NotNull SentryOptions options) { + return byteSizeOf(event, options) > MAX_EVENT_SIZE_BYTES; + } + + /** Calculates the size of the event when serialized to JSON without actually storing the data. */ + private static long byteSizeOf( + final @NotNull SentryEvent event, final @NotNull SentryOptions options) { + return JsonSerializationUtils.byteSizeOf(options.getSerializer(), options.getLogger(), event); + } + + private static @NotNull SentryEvent removeAllBreadcrumbs( + final @NotNull SentryEvent event, final @NotNull SentryOptions options) { + final List breadcrumbs = event.getBreadcrumbs(); + if (breadcrumbs != null && !breadcrumbs.isEmpty()) { + event.setBreadcrumbs(null); + options + .getLogger() + .log( + SentryLevel.DEBUG, "Removed %d breadcrumbs to reduce event size", breadcrumbs.size()); + } + return event; + } + + private static @NotNull SentryEvent truncateStackFrames( + final @NotNull SentryEvent event, final @NotNull SentryOptions options) { + final List exceptions = event.getExceptions(); + if (exceptions != null) { + for (final SentryException exception : exceptions) { + final SentryStackTrace stacktrace = exception.getStacktrace(); + if (stacktrace != null) { + final List frames = stacktrace.getFrames(); + if (frames != null && frames.size() > FRAMES_PER_SIDE * 2) { + // Keep first 250 frames and last 250 frames, removing middle + final List truncatedFrames = new ArrayList<>(); + truncatedFrames.addAll(frames.subList(0, FRAMES_PER_SIDE)); + truncatedFrames.addAll(frames.subList(frames.size() - FRAMES_PER_SIDE, frames.size())); + stacktrace.setFrames(truncatedFrames); + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Truncated stack frames from %d to %d (removed middle) for exception %s", + frames.size(), + truncatedFrames.size(), + exception.getType()); + } + } + } + } + + // Also truncate thread stack traces + final List threads = event.getThreads(); + if (threads != null) { + for (final SentryThread thread : threads) { + final SentryStackTrace stacktrace = thread.getStacktrace(); + if (stacktrace != null) { + final List frames = stacktrace.getFrames(); + if (frames != null && frames.size() > FRAMES_PER_SIDE * 2) { + // Keep first 250 frames and last 250 frames, removing middle + final List truncatedFrames = new ArrayList<>(); + truncatedFrames.addAll(frames.subList(0, FRAMES_PER_SIDE)); + truncatedFrames.addAll(frames.subList(frames.size() - FRAMES_PER_SIDE, frames.size())); + stacktrace.setFrames(truncatedFrames); + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Truncated stack frames from %d to %d (removed middle) for thread %d", + frames.size(), + truncatedFrames.size(), + thread.getId()); + } + } + } + } + + return event; + } +} diff --git a/sentry/src/main/java/io/sentry/util/JsonSerializationUtils.java b/sentry/src/main/java/io/sentry/util/JsonSerializationUtils.java index 83b4a03e8e2..694ff32f45d 100644 --- a/sentry/src/main/java/io/sentry/util/JsonSerializationUtils.java +++ b/sentry/src/main/java/io/sentry/util/JsonSerializationUtils.java @@ -65,4 +65,90 @@ public final class JsonSerializationUtils { return null; } } + + /** + * Calculates the size in bytes of a serializable object when serialized to JSON without actually + * storing the serialized data. This is more memory efficient than {@link #bytesFrom(ISerializer, + * ILogger, JsonSerializable)} when you only need the size. + * + * @param serializer the serializer + * @param logger the logger + * @param serializable the serializable object + * @return the size in bytes, or -1 if serialization fails + */ + public static long byteSizeOf( + final @NotNull ISerializer serializer, + final @NotNull ILogger logger, + final @Nullable JsonSerializable serializable) { + if (serializable == null) { + return 0; + } + try { + final ByteCountingWriter writer = new ByteCountingWriter(); + serializer.serialize(serializable, writer); + return writer.getByteCount(); + } catch (Throwable t) { + logger.log(SentryLevel.ERROR, "Could not calculate size of serializable", t); + return 0; + } + } + + /** + * A Writer that counts the number of bytes that would be written in UTF-8 encoding without + * actually storing the data. + */ + private static final class ByteCountingWriter extends Writer { + private long byteCount = 0L; + + @Override + public void write(final char[] cbuf, final int off, final int len) { + for (int i = off; i < off + len; i++) { + byteCount += utf8ByteCount(cbuf[i]); + } + } + + @Override + public void write(final int c) { + byteCount += utf8ByteCount((char) c); + } + + @Override + public void write(final @NotNull String str, final int off, final int len) { + for (int i = off; i < off + len; i++) { + byteCount += utf8ByteCount(str.charAt(i)); + } + } + + @Override + public void flush() { + // Nothing to flush since we don't store data + } + + @Override + public void close() { + // Nothing to close + } + + public long getByteCount() { + return byteCount; + } + + /** + * Calculates the number of bytes needed to encode a character in UTF-8. + * + * @param c the character + * @return the number of bytes (1-4) + */ + private static int utf8ByteCount(final char c) { + if (c <= 0x7F) { + return 1; // ASCII + } else if (c <= 0x7FF) { + return 2; // 2-byte character + } else if (Character.isSurrogate(c)) { + return 2; // Surrogate pair, counted as 2 bytes each (total 4 for the pair) + } else { + return 3; // 3-byte character + } + } + } } diff --git a/sentry/src/test/java/io/sentry/EventSizeLimitingUtilsTest.kt b/sentry/src/test/java/io/sentry/EventSizeLimitingUtilsTest.kt new file mode 100644 index 00000000000..f01c099ab69 --- /dev/null +++ b/sentry/src/test/java/io/sentry/EventSizeLimitingUtilsTest.kt @@ -0,0 +1,460 @@ +package io.sentry + +import io.sentry.protocol.Message +import io.sentry.protocol.SentryException +import io.sentry.protocol.SentryStackFrame +import io.sentry.protocol.SentryStackTrace +import io.sentry.util.EventSizeLimitingUtils +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class EventSizeLimitingUtilsTest { + class Fixture { + fun getOptions(): SentryOptions { + val options = SentryOptions() + options.isEnableEventSizeLimiting = true + return options + } + } + + var fixture = Fixture() + + @Test + fun `does not modify event if size is below limit`() { + val options = fixture.getOptions() + val event = SentryEvent() + val message = Message() + message.message = "test message" + event.setMessage(message) + event.setExtra("key", "value") + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertNotNull(result) + assertEquals(event.getMessage(), result.getMessage()) + assertEquals(event.getExtras(), result.getExtras()) + } + + @Test + fun `removes all breadcrumbs when event exceeds size limit`() { + val options = fixture.getOptions() + val event = createLargeEvent() + + // Add many breadcrumbs with large data to exceed 1MB limit + for (i in 0..100) { + val breadcrumb = Breadcrumb() + breadcrumb.message = "breadcrumb $i" + breadcrumb.setData("large_data", "x".repeat(15 * 1024)) // 15KB per breadcrumb + event.addBreadcrumb(breadcrumb) + } + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertNotNull(result) + // All breadcrumbs should be removed + assertNull(result.getBreadcrumbs()) + } + + @Test + fun `truncates stack frames when event exceeds size limit`() { + val options = fixture.getOptions() + val event = createLargeEvent() + + // Add exception with large stack trace + val exception = SentryException() + exception.setType("RuntimeException") + exception.setValue("Test exception") + val stacktrace = SentryStackTrace() + val frames = mutableListOf() + for (i in 0..200) { + val frame = SentryStackFrame() + frame.setModule("com.example.Class$i") + frame.setFunction("method$i") + frame.setFilename("File$i.java") + frame.setLineno(i) + frames.add(frame) + } + stacktrace.setFrames(frames) + exception.setStacktrace(stacktrace) + event.setExceptions(listOf(exception)) + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertNotNull(result) + val resultExceptions = result.getExceptions() + assertNotNull(resultExceptions) + assertTrue(resultExceptions!!.isNotEmpty()) + val resultStacktrace = resultExceptions[0].getStacktrace() + assertNotNull(resultStacktrace) + val resultFrames = resultStacktrace.getFrames() + assertNotNull(resultFrames) + // Should be truncated to 500 frames (250 from start + 250 from end) when over 500 + // For 200 frames, no truncation should occur since it's less than 500 + assertTrue(resultFrames!!.size <= 500) + } + + @Test + fun `truncates stack frames when event has more than 500 frames`() { + val options = fixture.getOptions() + val event = createLargeEvent() + + // Add exception with very large stack trace (> 500 frames) + val exception = SentryException() + exception.setType("RuntimeException") + exception.setValue("Test exception") + val stacktrace = SentryStackTrace() + val frames = mutableListOf() + // Create 601 frames (0..600) with large data to ensure event exceeds size limit + for (i in 0..600) { + val frame = SentryStackFrame() + frame.setModule("com.example.Class$i") + frame.setFunction("method$i" + "x".repeat(1024)) // Large function name + frame.setFilename("File$i.java") + frame.setLineno(i) + frame.setContextLine("x".repeat(2048)) // Large context line + frames.add(frame) + } + stacktrace.setFrames(frames) + exception.setStacktrace(stacktrace) + event.setExceptions(listOf(exception)) + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertNotNull(result) + val resultExceptions = result.getExceptions() + assertNotNull(resultExceptions) + assertTrue(resultExceptions!!.isNotEmpty()) + val resultStacktrace = resultExceptions[0].getStacktrace() + assertNotNull(resultStacktrace) + val resultFrames = resultStacktrace.getFrames() + assertNotNull(resultFrames) + // Should be truncated to 500 frames (250 from start + 250 from end) + assertEquals(500, resultFrames!!.size) + } + + @Test + fun `invokes onOversizedError callback when event exceeds size limit`() { + val options = fixture.getOptions() + var callbackInvoked = false + var receivedEvent: SentryEvent? = null + var receivedHint: Hint? = null + options.setOnOversizedError { event, hint -> + callbackInvoked = true + receivedEvent = event + receivedHint = hint + event + } + val event = createLargeEvent() + + // Add breadcrumbs to make it large + for (i in 0..100) { + val breadcrumb = Breadcrumb() + breadcrumb.message = "breadcrumb $i" + breadcrumb.setData("large_data", "x".repeat(15 * 1024)) + event.addBreadcrumb(breadcrumb) + } + + val hint = Hint() + val result = EventSizeLimitingUtils.limitEventSize(event, hint, options) + + assertTrue(callbackInvoked) + assertNotNull(receivedEvent) + assertEquals(event, receivedEvent) + assertEquals(hint, receivedHint) + assertNotNull(result) + } + + @Test + fun `onOversizedError callback successfully reduces size below limit`() { + val options = fixture.getOptions() + options.setOnOversizedError { event, _ -> + // Remove all breadcrumbs to reduce size + event.setBreadcrumbs(null) + event + } + val event = createLargeEvent() + + // Add breadcrumbs to make it large + for (i in 0..100) { + val breadcrumb = Breadcrumb() + breadcrumb.message = "breadcrumb $i" + breadcrumb.setData("large_data", "x".repeat(15 * 1024)) + event.addBreadcrumb(breadcrumb) + } + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertNotNull(result) + // Breadcrumbs should be removed by callback + assertNull(result.getBreadcrumbs()) + // No further reduction should be needed + } + + @Test + fun `onOversizedError callback insufficient reduction continues with automatic steps`() { + val options = fixture.getOptions() + var callbackInvoked = false + options.setOnOversizedError { event, _ -> + callbackInvoked = true + // Remove only some breadcrumbs, not enough to reduce size below limit + val breadcrumbs = event.getBreadcrumbs() + if (breadcrumbs != null && breadcrumbs.size > 20) { + // Keep only 20 breadcrumbs, but each is 15KB, so total is still ~300KB + // Add more data to ensure it's still oversized + val keptBreadcrumbs = breadcrumbs.subList(breadcrumbs.size - 20, breadcrumbs.size) + event.setBreadcrumbs(keptBreadcrumbs) + // Add extra data to ensure event is still oversized + event.setExtra("still_large", "x".repeat(800 * 1024)) // 800KB extra + } + event + } + val event = createLargeEvent() + + // Add many breadcrumbs with large data to exceed 1MB limit + for (i in 0..100) { + val breadcrumb = Breadcrumb() + breadcrumb.message = "breadcrumb $i" + breadcrumb.setData("large_data", "x".repeat(15 * 1024)) + event.addBreadcrumb(breadcrumb) + } + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertTrue(callbackInvoked) + assertNotNull(result) + // Automatic reduction should remove all remaining breadcrumbs + assertNull(result.getBreadcrumbs()) + } + + @Test + fun `onOversizedError callback exception continues with automatic reduction`() { + val options = fixture.getOptions() + options.setOnOversizedError { _, _ -> throw RuntimeException("Callback error") } + val event = createLargeEvent() + + // Add breadcrumbs to make it large + for (i in 0..100) { + val breadcrumb = Breadcrumb() + breadcrumb.message = "breadcrumb $i" + breadcrumb.setData("large_data", "x".repeat(15 * 1024)) + event.addBreadcrumb(breadcrumb) + } + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertNotNull(result) + // Automatic reduction should still work despite callback exception + assertNull(result.getBreadcrumbs()) + } + + @Test + fun `onOversizedError callback not invoked when event is below size limit`() { + val options = fixture.getOptions() + var callbackInvoked = false + options.setOnOversizedError { _, _ -> + callbackInvoked = true + SentryEvent() + } + val event = SentryEvent() + val message = Message() + message.message = "test message" + event.setMessage(message) + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertFalse(callbackInvoked) + assertNotNull(result) + } + + @Test + fun `onOversizedError callback not invoked when event size limiting is disabled`() { + val options = SentryOptions() + options.isEnableEventSizeLimiting = false + var callbackInvoked = false + options.setOnOversizedError { _, _ -> + callbackInvoked = true + SentryEvent() + } + val event = createLargeEvent() + + // Add breadcrumbs to make it large + for (i in 0..100) { + val breadcrumb = Breadcrumb() + breadcrumb.message = "breadcrumb $i" + breadcrumb.setData("large_data", "x".repeat(15 * 1024)) + event.addBreadcrumb(breadcrumb) + } + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertFalse(callbackInvoked) + assertNotNull(result) + // Event should be unchanged when limiting is disabled + assertNotNull(result.getBreadcrumbs()) + } + + @Test + fun `onOversizedError callback can replace event with a different event`() { + val options = fixture.getOptions() + val replacementEvent = SentryEvent() + val replacementMessage = Message() + replacementMessage.message = "Replacement event" + replacementEvent.setMessage(replacementMessage) + var callbackInvoked = false + options.setOnOversizedError { _, _ -> + callbackInvoked = true + replacementEvent // Return a completely different event + } + val event = createLargeEvent() + + // Add breadcrumbs to make it large + for (i in 0..100) { + val breadcrumb = Breadcrumb() + breadcrumb.message = "breadcrumb $i" + breadcrumb.setData("large_data", "x".repeat(15 * 1024)) + event.addBreadcrumb(breadcrumb) + } + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertTrue(callbackInvoked) + assertNotNull(result) + assertEquals("Replacement event", result!!.getMessage()?.message) + } + + @Test + fun `onOversizedError callback returning same event unchanged continues with automatic reduction`() { + val options = fixture.getOptions() + var callbackInvoked = false + options.setOnOversizedError { event, _ -> + callbackInvoked = true + event // Return the same event without modifications + } + val event = createLargeEvent() + + // Add breadcrumbs to make it large + for (i in 0..100) { + val breadcrumb = Breadcrumb() + breadcrumb.message = "breadcrumb $i" + breadcrumb.setData("large_data", "x".repeat(15 * 1024)) + event.addBreadcrumb(breadcrumb) + } + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertTrue(callbackInvoked) + assertNotNull(result) + // Automatic reduction should have removed breadcrumbs + assertNull(result.getBreadcrumbs()) + } + + @Test + fun `onOversizedError callback receives correct hint object`() { + val options = fixture.getOptions() + var receivedHint: Hint? = null + options.setOnOversizedError { event, hint -> + receivedHint = hint + event + } + val event = createLargeEvent() + + // Add breadcrumbs to make it large + for (i in 0..100) { + val breadcrumb = Breadcrumb() + breadcrumb.message = "breadcrumb $i" + breadcrumb.setData("large_data", "x".repeat(15 * 1024)) + event.addBreadcrumb(breadcrumb) + } + + val hint = Hint() + hint.set("custom_key", "custom_value") + val result = EventSizeLimitingUtils.limitEventSize(event, hint, options) + + assertNotNull(result) + assertNotNull(receivedHint) + assertEquals("custom_value", receivedHint!!.get("custom_key")) + } + + @Test + fun `onOversizedError callback can modify extras to reduce size`() { + val options = fixture.getOptions() + options.setOnOversizedError { event, _ -> + // Remove extras to reduce size + event.setExtras(null) + event + } + val event = createLargeEvent() + + // Add large extras + for (i in 0..100) { + event.setExtra("large_extra_$i", "x".repeat(15 * 1024)) + } + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertNotNull(result) + // Extras should be removed by callback + assertNull(result.getExtras()) + } + + @Test + fun `onOversizedError callback can modify contexts to reduce size`() { + val options = fixture.getOptions() + options.setOnOversizedError { event, _ -> + // Remove contexts to reduce size + event.contexts.keys().toList().forEach { event.contexts.remove(it) } + event + } + val event = createLargeEvent() + + // Add large contexts + for (i in 0..100) { + val context = mutableMapOf() + context["data"] = "x".repeat(15 * 1024) + event.contexts.set("context_$i", context) + } + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertNotNull(result) + // Contexts should be removed by callback + assertEquals(0, result.contexts.size) + } + + @Test + fun `onOversizedError callback multiple invocations not expected`() { + val options = fixture.getOptions() + var callbackInvocationCount = 0 + options.setOnOversizedError { event, _ -> + callbackInvocationCount++ + event + } + val event = createLargeEvent() + + // Add breadcrumbs to make it large + for (i in 0..100) { + val breadcrumb = Breadcrumb() + breadcrumb.message = "breadcrumb $i" + breadcrumb.setData("large_data", "x".repeat(15 * 1024)) + event.addBreadcrumb(breadcrumb) + } + + val result = EventSizeLimitingUtils.limitEventSize(event, Hint(), options) + + assertNotNull(result) + // Callback should be invoked exactly once + assertEquals(1, callbackInvocationCount) + } + + private fun createLargeEvent(): SentryEvent { + val event = SentryEvent() + val message = Message() + message.message = "Large event for testing" + event.setMessage(message) + return event + } +} diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 22f689abac0..6d6165e0e85 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -293,7 +293,10 @@ class SentryClientTest { assertClientReport( fixture.sentryOptions.clientReportRecorder, - listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1)), + listOf( + DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1), + DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogByte.category, 109), + ), ) } @@ -312,7 +315,10 @@ class SentryClientTest { assertClientReport( fixture.sentryOptions.clientReportRecorder, - listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1)), + listOf( + DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogItem.category, 1), + DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.LogByte.category, 109), + ), ) } diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt index 30cfece6267..ae4a5f35362 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -374,8 +374,12 @@ class ClientReportTest { 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 } + val logItem = + clientReport!!.discardedEvents!!.first { it.category == DataCategory.LogItem.category } assertEquals(2, logItem.quantity) + val logByte = + clientReport!!.discardedEvents!!.first { it.category == DataCategory.LogByte.category } + assertEquals(226, logByte.quantity) } private fun givenClientReportRecorder( diff --git a/sentry/src/test/java/io/sentry/util/JsonSerializationUtilsTest.kt b/sentry/src/test/java/io/sentry/util/JsonSerializationUtilsTest.kt index 835182ed105..caa93f1af7a 100644 --- a/sentry/src/test/java/io/sentry/util/JsonSerializationUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/JsonSerializationUtilsTest.kt @@ -3,6 +3,11 @@ package io.sentry.util import io.sentry.ILogger import io.sentry.JsonSerializable import io.sentry.JsonSerializer +import io.sentry.ObjectWriter +import io.sentry.SentryLogEvent +import io.sentry.SentryLogLevel +import io.sentry.SentryOptions +import io.sentry.protocol.SentryId import java.io.Writer import java.util.Calendar import java.util.concurrent.atomic.AtomicIntegerArray @@ -10,11 +15,16 @@ import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertNull +import kotlin.test.assertTrue import org.mockito.invocation.InvocationOnMock import org.mockito.kotlin.any import org.mockito.kotlin.mock class JsonSerializationUtilsTest { + + private val serializer = JsonSerializer(SentryOptions()) + private val logger: ILogger = mock() + @Test fun `serializes calendar to map`() { val calendar = Calendar.getInstance() @@ -74,4 +84,57 @@ class JsonSerializationUtilsTest { assertNull(actualBytes, "Mocker error should be captured and null returned.") } + + @Test + fun `byteSizeOf returns same size as bytesFrom for ASCII`() { + val logEvent = SentryLogEvent(SentryId(), 1234567890.0, "Hello ASCII", SentryLogLevel.INFO) + + val actualBytes = JsonSerializationUtils.bytesFrom(serializer, logger, logEvent) + val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, logEvent) + + assertEquals( + (actualBytes?.size ?: -1).toLong(), + byteSize, + "byteSizeOf should match actual byte array length", + ) + assertTrue(byteSize > 0, "byteSize should be positive") + } + + @Test + fun `byteSizeOf returns same size as bytesFrom for UTF-8 characters`() { + // Mix of 1-byte, 2-byte, 3-byte and 4-byte UTF-8 characters + val logEvent = + SentryLogEvent(SentryId(), 1234567890.0, "Hello 世界 café 🎉 🚀", SentryLogLevel.WARN) + + val actualBytes = JsonSerializationUtils.bytesFrom(serializer, logger, logEvent) + val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, logEvent) + + assertEquals( + (actualBytes?.size ?: -1).toLong(), + byteSize, + "byteSizeOf should match actual byte array length for UTF-8", + ) + assertTrue(byteSize > 0, "byteSize should be positive") + } + + @Test + fun `byteSizeOf returns 0 on serialization error`() { + val serializable = + object : JsonSerializable { + override fun serialize(writer: ObjectWriter, logger: ILogger) { + throw RuntimeException("Serialization error") + } + } + + val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, serializable) + + assertEquals(0, byteSize, "byteSizeOf should return 0 on error") + } + + @Test + fun `byteSizeOf returns 0 on null serializable`() { + val byteSize = JsonSerializationUtils.byteSizeOf(serializer, logger, null) + + assertEquals(0, byteSize, "byteSizeOf should return 0 on null serializable") + } } From ee63d11be8221ab2d6a5c1421c61b6b6e43ae346 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 14 Nov 2025 10:30:15 +0100 Subject: [PATCH 04/12] cleanup --- .../sentry/util/EventSizeLimitingUtils.java | 96 +++++++------------ 1 file changed, 32 insertions(+), 64 deletions(-) diff --git a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java index 83187432c76..007d70936c3 100644 --- a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java +++ b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java @@ -17,23 +17,13 @@ /** * Utility class that limits event size to 1MB by incrementally dropping fields when the event - * exceeds the limit. This runs after beforeSend and right before sending the event. - * - *

Fields are reduced in order of least importance: - * - *

    - *
  1. All breadcrumbs - *
  2. Exception stack frames (keep 250 frames from start and 250 frames from end, removing - * middle) - *
- * - *

Note: Extras, tags, threads, request data, debug meta, and contexts are preserved. + * exceeds the limit. */ @ApiStatus.Internal public final class EventSizeLimitingUtils { - private static final long MAX_EVENT_SIZE_BYTES = 1024 * 1024; // 1MB - private static final int FRAMES_PER_SIDE = 250; // Keep 250 frames from start and 250 from end + private static final long MAX_EVENT_SIZE_BYTES = 1024 * 1024; + private static final int FRAMES_PER_SIDE = 250; private EventSizeLimitingUtils() {} @@ -57,22 +47,20 @@ private EventSizeLimitingUtils() {} return event; } - long eventSize = byteSizeOf(event, options); options .getLogger() .log( SentryLevel.INFO, - "Event size (%d bytes) exceeds %d bytes limit. Reducing size by dropping fields.", - eventSize, + "Event %s exceeds %d bytes limit. Reducing size by dropping fields.", + event.getEventId(), MAX_EVENT_SIZE_BYTES); - SentryEvent reducedEvent = event; + @NotNull SentryEvent reducedEvent = event; - // Step 0: Invoke custom callback if defined - final SentryOptions.OnOversizedErrorCallback onOversizedError = options.getOnOversizedError(); - if (onOversizedError != null) { + final @Nullable SentryOptions.OnOversizedErrorCallback callback = options.getOnOversizedError(); + if (callback != null) { try { - reducedEvent = onOversizedError.execute(reducedEvent, hint); + reducedEvent = callback.execute(reducedEvent, hint); if (!isTooLarge(reducedEvent, options)) { return reducedEvent; } @@ -83,48 +71,33 @@ private EventSizeLimitingUtils() {} SentryLevel.ERROR, "The onOversizedError callback threw an exception. It will be ignored and automatic reduction will continue.", e); - // Continue with automatic reduction if callback fails reducedEvent = event; } } - // Step 1: Remove all breadcrumbs reducedEvent = removeAllBreadcrumbs(reducedEvent, options); if (!isTooLarge(reducedEvent, options)) { return reducedEvent; } - // Step 2: Truncate stack frames (keep 250 from start and 250 from end) reducedEvent = truncateStackFrames(reducedEvent, options); if (isTooLarge(reducedEvent, options)) { - long finalEventSize = byteSizeOf(reducedEvent, options); options .getLogger() .log( SentryLevel.WARNING, - "Event size (%d bytes) still exceeds limit after reducing all fields. Event may be rejected by server.", - finalEventSize); + "Event %s still exceeds size limit after reducing all fields. Event may be rejected by server.", + event.getEventId()); } return reducedEvent; } - /** - * Checks if the event exceeds the size limit. - * - * @param event the event to check - * @param options the SentryOptions - * @return true if the event exceeds the size limit - */ private static boolean isTooLarge( final @NotNull SentryEvent event, final @NotNull SentryOptions options) { - return byteSizeOf(event, options) > MAX_EVENT_SIZE_BYTES; - } - - /** Calculates the size of the event when serialized to JSON without actually storing the data. */ - private static long byteSizeOf( - final @NotNull SentryEvent event, final @NotNull SentryOptions options) { - return JsonSerializationUtils.byteSizeOf(options.getSerializer(), options.getLogger(), event); + final long size = + JsonSerializationUtils.byteSizeOf(options.getSerializer(), options.getLogger(), event); + return size > MAX_EVENT_SIZE_BYTES; } private static @NotNull SentryEvent removeAllBreadcrumbs( @@ -135,22 +108,23 @@ private static long byteSizeOf( options .getLogger() .log( - SentryLevel.DEBUG, "Removed %d breadcrumbs to reduce event size", breadcrumbs.size()); + SentryLevel.DEBUG, + "Removed breadcrumbs to reduce size of event %s", + event.getEventId()); } return event; } private static @NotNull SentryEvent truncateStackFrames( final @NotNull SentryEvent event, final @NotNull SentryOptions options) { - final List exceptions = event.getExceptions(); + final @Nullable List exceptions = event.getExceptions(); if (exceptions != null) { - for (final SentryException exception : exceptions) { - final SentryStackTrace stacktrace = exception.getStacktrace(); + for (final @NotNull SentryException exception : exceptions) { + final @Nullable SentryStackTrace stacktrace = exception.getStacktrace(); if (stacktrace != null) { - final List frames = stacktrace.getFrames(); - if (frames != null && frames.size() > FRAMES_PER_SIDE * 2) { - // Keep first 250 frames and last 250 frames, removing middle - final List truncatedFrames = new ArrayList<>(); + final @Nullable List frames = stacktrace.getFrames(); + if (frames != null && frames.size() > (FRAMES_PER_SIDE * 2)) { + final @NotNull List truncatedFrames = new ArrayList<>(); truncatedFrames.addAll(frames.subList(0, FRAMES_PER_SIDE)); truncatedFrames.addAll(frames.subList(frames.size() - FRAMES_PER_SIDE, frames.size())); stacktrace.setFrames(truncatedFrames); @@ -158,25 +132,21 @@ private static long byteSizeOf( .getLogger() .log( SentryLevel.DEBUG, - "Truncated stack frames from %d to %d (removed middle) for exception %s", - frames.size(), - truncatedFrames.size(), - exception.getType()); + "Truncated exception stack frames of event %s", + event.getEventId()); } } } } - // Also truncate thread stack traces - final List threads = event.getThreads(); + final @Nullable List threads = event.getThreads(); if (threads != null) { for (final SentryThread thread : threads) { - final SentryStackTrace stacktrace = thread.getStacktrace(); + final @Nullable SentryStackTrace stacktrace = thread.getStacktrace(); if (stacktrace != null) { - final List frames = stacktrace.getFrames(); - if (frames != null && frames.size() > FRAMES_PER_SIDE * 2) { - // Keep first 250 frames and last 250 frames, removing middle - final List truncatedFrames = new ArrayList<>(); + final @Nullable List frames = stacktrace.getFrames(); + if (frames != null && frames.size() > (FRAMES_PER_SIDE * 2)) { + final @NotNull List truncatedFrames = new ArrayList<>(); truncatedFrames.addAll(frames.subList(0, FRAMES_PER_SIDE)); truncatedFrames.addAll(frames.subList(frames.size() - FRAMES_PER_SIDE, frames.size())); stacktrace.setFrames(truncatedFrames); @@ -184,10 +154,8 @@ private static long byteSizeOf( .getLogger() .log( SentryLevel.DEBUG, - "Truncated stack frames from %d to %d (removed middle) for thread %d", - frames.size(), - truncatedFrames.size(), - thread.getId()); + "Truncated thread stack frames for event %s", + event.getEventId()); } } } From 486fc42b52c09cd2308889a9a072961eb70ae33c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 14 Nov 2025 10:37:37 +0100 Subject: [PATCH 05/12] invert size check method --- .../java/io/sentry/util/EventSizeLimitingUtils.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java index 007d70936c3..13e895e673b 100644 --- a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java +++ b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java @@ -43,7 +43,7 @@ private EventSizeLimitingUtils() {} return event; } - if (!isTooLarge(event, options)) { + if (isSizeOk(event, options)) { return event; } @@ -61,7 +61,7 @@ private EventSizeLimitingUtils() {} if (callback != null) { try { reducedEvent = callback.execute(reducedEvent, hint); - if (!isTooLarge(reducedEvent, options)) { + if (isSizeOk(reducedEvent, options)) { return reducedEvent; } } catch (Exception e) { @@ -76,12 +76,12 @@ private EventSizeLimitingUtils() {} } reducedEvent = removeAllBreadcrumbs(reducedEvent, options); - if (!isTooLarge(reducedEvent, options)) { + if (isSizeOk(reducedEvent, options)) { return reducedEvent; } reducedEvent = truncateStackFrames(reducedEvent, options); - if (isTooLarge(reducedEvent, options)) { + if (!isSizeOk(reducedEvent, options)) { options .getLogger() .log( @@ -93,11 +93,11 @@ private EventSizeLimitingUtils() {} return reducedEvent; } - private static boolean isTooLarge( + private static boolean isSizeOk( final @NotNull SentryEvent event, final @NotNull SentryOptions options) { final long size = JsonSerializationUtils.byteSizeOf(options.getSerializer(), options.getLogger(), event); - return size > MAX_EVENT_SIZE_BYTES; + return size <= MAX_EVENT_SIZE_BYTES; } private static @NotNull SentryEvent removeAllBreadcrumbs( From 9afb21debe775e94c39151838d067f9238763e67 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 14 Nov 2025 11:11:20 +0100 Subject: [PATCH 06/12] try catch --- .../sentry/util/EventSizeLimitingUtils.java | 89 +++++++++++-------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java index 13e895e673b..7ed9b32b865 100644 --- a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java +++ b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java @@ -39,58 +39,69 @@ private EventSizeLimitingUtils() {} final @NotNull SentryEvent event, final @NotNull Hint hint, final @NotNull SentryOptions options) { - if (!options.isEnableEventSizeLimiting()) { - return event; - } + try { + if (!options.isEnableEventSizeLimiting()) { + return event; + } - if (isSizeOk(event, options)) { - return event; - } + if (isSizeOk(event, options)) { + return event; + } - options - .getLogger() - .log( - SentryLevel.INFO, - "Event %s exceeds %d bytes limit. Reducing size by dropping fields.", - event.getEventId(), - MAX_EVENT_SIZE_BYTES); - - @NotNull SentryEvent reducedEvent = event; - - final @Nullable SentryOptions.OnOversizedErrorCallback callback = options.getOnOversizedError(); - if (callback != null) { - try { - reducedEvent = callback.execute(reducedEvent, hint); - if (isSizeOk(reducedEvent, options)) { - return reducedEvent; + options + .getLogger() + .log( + SentryLevel.INFO, + "Event %s exceeds %d bytes limit. Reducing size by dropping fields.", + event.getEventId(), + MAX_EVENT_SIZE_BYTES); + + @NotNull SentryEvent reducedEvent = event; + + final @Nullable SentryOptions.OnOversizedErrorCallback callback = + options.getOnOversizedError(); + if (callback != null) { + try { + reducedEvent = callback.execute(reducedEvent, hint); + if (isSizeOk(reducedEvent, options)) { + return reducedEvent; + } + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.ERROR, + "The onOversizedError callback threw an exception. It will be ignored and automatic reduction will continue.", + e); + reducedEvent = event; } - } catch (Exception e) { + } + + reducedEvent = removeAllBreadcrumbs(reducedEvent, options); + if (isSizeOk(reducedEvent, options)) { + return reducedEvent; + } + + reducedEvent = truncateStackFrames(reducedEvent, options); + if (!isSizeOk(reducedEvent, options)) { options .getLogger() .log( - SentryLevel.ERROR, - "The onOversizedError callback threw an exception. It will be ignored and automatic reduction will continue.", - e); - reducedEvent = event; + SentryLevel.WARNING, + "Event %s still exceeds size limit after reducing all fields. Event may be rejected by server.", + event.getEventId()); } - } - reducedEvent = removeAllBreadcrumbs(reducedEvent, options); - if (isSizeOk(reducedEvent, options)) { return reducedEvent; - } - - reducedEvent = truncateStackFrames(reducedEvent, options); - if (!isSizeOk(reducedEvent, options)) { + } catch (Throwable e) { options .getLogger() .log( - SentryLevel.WARNING, - "Event %s still exceeds size limit after reducing all fields. Event may be rejected by server.", - event.getEventId()); + SentryLevel.ERROR, + "An error occurred while limiting event size. Event will be sent as-is.", + e); + return event; } - - return reducedEvent; } private static boolean isSizeOk( From e818730aa03ffa5f9cc3e459cc0e2d5042761c5a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 17 Nov 2025 14:49:06 +0100 Subject: [PATCH 07/12] rename callback to onOversizedEvent --- sentry/api/sentry.api | 6 +-- .../main/java/io/sentry/SentryOptions.java | 22 ++++----- .../sentry/util/EventSizeLimitingUtils.java | 6 +-- .../io/sentry/EventSizeLimitingUtilsTest.kt | 48 +++++++++---------- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index ee8ed97fdff..b3f7a5c2752 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3417,7 +3417,7 @@ public class io/sentry/SentryOptions { public fun getMaxTraceFileSize ()J public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader; public fun getOnDiscard ()Lio/sentry/SentryOptions$OnDiscardCallback; - public fun getOnOversizedError ()Lio/sentry/SentryOptions$OnOversizedErrorCallback; + public fun getOnOversizedEvent ()Lio/sentry/SentryOptions$OnOversizedEventCallback; public fun getOpenTelemetryMode ()Lio/sentry/SentryOpenTelemetryMode; public fun getOptionsObservers ()Ljava/util/List; public fun getOutboxPath ()Ljava/lang/String; @@ -3569,7 +3569,7 @@ public class io/sentry/SentryOptions { public fun setMaxTraceFileSize (J)V public fun setModulesLoader (Lio/sentry/internal/modules/IModulesLoader;)V public fun setOnDiscard (Lio/sentry/SentryOptions$OnDiscardCallback;)V - public fun setOnOversizedError (Lio/sentry/SentryOptions$OnOversizedErrorCallback;)V + public fun setOnOversizedEvent (Lio/sentry/SentryOptions$OnOversizedEventCallback;)V public fun setOpenTelemetryMode (Lio/sentry/SentryOpenTelemetryMode;)V public fun setPrintUncaughtStackTrace (Z)V public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V @@ -3680,7 +3680,7 @@ public abstract interface class io/sentry/SentryOptions$OnDiscardCallback { public abstract fun execute (Lio/sentry/clientreport/DiscardReason;Lio/sentry/DataCategory;Ljava/lang/Long;)V } -public abstract interface class io/sentry/SentryOptions$OnOversizedErrorCallback { +public abstract interface class io/sentry/SentryOptions$OnOversizedEventCallback { public abstract fun execute (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index a01def2762c..368d9121959 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -363,7 +363,7 @@ public class SentryOptions { * Callback invoked when an oversized event is detected. This allows custom handling of oversized * events before the automatic reduction steps are applied. */ - private @Nullable OnOversizedErrorCallback onOversizedError; + private @Nullable OnOversizedEventCallback onOversizedEvent; /** Maximum number of spans that can be atteched to single transaction. */ private int maxSpans = 1000; @@ -1784,22 +1784,22 @@ public void setEnableEventSizeLimiting(final boolean enableEventSizeLimiting) { } /** - * Returns the onOversizedError callback. + * Returns the onOversizedEvent callback. * - * @return the onOversizedError callback or null if not set + * @return the onOversizedEvent callback or null if not set */ - public @Nullable OnOversizedErrorCallback getOnOversizedError() { - return onOversizedError; + public @Nullable OnOversizedEventCallback getOnOversizedEvent() { + return onOversizedEvent; } /** - * Sets the onOversizedError callback. This callback is invoked when an oversized event is + * Sets the onOversizedEvent callback. This callback is invoked when an oversized event is * detected, before the automatic reduction steps are applied. * - * @param onOversizedError the onOversizedError callback + * @param onOversizedEvent the onOversizedEvent callback */ - public void setOnOversizedError(@Nullable OnOversizedErrorCallback onOversizedError) { - this.onOversizedError = onOversizedError; + public void setOnOversizedEvent(@Nullable OnOversizedEventCallback onOversizedEvent) { + this.onOversizedEvent = onOversizedEvent; } /** @@ -3186,8 +3186,8 @@ public interface BeforeBreadcrumbCallback { Breadcrumb execute(@NotNull Breadcrumb breadcrumb, @NotNull Hint hint); } - /** The OnOversizedError callback */ - public interface OnOversizedErrorCallback { + /** The OnOversizedEvent callback */ + public interface OnOversizedEventCallback { /** * Called when an oversized event is detected. This callback allows custom handling of oversized diff --git a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java index 7ed9b32b865..f291e119e6b 100644 --- a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java +++ b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java @@ -58,8 +58,8 @@ private EventSizeLimitingUtils() {} @NotNull SentryEvent reducedEvent = event; - final @Nullable SentryOptions.OnOversizedErrorCallback callback = - options.getOnOversizedError(); + final @Nullable SentryOptions.OnOversizedEventCallback callback = + options.getOnOversizedEvent(); if (callback != null) { try { reducedEvent = callback.execute(reducedEvent, hint); @@ -71,7 +71,7 @@ private EventSizeLimitingUtils() {} .getLogger() .log( SentryLevel.ERROR, - "The onOversizedError callback threw an exception. It will be ignored and automatic reduction will continue.", + "The onOversizedEvent callback threw an exception. It will be ignored and automatic reduction will continue.", e); reducedEvent = event; } diff --git a/sentry/src/test/java/io/sentry/EventSizeLimitingUtilsTest.kt b/sentry/src/test/java/io/sentry/EventSizeLimitingUtilsTest.kt index f01c099ab69..d0efd855838 100644 --- a/sentry/src/test/java/io/sentry/EventSizeLimitingUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/EventSizeLimitingUtilsTest.kt @@ -137,12 +137,12 @@ class EventSizeLimitingUtilsTest { } @Test - fun `invokes onOversizedError callback when event exceeds size limit`() { + fun `invokes onOversizedEvent callback when event exceeds size limit`() { val options = fixture.getOptions() var callbackInvoked = false var receivedEvent: SentryEvent? = null var receivedHint: Hint? = null - options.setOnOversizedError { event, hint -> + options.setOnOversizedEvent { event, hint -> callbackInvoked = true receivedEvent = event receivedHint = hint @@ -169,9 +169,9 @@ class EventSizeLimitingUtilsTest { } @Test - fun `onOversizedError callback successfully reduces size below limit`() { + fun `onOversizedEvent callback successfully reduces size below limit`() { val options = fixture.getOptions() - options.setOnOversizedError { event, _ -> + options.setOnOversizedEvent { event, _ -> // Remove all breadcrumbs to reduce size event.setBreadcrumbs(null) event @@ -195,10 +195,10 @@ class EventSizeLimitingUtilsTest { } @Test - fun `onOversizedError callback insufficient reduction continues with automatic steps`() { + fun `onOversizedEvent callback insufficient reduction continues with automatic steps`() { val options = fixture.getOptions() var callbackInvoked = false - options.setOnOversizedError { event, _ -> + options.setOnOversizedEvent { event, _ -> callbackInvoked = true // Remove only some breadcrumbs, not enough to reduce size below limit val breadcrumbs = event.getBreadcrumbs() @@ -231,9 +231,9 @@ class EventSizeLimitingUtilsTest { } @Test - fun `onOversizedError callback exception continues with automatic reduction`() { + fun `onOversizedEvent callback exception continues with automatic reduction`() { val options = fixture.getOptions() - options.setOnOversizedError { _, _ -> throw RuntimeException("Callback error") } + options.setOnOversizedEvent { _, _ -> throw RuntimeException("Callback error") } val event = createLargeEvent() // Add breadcrumbs to make it large @@ -252,10 +252,10 @@ class EventSizeLimitingUtilsTest { } @Test - fun `onOversizedError callback not invoked when event is below size limit`() { + fun `onOversizedEvent callback not invoked when event is below size limit`() { val options = fixture.getOptions() var callbackInvoked = false - options.setOnOversizedError { _, _ -> + options.setOnOversizedEvent { _, _ -> callbackInvoked = true SentryEvent() } @@ -271,11 +271,11 @@ class EventSizeLimitingUtilsTest { } @Test - fun `onOversizedError callback not invoked when event size limiting is disabled`() { + fun `onOversizedEvent callback not invoked when event size limiting is disabled`() { val options = SentryOptions() options.isEnableEventSizeLimiting = false var callbackInvoked = false - options.setOnOversizedError { _, _ -> + options.setOnOversizedEvent { _, _ -> callbackInvoked = true SentryEvent() } @@ -298,14 +298,14 @@ class EventSizeLimitingUtilsTest { } @Test - fun `onOversizedError callback can replace event with a different event`() { + fun `onOversizedEvent callback can replace event with a different event`() { val options = fixture.getOptions() val replacementEvent = SentryEvent() val replacementMessage = Message() replacementMessage.message = "Replacement event" replacementEvent.setMessage(replacementMessage) var callbackInvoked = false - options.setOnOversizedError { _, _ -> + options.setOnOversizedEvent { _, _ -> callbackInvoked = true replacementEvent // Return a completely different event } @@ -327,10 +327,10 @@ class EventSizeLimitingUtilsTest { } @Test - fun `onOversizedError callback returning same event unchanged continues with automatic reduction`() { + fun `onOversizedEvent callback returning same event unchanged continues with automatic reduction`() { val options = fixture.getOptions() var callbackInvoked = false - options.setOnOversizedError { event, _ -> + options.setOnOversizedEvent { event, _ -> callbackInvoked = true event // Return the same event without modifications } @@ -353,10 +353,10 @@ class EventSizeLimitingUtilsTest { } @Test - fun `onOversizedError callback receives correct hint object`() { + fun `onOversizedEvent callback receives correct hint object`() { val options = fixture.getOptions() var receivedHint: Hint? = null - options.setOnOversizedError { event, hint -> + options.setOnOversizedEvent { event, hint -> receivedHint = hint event } @@ -380,9 +380,9 @@ class EventSizeLimitingUtilsTest { } @Test - fun `onOversizedError callback can modify extras to reduce size`() { + fun `onOversizedEvent callback can modify extras to reduce size`() { val options = fixture.getOptions() - options.setOnOversizedError { event, _ -> + options.setOnOversizedEvent { event, _ -> // Remove extras to reduce size event.setExtras(null) event @@ -402,9 +402,9 @@ class EventSizeLimitingUtilsTest { } @Test - fun `onOversizedError callback can modify contexts to reduce size`() { + fun `onOversizedEvent callback can modify contexts to reduce size`() { val options = fixture.getOptions() - options.setOnOversizedError { event, _ -> + options.setOnOversizedEvent { event, _ -> // Remove contexts to reduce size event.contexts.keys().toList().forEach { event.contexts.remove(it) } event @@ -426,10 +426,10 @@ class EventSizeLimitingUtilsTest { } @Test - fun `onOversizedError callback multiple invocations not expected`() { + fun `onOversizedEvent callback multiple invocations not expected`() { val options = fixture.getOptions() var callbackInvocationCount = 0 - options.setOnOversizedError { event, _ -> + options.setOnOversizedEvent { event, _ -> callbackInvocationCount++ event } From 90fa44c0d3f844483cc68c47c270ca416d5a591f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 18 Nov 2025 09:57:44 +0100 Subject: [PATCH 08/12] Apply suggestions from code review Co-authored-by: Markus Hintersteiner --- .../main/java/io/sentry/util/EventSizeLimitingUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java index f291e119e6b..6fcfa81542b 100644 --- a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java +++ b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java @@ -23,7 +23,7 @@ public final class EventSizeLimitingUtils { private static final long MAX_EVENT_SIZE_BYTES = 1024 * 1024; - private static final int FRAMES_PER_SIDE = 250; + private static final int MAX_FRAMES_PER_STACK = 500; private EventSizeLimitingUtils() {} @@ -113,7 +113,7 @@ private static boolean isSizeOk( private static @NotNull SentryEvent removeAllBreadcrumbs( final @NotNull SentryEvent event, final @NotNull SentryOptions options) { - final List breadcrumbs = event.getBreadcrumbs(); + final @Nullable List breadcrumbs = event.getBreadcrumbs(); if (breadcrumbs != null && !breadcrumbs.isEmpty()) { event.setBreadcrumbs(null); options @@ -157,7 +157,7 @@ private static boolean isSizeOk( if (stacktrace != null) { final @Nullable List frames = stacktrace.getFrames(); if (frames != null && frames.size() > (FRAMES_PER_SIDE * 2)) { - final @NotNull List truncatedFrames = new ArrayList<>(); + final @NotNull List truncatedFrames = new ArrayList<>(FRAMES_PER_SIDE * 2); truncatedFrames.addAll(frames.subList(0, FRAMES_PER_SIDE)); truncatedFrames.addAll(frames.subList(frames.size() - FRAMES_PER_SIDE, frames.size())); stacktrace.setFrames(truncatedFrames); From af4060dccb76aa25b275d09b7cd3171062185bd8 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 18 Nov 2025 09:01:16 +0000 Subject: [PATCH 09/12] Format code --- .../src/main/java/io/sentry/util/EventSizeLimitingUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java index 6fcfa81542b..80b5616d49f 100644 --- a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java +++ b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java @@ -157,7 +157,8 @@ private static boolean isSizeOk( if (stacktrace != null) { final @Nullable List frames = stacktrace.getFrames(); if (frames != null && frames.size() > (FRAMES_PER_SIDE * 2)) { - final @NotNull List truncatedFrames = new ArrayList<>(FRAMES_PER_SIDE * 2); + final @NotNull List truncatedFrames = + new ArrayList<>(FRAMES_PER_SIDE * 2); truncatedFrames.addAll(frames.subList(0, FRAMES_PER_SIDE)); truncatedFrames.addAll(frames.subList(frames.size() - FRAMES_PER_SIDE, frames.size())); stacktrace.setFrames(truncatedFrames); From ec919441fa857b636c4a05efc2b3517ba3618520 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 18 Nov 2025 13:30:39 +0100 Subject: [PATCH 10/12] code review changes --- .../sentry/util/EventSizeLimitingUtils.java | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java index 80b5616d49f..f6a83ebc575 100644 --- a/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java +++ b/sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java @@ -24,6 +24,7 @@ public final class EventSizeLimitingUtils { private static final long MAX_EVENT_SIZE_BYTES = 1024 * 1024; private static final int MAX_FRAMES_PER_STACK = 500; + private static final int FRAMES_PER_SIDE = MAX_FRAMES_PER_STACK / 2; private EventSizeLimitingUtils() {} @@ -133,19 +134,8 @@ private static boolean isSizeOk( for (final @NotNull SentryException exception : exceptions) { final @Nullable SentryStackTrace stacktrace = exception.getStacktrace(); if (stacktrace != null) { - final @Nullable List frames = stacktrace.getFrames(); - if (frames != null && frames.size() > (FRAMES_PER_SIDE * 2)) { - final @NotNull List truncatedFrames = new ArrayList<>(); - truncatedFrames.addAll(frames.subList(0, FRAMES_PER_SIDE)); - truncatedFrames.addAll(frames.subList(frames.size() - FRAMES_PER_SIDE, frames.size())); - stacktrace.setFrames(truncatedFrames); - options - .getLogger() - .log( - SentryLevel.DEBUG, - "Truncated exception stack frames of event %s", - event.getEventId()); - } + truncateStackFramesInStackTrace( + stacktrace, event, options, "Truncated exception stack frames of event %s"); } } } @@ -155,24 +145,27 @@ private static boolean isSizeOk( for (final SentryThread thread : threads) { final @Nullable SentryStackTrace stacktrace = thread.getStacktrace(); if (stacktrace != null) { - final @Nullable List frames = stacktrace.getFrames(); - if (frames != null && frames.size() > (FRAMES_PER_SIDE * 2)) { - final @NotNull List truncatedFrames = - new ArrayList<>(FRAMES_PER_SIDE * 2); - truncatedFrames.addAll(frames.subList(0, FRAMES_PER_SIDE)); - truncatedFrames.addAll(frames.subList(frames.size() - FRAMES_PER_SIDE, frames.size())); - stacktrace.setFrames(truncatedFrames); - options - .getLogger() - .log( - SentryLevel.DEBUG, - "Truncated thread stack frames for event %s", - event.getEventId()); - } + truncateStackFramesInStackTrace( + stacktrace, event, options, "Truncated thread stack frames for event %s"); } } } return event; } + + private static void truncateStackFramesInStackTrace( + final @NotNull SentryStackTrace stacktrace, + final @NotNull SentryEvent event, + final @NotNull SentryOptions options, + final @NotNull String logMessage) { + final @Nullable List frames = stacktrace.getFrames(); + if (frames != null && frames.size() > MAX_FRAMES_PER_STACK) { + final @NotNull List truncatedFrames = new ArrayList<>(MAX_FRAMES_PER_STACK); + truncatedFrames.addAll(frames.subList(0, FRAMES_PER_SIDE)); + truncatedFrames.addAll(frames.subList(frames.size() - FRAMES_PER_SIDE, frames.size())); + stacktrace.setFrames(truncatedFrames); + options.getLogger().log(SentryLevel.DEBUG, logMessage, event.getEventId()); + } + } } From 0402a74513f23e30502c39b56adbb935ba8844d2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 18 Nov 2025 13:45:29 +0100 Subject: [PATCH 11/12] changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bb577a438b..72acb6b54cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Features + +- Detect oversized events and reduce their size ([#4903](https://github.com/getsentry/sentry-java/pull/4903)) + - You can opt into this new behaviour by setting `enableEventSizeLimiting` to `true` (`sentry.enable-event-size-limiting=true` for Spring Boot `application.properties`) + - You may optionally register an `onOversizedEvent` callback to implement custom logic that is executed in case an oversized event is detected + - This is executed first and if event size was reduced sufficiently, no further truncation is performed + - Currently we are dropping breadcrumbs and if that isn't sufficient also stack frames in order to get an events size down + ### Improvements - Do not send manual log origin ([#4897](https://github.com/getsentry/sentry-java/pull/4897)) From c7ee1e20034f934f66050fd95c7c9d0b9731381e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 19 Nov 2025 10:55:58 +0100 Subject: [PATCH 12/12] changelog update --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72acb6b54cd..48cd0a9cc56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - You can opt into this new behaviour by setting `enableEventSizeLimiting` to `true` (`sentry.enable-event-size-limiting=true` for Spring Boot `application.properties`) - You may optionally register an `onOversizedEvent` callback to implement custom logic that is executed in case an oversized event is detected - This is executed first and if event size was reduced sufficiently, no further truncation is performed - - Currently we are dropping breadcrumbs and if that isn't sufficient also stack frames in order to get an events size down + - In case we detect an oversized event, we first drop breadcrumbs and if that isn't sufficient we also drop stack frames in order to get an events size down ### Improvements