-
-
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 all 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,166 @@ | ||
| 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; | ||
|
|
||
| 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 (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; | ||
| } | ||
|
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); | ||
| 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; | ||
| } | ||
|
|
||
| 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(); | ||
| 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<>(); | ||
| 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()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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.