Skip to content

Commit 1b841a7

Browse files
adinauermarkushigetsentry-bot
authored
feat(events): Detect oversized events and reduce their size (#4903)
* Fix log count in client reports * add assertion to test * Detect oversized events * cleanup * invert size check method * try catch * rename callback to onOversizedEvent * Apply suggestions from code review Co-authored-by: Markus Hintersteiner <[email protected]> * Format code * code review changes * changelog * changelog update --------- Co-authored-by: Markus Hintersteiner <[email protected]> Co-authored-by: Sentry Github Bot <[email protected]>
1 parent a5ab36f commit 1b841a7

File tree

6 files changed

+720
-0
lines changed

6 files changed

+720
-0
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Detect oversized events and reduce their size ([#4903](https://github.com/getsentry/sentry-java/pull/4903))
8+
- You can opt into this new behaviour by setting `enableEventSizeLimiting` to `true` (`sentry.enable-event-size-limiting=true` for Spring Boot `application.properties`)
9+
- You may optionally register an `onOversizedEvent` callback to implement custom logic that is executed in case an oversized event is detected
10+
- This is executed first and if event size was reduced sufficiently, no further truncation is performed
11+
- 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
12+
513
### Improvements
614

715
- Do not send manual log origin ([#4897](https://github.com/getsentry/sentry-java/pull/4897))

sentry/api/sentry.api

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3417,6 +3417,7 @@ public class io/sentry/SentryOptions {
34173417
public fun getMaxTraceFileSize ()J
34183418
public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader;
34193419
public fun getOnDiscard ()Lio/sentry/SentryOptions$OnDiscardCallback;
3420+
public fun getOnOversizedEvent ()Lio/sentry/SentryOptions$OnOversizedEventCallback;
34203421
public fun getOpenTelemetryMode ()Lio/sentry/SentryOpenTelemetryMode;
34213422
public fun getOptionsObservers ()Ljava/util/List;
34223423
public fun getOutboxPath ()Ljava/lang/String;
@@ -3468,6 +3469,7 @@ public class io/sentry/SentryOptions {
34683469
public fun isEnableAutoSessionTracking ()Z
34693470
public fun isEnableBackpressureHandling ()Z
34703471
public fun isEnableDeduplication ()Z
3472+
public fun isEnableEventSizeLimiting ()Z
34713473
public fun isEnableExternalConfiguration ()Z
34723474
public fun isEnablePrettySerializationOutput ()Z
34733475
public fun isEnableScopePersistence ()Z
@@ -3524,6 +3526,7 @@ public class io/sentry/SentryOptions {
35243526
public fun setEnableAutoSessionTracking (Z)V
35253527
public fun setEnableBackpressureHandling (Z)V
35263528
public fun setEnableDeduplication (Z)V
3529+
public fun setEnableEventSizeLimiting (Z)V
35273530
public fun setEnableExternalConfiguration (Z)V
35283531
public fun setEnablePrettySerializationOutput (Z)V
35293532
public fun setEnableScopePersistence (Z)V
@@ -3566,6 +3569,7 @@ public class io/sentry/SentryOptions {
35663569
public fun setMaxTraceFileSize (J)V
35673570
public fun setModulesLoader (Lio/sentry/internal/modules/IModulesLoader;)V
35683571
public fun setOnDiscard (Lio/sentry/SentryOptions$OnDiscardCallback;)V
3572+
public fun setOnOversizedEvent (Lio/sentry/SentryOptions$OnOversizedEventCallback;)V
35693573
public fun setOpenTelemetryMode (Lio/sentry/SentryOpenTelemetryMode;)V
35703574
public fun setPrintUncaughtStackTrace (Z)V
35713575
public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V
@@ -3676,6 +3680,10 @@ public abstract interface class io/sentry/SentryOptions$OnDiscardCallback {
36763680
public abstract fun execute (Lio/sentry/clientreport/DiscardReason;Lio/sentry/DataCategory;Ljava/lang/Long;)V
36773681
}
36783682

3683+
public abstract interface class io/sentry/SentryOptions$OnOversizedEventCallback {
3684+
public abstract fun execute (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
3685+
}
3686+
36793687
public abstract interface class io/sentry/SentryOptions$ProfilesSamplerCallback {
36803688
public abstract fun sample (Lio/sentry/SamplingContext;)Ljava/lang/Double;
36813689
}
@@ -7124,6 +7132,10 @@ public final class io/sentry/util/EventProcessorUtils {
71247132
public static fun unwrap (Ljava/util/List;)Ljava/util/List;
71257133
}
71267134

7135+
public final class io/sentry/util/EventSizeLimitingUtils {
7136+
public static fun limitEventSize (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/SentryOptions;)Lio/sentry/SentryEvent;
7137+
}
7138+
71277139
public final class io/sentry/util/ExceptionUtils {
71287140
public fun <init> ()V
71297141
public static fun findRootCause (Ljava/lang/Throwable;)Ljava/lang/Throwable;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul
164164
}
165165
}
166166

167+
if (event != null) {
168+
event = EventSizeLimitingUtils.limitEventSize(event, hint, options);
169+
}
170+
167171
if (event == null) {
168172
return SentryId.EMPTY_ID;
169173
}

sentry/src/main/java/io/sentry/SentryOptions.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,18 @@ public class SentryOptions {
353353
*/
354354
private boolean enableDeduplication = true;
355355

356+
/**
357+
* Enables event size limiting with {@link EventSizeLimitingEventProcessor}. When enabled, events
358+
* exceeding 1MB will have breadcrumbs and stack frames reduced to stay under the limit.
359+
*/
360+
private boolean enableEventSizeLimiting = false;
361+
362+
/**
363+
* Callback invoked when an oversized event is detected. This allows custom handling of oversized
364+
* events before the automatic reduction steps are applied.
365+
*/
366+
private @Nullable OnOversizedEventCallback onOversizedEvent;
367+
356368
/** Maximum number of spans that can be atteched to single transaction. */
357369
private int maxSpans = 1000;
358370

@@ -1752,6 +1764,44 @@ public void setEnableDeduplication(final boolean enableDeduplication) {
17521764
this.enableDeduplication = enableDeduplication;
17531765
}
17541766

1767+
/**
1768+
* Returns if event size limiting is enabled.
1769+
*
1770+
* @return true if event size limiting is enabled, false otherwise
1771+
*/
1772+
public boolean isEnableEventSizeLimiting() {
1773+
return enableEventSizeLimiting;
1774+
}
1775+
1776+
/**
1777+
* Enables or disables event size limiting. When enabled, events exceeding 1MB will have
1778+
* breadcrumbs and stack frames reduced to stay under the limit.
1779+
*
1780+
* @param enableEventSizeLimiting true to enable, false to disable
1781+
*/
1782+
public void setEnableEventSizeLimiting(final boolean enableEventSizeLimiting) {
1783+
this.enableEventSizeLimiting = enableEventSizeLimiting;
1784+
}
1785+
1786+
/**
1787+
* Returns the onOversizedEvent callback.
1788+
*
1789+
* @return the onOversizedEvent callback or null if not set
1790+
*/
1791+
public @Nullable OnOversizedEventCallback getOnOversizedEvent() {
1792+
return onOversizedEvent;
1793+
}
1794+
1795+
/**
1796+
* Sets the onOversizedEvent callback. This callback is invoked when an oversized event is
1797+
* detected, before the automatic reduction steps are applied.
1798+
*
1799+
* @param onOversizedEvent the onOversizedEvent callback
1800+
*/
1801+
public void setOnOversizedEvent(@Nullable OnOversizedEventCallback onOversizedEvent) {
1802+
this.onOversizedEvent = onOversizedEvent;
1803+
}
1804+
17551805
/**
17561806
* Returns if tracing should be enabled. If tracing is disabled, starting transactions returns
17571807
* {@link NoOpTransaction}.
@@ -3136,6 +3186,21 @@ public interface BeforeBreadcrumbCallback {
31363186
Breadcrumb execute(@NotNull Breadcrumb breadcrumb, @NotNull Hint hint);
31373187
}
31383188

3189+
/** The OnOversizedEvent callback */
3190+
public interface OnOversizedEventCallback {
3191+
3192+
/**
3193+
* Called when an oversized event is detected. This callback allows custom handling of oversized
3194+
* events before automatic reduction steps are applied.
3195+
*
3196+
* @param event the oversized event
3197+
* @param hint the hints
3198+
* @return the modified event (should ideally be reduced in size)
3199+
*/
3200+
@NotNull
3201+
SentryEvent execute(@NotNull SentryEvent event, @NotNull Hint hint);
3202+
}
3203+
31393204
/** The OnDiscard callback */
31403205
public interface OnDiscardCallback {
31413206

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package io.sentry.util;
2+
3+
import io.sentry.Breadcrumb;
4+
import io.sentry.Hint;
5+
import io.sentry.SentryEvent;
6+
import io.sentry.SentryLevel;
7+
import io.sentry.SentryOptions;
8+
import io.sentry.protocol.SentryException;
9+
import io.sentry.protocol.SentryStackFrame;
10+
import io.sentry.protocol.SentryStackTrace;
11+
import io.sentry.protocol.SentryThread;
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import org.jetbrains.annotations.ApiStatus;
15+
import org.jetbrains.annotations.NotNull;
16+
import org.jetbrains.annotations.Nullable;
17+
18+
/**
19+
* Utility class that limits event size to 1MB by incrementally dropping fields when the event
20+
* exceeds the limit.
21+
*/
22+
@ApiStatus.Internal
23+
public final class EventSizeLimitingUtils {
24+
25+
private static final long MAX_EVENT_SIZE_BYTES = 1024 * 1024;
26+
private static final int MAX_FRAMES_PER_STACK = 500;
27+
private static final int FRAMES_PER_SIDE = MAX_FRAMES_PER_STACK / 2;
28+
29+
private EventSizeLimitingUtils() {}
30+
31+
/**
32+
* Limits the size of an event by incrementally dropping fields when it exceeds the limit.
33+
*
34+
* @param event the event to limit
35+
* @param hint the hint
36+
* @param options the SentryOptions
37+
* @return the potentially reduced event
38+
*/
39+
public static @Nullable SentryEvent limitEventSize(
40+
final @NotNull SentryEvent event,
41+
final @NotNull Hint hint,
42+
final @NotNull SentryOptions options) {
43+
try {
44+
if (!options.isEnableEventSizeLimiting()) {
45+
return event;
46+
}
47+
48+
if (isSizeOk(event, options)) {
49+
return event;
50+
}
51+
52+
options
53+
.getLogger()
54+
.log(
55+
SentryLevel.INFO,
56+
"Event %s exceeds %d bytes limit. Reducing size by dropping fields.",
57+
event.getEventId(),
58+
MAX_EVENT_SIZE_BYTES);
59+
60+
@NotNull SentryEvent reducedEvent = event;
61+
62+
final @Nullable SentryOptions.OnOversizedEventCallback callback =
63+
options.getOnOversizedEvent();
64+
if (callback != null) {
65+
try {
66+
reducedEvent = callback.execute(reducedEvent, hint);
67+
if (isSizeOk(reducedEvent, options)) {
68+
return reducedEvent;
69+
}
70+
} catch (Throwable e) {
71+
options
72+
.getLogger()
73+
.log(
74+
SentryLevel.ERROR,
75+
"The onOversizedEvent callback threw an exception. It will be ignored and automatic reduction will continue.",
76+
e);
77+
reducedEvent = event;
78+
}
79+
}
80+
81+
reducedEvent = removeAllBreadcrumbs(reducedEvent, options);
82+
if (isSizeOk(reducedEvent, options)) {
83+
return reducedEvent;
84+
}
85+
86+
reducedEvent = truncateStackFrames(reducedEvent, options);
87+
if (!isSizeOk(reducedEvent, options)) {
88+
options
89+
.getLogger()
90+
.log(
91+
SentryLevel.WARNING,
92+
"Event %s still exceeds size limit after reducing all fields. Event may be rejected by server.",
93+
event.getEventId());
94+
}
95+
96+
return reducedEvent;
97+
} catch (Throwable e) {
98+
options
99+
.getLogger()
100+
.log(
101+
SentryLevel.ERROR,
102+
"An error occurred while limiting event size. Event will be sent as-is.",
103+
e);
104+
return event;
105+
}
106+
}
107+
108+
private static boolean isSizeOk(
109+
final @NotNull SentryEvent event, final @NotNull SentryOptions options) {
110+
final long size =
111+
JsonSerializationUtils.byteSizeOf(options.getSerializer(), options.getLogger(), event);
112+
return size <= MAX_EVENT_SIZE_BYTES;
113+
}
114+
115+
private static @NotNull SentryEvent removeAllBreadcrumbs(
116+
final @NotNull SentryEvent event, final @NotNull SentryOptions options) {
117+
final @Nullable List<Breadcrumb> breadcrumbs = event.getBreadcrumbs();
118+
if (breadcrumbs != null && !breadcrumbs.isEmpty()) {
119+
event.setBreadcrumbs(null);
120+
options
121+
.getLogger()
122+
.log(
123+
SentryLevel.DEBUG,
124+
"Removed breadcrumbs to reduce size of event %s",
125+
event.getEventId());
126+
}
127+
return event;
128+
}
129+
130+
private static @NotNull SentryEvent truncateStackFrames(
131+
final @NotNull SentryEvent event, final @NotNull SentryOptions options) {
132+
final @Nullable List<SentryException> exceptions = event.getExceptions();
133+
if (exceptions != null) {
134+
for (final @NotNull SentryException exception : exceptions) {
135+
final @Nullable SentryStackTrace stacktrace = exception.getStacktrace();
136+
if (stacktrace != null) {
137+
truncateStackFramesInStackTrace(
138+
stacktrace, event, options, "Truncated exception stack frames of event %s");
139+
}
140+
}
141+
}
142+
143+
final @Nullable List<SentryThread> threads = event.getThreads();
144+
if (threads != null) {
145+
for (final SentryThread thread : threads) {
146+
final @Nullable SentryStackTrace stacktrace = thread.getStacktrace();
147+
if (stacktrace != null) {
148+
truncateStackFramesInStackTrace(
149+
stacktrace, event, options, "Truncated thread stack frames for event %s");
150+
}
151+
}
152+
}
153+
154+
return event;
155+
}
156+
157+
private static void truncateStackFramesInStackTrace(
158+
final @NotNull SentryStackTrace stacktrace,
159+
final @NotNull SentryEvent event,
160+
final @NotNull SentryOptions options,
161+
final @NotNull String logMessage) {
162+
final @Nullable List<SentryStackFrame> frames = stacktrace.getFrames();
163+
if (frames != null && frames.size() > MAX_FRAMES_PER_STACK) {
164+
final @NotNull List<SentryStackFrame> truncatedFrames = new ArrayList<>(MAX_FRAMES_PER_STACK);
165+
truncatedFrames.addAll(frames.subList(0, FRAMES_PER_SIDE));
166+
truncatedFrames.addAll(frames.subList(frames.size() - FRAMES_PER_SIDE, frames.size()));
167+
stacktrace.setFrames(truncatedFrames);
168+
options.getLogger().log(SentryLevel.DEBUG, logMessage, event.getEventId());
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)