-
-
Notifications
You must be signed in to change notification settings - Fork 467
feat(events): Detect oversized events and reduce their size #4903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
d2a38cc
3bd2cf0
6c5011f
e6e75dc
ee63d11
486fc42
9afb21d
e818730
90fa44c
af4060d
ec91944
0402a74
00e07b1
c7ee1e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
||
| /** | ||
| * 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; | ||
|
|
||
|
|
@@ -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}. | ||
|
|
@@ -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); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO running this between |
||
| } | ||
|
|
||
| /** The OnDiscard callback */ | ||
| public interface OnDiscardCallback { | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| 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 FRAMES_PER_SIDE = 250; | ||
|
adinauer marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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; | ||
|
adinauer marked this conversation as resolved.
|
||
| } | ||
|
adinauer marked this conversation as resolved.
|
||
| } | ||
|
|
||
| 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()); | ||
| } | ||
|
adinauer marked this conversation as resolved.
|
||
|
|
||
| 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 List<Breadcrumb> breadcrumbs = event.getBreadcrumbs(); | ||
|
adinauer marked this conversation as resolved.
Outdated
|
||
| 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) { | ||
| final @Nullable List<SentryStackFrame> frames = stacktrace.getFrames(); | ||
| if (frames != null && frames.size() > (FRAMES_PER_SIDE * 2)) { | ||
| final @NotNull List<SentryStackFrame> 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()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| final @Nullable List<SentryThread> threads = event.getThreads(); | ||
| if (threads != null) { | ||
| for (final SentryThread thread : threads) { | ||
| final @Nullable SentryStackTrace stacktrace = thread.getStacktrace(); | ||
| if (stacktrace != null) { | ||
| final @Nullable List<SentryStackFrame> frames = stacktrace.getFrames(); | ||
| if (frames != null && frames.size() > (FRAMES_PER_SIDE * 2)) { | ||
| final @NotNull List<SentryStackFrame> truncatedFrames = new ArrayList<>(); | ||
|
adinauer marked this conversation as resolved.
Outdated
|
||
| truncatedFrames.addAll(frames.subList(0, FRAMES_PER_SIDE)); | ||
| truncatedFrames.addAll(frames.subList(frames.size() - FRAMES_PER_SIDE, frames.size())); | ||
| stacktrace.setFrames(truncatedFrames); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could extract this into a |
||
| options | ||
| .getLogger() | ||
| .log( | ||
| SentryLevel.DEBUG, | ||
| "Truncated thread stack frames for event %s", | ||
| event.getEventId()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return event; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.