-
-
Notifications
You must be signed in to change notification settings - Fork 461
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
base: main
Are you sure you want to change the base?
Changes from 4 commits
d2a38cc
3bd2cf0
6c5011f
e6e75dc
ee63d11
486fc42
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 OnOversizedErrorCallback onOversizedError; | ||||||
|
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. What should we best call this so customers know what it's used for?
Suggested change
|
||||||
|
|
||||||
| /** 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 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}. | ||||||
|
|
@@ -3136,6 +3186,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); | ||||||
|
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,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. | ||
| * | ||
| * <p>Fields are reduced in order of least importance: | ||
| * | ||
| * <ol> | ||
| * <li>All breadcrumbs | ||
| * <li>Exception stack frames (keep 250 frames from start and 250 frames from end, removing | ||
| * middle) | ||
| * </ol> | ||
| * | ||
| * <p>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; | ||
| } | ||
|
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. Bug: Misinterpreted Null Leads to Silent Data LossWhen |
||
| } catch (Exception e) { | ||
|
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. Bug: Callback Errors: A Crash HazardThe callback exception handler catches |
||
| 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<Breadcrumb> 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<SentryException> exceptions = event.getExceptions(); | ||
| if (exceptions != null) { | ||
| for (final SentryException exception : exceptions) { | ||
| final SentryStackTrace stacktrace = exception.getStacktrace(); | ||
| if (stacktrace != null) { | ||
| final List<SentryStackFrame> frames = stacktrace.getFrames(); | ||
| if (frames != null && frames.size() > FRAMES_PER_SIDE * 2) { | ||
| // Keep first 250 frames and last 250 frames, removing middle | ||
| final 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 stack frames from %d to %d (removed middle) for exception %s", | ||
| frames.size(), | ||
| truncatedFrames.size(), | ||
| exception.getType()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Also truncate thread stack traces | ||
| final List<SentryThread> threads = event.getThreads(); | ||
| if (threads != null) { | ||
| for (final SentryThread thread : threads) { | ||
| final SentryStackTrace stacktrace = thread.getStacktrace(); | ||
| if (stacktrace != null) { | ||
| final List<SentryStackFrame> frames = stacktrace.getFrames(); | ||
| if (frames != null && frames.size() > FRAMES_PER_SIDE * 2) { | ||
| // Keep first 250 frames and last 250 frames, removing middle | ||
| final 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 stack frames from %d to %d (removed middle) for thread %d", | ||
| frames.size(), | ||
| truncatedFrames.size(), | ||
| thread.getId()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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.