Skip to content
Merged
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- 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

- Do not send manual log origin ([#4897](https://github.com/getsentry/sentry-java/pull/4897))
Expand Down
12 changes: 12 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -3417,6 +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 getOnOversizedEvent ()Lio/sentry/SentryOptions$OnOversizedEventCallback;
public fun getOpenTelemetryMode ()Lio/sentry/SentryOpenTelemetryMode;
public fun getOptionsObservers ()Ljava/util/List;
public fun getOutboxPath ()Ljava/lang/String;
Expand Down Expand Up @@ -3468,6 +3469,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
Expand Down Expand Up @@ -3524,6 +3526,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
Expand Down Expand Up @@ -3566,6 +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 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
Expand Down Expand Up @@ -3676,6 +3680,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$OnOversizedEventCallback {
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;
}
Expand Down Expand Up @@ -7124,6 +7132,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 <init> ()V
public static fun findRootCause (Ljava/lang/Throwable;)Ljava/lang/Throwable;
Expand Down
4 changes: 4 additions & 0 deletions sentry/src/main/java/io/sentry/SentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,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;
}
Expand Down
65 changes: 65 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,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;
Copy link
Member Author

Choose a reason for hiding this comment

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

Kepping this opt-in for now, we could turn it on by default in the next major.


/**
* Callback invoked when an oversized event is detected. This allows custom handling of oversized
* events before the automatic reduction steps are applied.
*/
private @Nullable OnOversizedEventCallback onOversizedEvent;

/** Maximum number of spans that can be atteched to single transaction. */
private int maxSpans = 1000;

Expand Down Expand Up @@ -1752,6 +1764,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 onOversizedEvent callback.
*
* @return the onOversizedEvent callback or null if not set
*/
public @Nullable OnOversizedEventCallback getOnOversizedEvent() {
return onOversizedEvent;
}

/**
* Sets the onOversizedEvent callback. This callback is invoked when an oversized event is
* detected, before the automatic reduction steps are applied.
*
* @param onOversizedEvent the onOversizedEvent callback
*/
public void setOnOversizedEvent(@Nullable OnOversizedEventCallback onOversizedEvent) {
this.onOversizedEvent = onOversizedEvent;
}

/**
* Returns if tracing should be enabled. If tracing is disabled, starting transactions returns
* {@link NoOpTransaction}.
Expand Down Expand Up @@ -3136,6 +3186,21 @@ public interface BeforeBreadcrumbCallback {
Breadcrumb execute(@NotNull Breadcrumb breadcrumb, @NotNull Hint hint);
}

/** The OnOversizedEvent callback */
public interface OnOversizedEventCallback {

/**
* 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);
Copy link
Member Author

@adinauer adinauer Nov 14, 2025

Choose a reason for hiding this comment

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

IMO running this between beforeSend and passing this on to the transport is the best place to check since customers might be adding large amounts of data in beforeSend, so first reducing size and then adding back to it could easily cause the event to become too large again.

}

/** The OnDiscard callback */
public interface OnDiscardCallback {

Expand Down
171 changes: 171 additions & 0 deletions sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
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.
*/
@ApiStatus.Internal
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() {}

/**
* 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) {
try {
if (!options.isEnableEventSizeLimiting()) {
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.OnOversizedEventCallback callback =
options.getOnOversizedEvent();
if (callback != null) {
try {
reducedEvent = callback.execute(reducedEvent, hint);
if (isSizeOk(reducedEvent, options)) {
return reducedEvent;
}
} catch (Throwable e) {
options
.getLogger()
.log(
SentryLevel.ERROR,
"The onOversizedEvent callback threw an exception. It will be ignored and automatic reduction will continue.",
e);
reducedEvent = event;
}
}

reducedEvent = removeAllBreadcrumbs(reducedEvent, options);
if (isSizeOk(reducedEvent, options)) {
return reducedEvent;
}

reducedEvent = truncateStackFrames(reducedEvent, options);
if (!isSizeOk(reducedEvent, options)) {
options
.getLogger()
.log(
SentryLevel.WARNING,
"Event %s still exceeds size limit after reducing all fields. Event may be rejected by server.",
event.getEventId());
}

return reducedEvent;
} catch (Throwable e) {
options
.getLogger()
.log(
SentryLevel.ERROR,
"An error occurred while limiting event size. Event will be sent as-is.",
e);
return event;
}
}

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;
}

private static @NotNull SentryEvent removeAllBreadcrumbs(
final @NotNull SentryEvent event, final @NotNull SentryOptions options) {
final @Nullable List<Breadcrumb> breadcrumbs = event.getBreadcrumbs();
if (breadcrumbs != null && !breadcrumbs.isEmpty()) {
event.setBreadcrumbs(null);
options
.getLogger()
.log(
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 @Nullable List<SentryException> exceptions = event.getExceptions();
if (exceptions != null) {
for (final @NotNull SentryException exception : exceptions) {
final @Nullable SentryStackTrace stacktrace = exception.getStacktrace();
if (stacktrace != null) {
truncateStackFramesInStackTrace(
stacktrace, event, options, "Truncated exception stack frames of event %s");
}
}
}

final @Nullable List<SentryThread> threads = event.getThreads();
if (threads != null) {
for (final SentryThread thread : threads) {
final @Nullable SentryStackTrace stacktrace = thread.getStacktrace();
if (stacktrace != null) {
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<SentryStackFrame> frames = stacktrace.getFrames();
if (frames != null && frames.size() > MAX_FRAMES_PER_STACK) {
final @NotNull List<SentryStackFrame> 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());
}
}
}
Loading
Loading