-
-
Notifications
You must be signed in to change notification settings - Fork 462
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
Merged
+720
−0
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
d2a38cc
Fix log count in client reports
adinauer 3bd2cf0
add assertion to test
adinauer 6c5011f
Detect oversized events
adinauer e6e75dc
Merge branch 'main' into feat/detect-oversized-error-events
adinauer ee63d11
cleanup
adinauer 486fc42
invert size check method
adinauer 9afb21d
try catch
adinauer e818730
rename callback to onOversizedEvent
adinauer 90fa44c
Apply suggestions from code review
adinauer af4060d
Format code
getsentry-bot ec91944
code review changes
adinauer 0402a74
changelog
adinauer 00e07b1
Merge branch 'main' into feat/detect-oversized-error-events
adinauer c7ee1e2
changelog update
adinauer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
|
|
||
|
|
||
171 changes: 171 additions & 0 deletions
171
sentry/src/main/java/io/sentry/util/EventSizeLimitingUtils.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
adinauer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
adinauer marked this conversation as resolved.
Show resolved
Hide 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.
Show resolved
Hide 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 @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()); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.