Skip to content

Commit e6ec71a

Browse files
adinauerclaude
andauthored
fix(core): Handle callback errors (#6140)
* collection: callback error handling * fix(core): [Callback Errors 1] Report callback failures Add the callback_error discard reason and use it when a beforeSend callback throws. Preserve before_send for callbacks that intentionally return null and retain item, span, and byte accounting. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * fix(core): [Callback Errors 2] Drop failed processor data Stop processing and drop telemetry when an event processor throws. Record callback_error outcomes for every supported category instead of sending potentially partially processed data. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * fix(core): [Callback Errors 3] Drop failed breadcrumbs Drop breadcrumbs when beforeBreadcrumb throws instead of storing the breadcrumb with exception details. Keep observer state unchanged and release the callback reentrancy guard for later additions. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * ref(core): [Callback Errors 2] Mark SDK-owned event processors Add an internal marker to SDK event processors across core and integrations. Keep customer processors and the backfilling marker independent so later callback handling can distinguish ownership regardless of registration path. Leave exception handling unchanged in this preparatory change. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * fix(core): [Callback Errors 3] Preserve SDK processor data Use the internal processor marker to continue processing after SDK-owned processor failures without recording callback_error losses. Keep customer processor failures fail-closed across events, transactions, replays, feedback, logs, and metrics. Cover scope and options registration, continued callbacks and delivery, logging without discard notifications, intentional drops, and span loss accounting. Clarify the customer-only failure policy in the changelog. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * fix(core): [Callback Errors 5] Handle tracesSampler failures Inherit the parent sampling decision when tracesSampler throws. Without a parent decision, leave the trace unsampled instead of applying the static tracesSampleRate, which can override the failed sampling policy. Keep normal null-result fallback, profiling callbacks, and catch types unchanged. Cover parent inheritance, backfilling, and static-rate bypass. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * changelog Link the tracesSampler failure fallback entry to #6163 in the callback error handling stack. * fix(core): [Callback Errors 6] Handle profilesSampler failures Disable profiling when profilesSampler throws instead of applying the static profilesSampleRate or inheriting a parent profiling decision. Preserve trace sampling metadata and normal null-result fallback. Cover static-rate bypass, parent inheritance, and failures in both samplers without changing existing catch types. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * changelog Link the profilesSampler failure handling entry to #6164 in the callback error handling stack. * fix(core): [Callback Errors 7] Handle replay sampling failures Skip replay capture when beforeErrorSampling throws instead of capturing replay after a failed user filter. Continue sending the error event and retain existing exception logging and catch types. Cover handled errors, crashes, and successful replay capture after a previous callback failure. Verify error delivery and failure logging. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * changelog Link the beforeErrorSampling failure entry to PR #6165. Co-Authored-By: Claude <noreply@anthropic.com> * fix(core): [Callback Errors 8] Report dropped profiles Record callback_error/profile when beforeSendTransaction or a customer transaction processor throws and an attached profile is dropped. Notify OnDiscardCallback through the same client-report accounting path. Cover scope and options processors, absent profiles, and SDK processor failures that retain profiles. Leave catch types, intentional drops, and profile-file cleanup unchanged. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * changelog Link the transaction profile loss accounting entry to PR #6166. Co-Authored-By: Claude <noreply@anthropic.com> * fix(integrations): [Callback Errors 9] Guard custom callbacks Skip Android screenshot and view hierarchy capture when their callbacks throw, while retaining the error event. Drop spans on beforeSpan failures in OkHttp, OpenFeign, GraphQL, Ktor, and Apollo without disrupting requests or replacing the original request error. Preserve normal callback results and existing catch types. Finish failed spans and retain request cleanup and breadcrumbs. Add regression coverage for callback failures, partial mutations, original request errors, asynchronous GraphQL results, and subsequent Android captures. Correct callback wiring in the Ktor and screenshot test fixtures. Verify 18 regression cases fail before the fix and pass afterward; all 454 tests in the affected suites pass, along with formatting and API checks. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * changelog Link the Android capture and integration beforeSpan callback fixes to Callback Errors 9 (#6167). Co-Authored-By: Claude <noreply@anthropic.com> * fix(core): Drop transactions when tracesSampler fails Replace parent-decision fallback after tracesSampler errors with an unsampled decision and callback_error reports for the transaction and root span. Preserve deliberate null-result fallback and existing catches. Report callback errors directly in the sampler and retain existing sample_rate and backpressure accounting, accepting duplicate loss reports instead of adding discard-reason propagation. Cover core and OpenTelemetry paths with regression tests and update the changelog. Refs #6163 Co-Authored-By: Claude <noreply@anthropic.com> * fix(integrations): Report beforeSpan callback losses Record callback_error/span when a throwing beforeSpan callback drops a sampled span in OkHttp, OpenFeign, GraphQL, Ktor, and Apollo. Use the pre-callback sampling decision so partial callback mutations do not hide losses, and leave unsampled spans and intentional null drops uncounted. Add regression coverage for discard notifications, sampling states, and intentional drops while preserving request and span completion behavior. Refs #6167 Co-Authored-By: Claude <noreply@anthropic.com> * docs(core): Document SDK backfilling processor marker Clarify that SDK-owned backfilling processors must also use the Sentry processor marker so callback error handling can identify their ownership. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * fix: Report profiles dropped with transactions Record attached profiles as lost when transaction event processors or beforeSendTransaction intentionally discard the transaction. Keep client reports and discard callbacks aligned with dropped profile payloads. Co-Authored-By: Claude <noreply@anthropic.com> * fix(core): Report replay sampling callback errors Record callback_error/replay when beforeErrorSampling throws while the replay controller has an active replay ID. Do not report a loss when no replay is active. Refs GH-6081 Co-Authored-By: Claude <noreply@anthropic.com> * fix(apollo5): Handle beforeSpan callback failures Drop spans when the Apollo 5 beforeSpan callback throws while preserving the GraphQL response. Report sampled spans as callback errors so client reports and discard callbacks remain accurate. Co-Authored-By: Claude <noreply@anthropic.com> * changelog * fix(android): Handle feedback configurator failures Keep the feedback form usable when an options configuration callback throws. Apply callback changes transactionally so partially mutated feedback options are discarded. Co-Authored-By: Claude <noreply@anthropic.com> * changelog * fix(spring): Handle user provider failures Keep requests running when a Spring user provider throws. Continue invoking later providers, but discard all provider-derived identity and install an empty user so inherited identity cannot leak into the event. Co-Authored-By: Claude <noreply@anthropic.com> * changelog * fix(spring): Handle transaction name provider failures Keep Spring requests and event capture running when a transaction name provider throws. Use an unlabeled custom transaction as the privacy-safe fallback and continue to later providers when resolving combined names. Apply the same behavior across the Spring, Spring Jakarta, and Spring 7 integrations. Co-Authored-By: Claude <noreply@anthropic.com> * changelog * test(apollo): Fix callback test lint violations Use Kotlin's error helper in callback failure tests so Detekt accepts the intentional IllegalStateException across Apollo 2, 3, and 4. Co-Authored-By: Claude <noreply@anthropic.com> * fix(spring): Include user provider name in error log Identify which SentryUserProvider failed when multiple providers are configured, making callback failures actionable across all Spring variants. Refs GH-6240 Co-Authored-By: Claude <noreply@anthropic.com> * test(android): Verify configurator runs after configuration failure Ensure a failed legacy configuration does not prevent the configurator from receiving the original feedback options and applying its changes. Co-Authored-By: Claude <noreply@anthropic.com> * test(android): Verify configurator propagates fatal errors Ensure fatal errors from the feedback options configurator are rethrown instead of being handled as recoverable callback failures. Co-Authored-By: Claude <noreply@anthropic.com> * docs(changelog): Consolidate callback error fixes Move the callback error handling entry back to Unreleased and group the related changes by impact so the full behavior change is documented in one place. Co-Authored-By: Claude <noreply@anthropic.com> * fix(callbacks): Handle nonfatal callback throwables Catch nonfatal Throwable failures from newly guarded customer callbacks while continuing to propagate fatal JVM errors. This keeps callback isolation consistent without introducing new fatal-error swallowing. Add regression coverage for nonfatal fallback behavior and fatal error propagation across the affected integrations. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> * changelog * fix(callbacks): Preserve Apollo 2 error handling Keep Apollo 2's existing Exception boundary unchanged so non-Exception errors retain their released behavior. Limit the broader nonfatal Throwable handling to callback guards introduced by the unreleased stack. Refs #6081 Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9122a68 commit e6ec71a

109 files changed

Lines changed: 3861 additions & 333 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

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

33
## Unreleased
44

5+
### Fixes
6+
7+
- Fix SDK callback error handling ([#6140](https://github.com/getsentry/sentry-java/pull/6140))
8+
- Drop telemetry when a `beforeSend*` callback throws and report it using `DiscardReason.CALLBACK_ERROR`.
9+
- Drop transactions when `tracesSampler` throws instead of inheriting the parent sampling decision or falling back to `tracesSampleRate` ([#6163](https://github.com/getsentry/sentry-java/pull/6163)).
10+
- Drop spans when `beforeSpan` throws without disrupting requests, covering OkHttp, OpenFeign, GraphQL, Ktor, and Apollo—including Apollo 5 ([#6167](https://github.com/getsentry/sentry-java/pull/6167), [#6238](https://github.com/getsentry/sentry-java/pull/6238)).
11+
- Drop telemetry and record `callback_error` when a customer event processor throws instead of continuing with a potentially partially processed item. SDK-owned processor failures are logged and processing continues.
12+
- Drop breadcrumbs when `beforeBreadcrumb` throws instead of storing exception details on the breadcrumb.
13+
- No longer fail Spring requests when a `TransactionNameProvider` throws, falling back to `<unlabeled transaction>` ([#6241](https://github.com/getsentry/sentry-java/pull/6241)).
14+
- No longer fail Spring requests when a `SentryUserProvider` throws and overwrite the user with an empty user object ([#6240](https://github.com/getsentry/sentry-java/pull/6240)).
15+
- Disable profiling when `profilesSampler` throws instead of falling back to `profilesSampleRate` or inheriting the parent profiling decision ([#6164](https://github.com/getsentry/sentry-java/pull/6164)).
16+
- Report attached profiles dropped by transaction callback errors as `callback_error` in client reports and `OnDiscardCallback` ([#6166](https://github.com/getsentry/sentry-java/pull/6166)).
17+
- Skip replay capture when `beforeErrorSampling` throws while retaining the error event ([#6165](https://github.com/getsentry/sentry-java/pull/6165)).
18+
- Skip Android screenshots or view hierarchies when their capture callbacks throw while retaining the error event ([#6167](https://github.com/getsentry/sentry-java/pull/6167)).
19+
- Keep the Android feedback form available when its options configurator throws ([#6239](https://github.com/getsentry/sentry-java/pull/6239)).
20+
- Isolate nonfatal callback throwables while continuing to propagate fatal JVM errors ([#6251](https://github.com/getsentry/sentry-java/pull/6251)).
21+
522
### Dependencies
623

724
- Bump Native SDK from v0.17.2 to v0.17.3 ([#6250](https://github.com/getsentry/sentry-java/pull/6250))

‎sentry-android-core/api/sentry-android-core.api‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ public final class io/sentry/android/core/AppState$LifecycleObserver : androidx/
233233
public fun onStop (Landroidx/lifecycle/LifecycleOwner;)V
234234
}
235235

236-
public final class io/sentry/android/core/ApplicationExitInfoEventProcessor : io/sentry/BackfillingEventProcessor {
236+
public final class io/sentry/android/core/ApplicationExitInfoEventProcessor : io/sentry/BackfillingEventProcessor, io/sentry/internal/eventprocessor/SentryEventProcessor {
237237
public fun <init> (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V
238238
public fun getOrder ()Ljava/lang/Long;
239239
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
@@ -401,7 +401,7 @@ public class io/sentry/android/core/PerfettoProfiler {
401401
public fun endAndCollect (Ljava/util/function/Consumer;)V
402402
}
403403

404-
public final class io/sentry/android/core/ScreenshotEventProcessor : io/sentry/EventProcessor {
404+
public final class io/sentry/android/core/ScreenshotEventProcessor : io/sentry/internal/eventprocessor/SentryEventProcessor {
405405
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;Z)V
406406
public fun getOrder ()Ljava/lang/Long;
407407
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
@@ -680,7 +680,7 @@ public final class io/sentry/android/core/UserInteractionIntegration : android/a
680680
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
681681
}
682682

683-
public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentry/EventProcessor {
683+
public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentry/internal/eventprocessor/SentryEventProcessor {
684684
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
685685
public fun getOrder ()Ljava/lang/Long;
686686
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;

‎sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import io.sentry.hints.AbnormalExit;
5353
import io.sentry.hints.Backfillable;
5454
import io.sentry.hints.NativeCrashExit;
55+
import io.sentry.internal.eventprocessor.SentryEventProcessor;
5556
import io.sentry.protocol.App;
5657
import io.sentry.protocol.Contexts;
5758
import io.sentry.protocol.DebugImage;
@@ -89,7 +90,8 @@
8990
*/
9091
@ApiStatus.Internal
9192
@WorkerThread
92-
public final class ApplicationExitInfoEventProcessor implements BackfillingEventProcessor {
93+
public final class ApplicationExitInfoEventProcessor
94+
implements BackfillingEventProcessor, SentryEventProcessor {
9395

9496
private final @NotNull Context context;
9597

‎sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.sentry.android.core.internal.util.AndroidThreadChecker;
99
import io.sentry.android.core.performance.AppStartMetrics;
1010
import io.sentry.android.core.performance.TimeSpan;
11+
import io.sentry.internal.eventprocessor.SentryEventProcessor;
1112
import io.sentry.protocol.App;
1213
import io.sentry.protocol.OperatingSystem;
1314
import io.sentry.protocol.SentryException;
@@ -32,7 +33,7 @@
3233
import org.jetbrains.annotations.Nullable;
3334
import org.jetbrains.annotations.TestOnly;
3435

35-
final class DefaultAndroidEventProcessor implements EventProcessor {
36+
final class DefaultAndroidEventProcessor implements SentryEventProcessor {
3637

3738
@TestOnly final Context context;
3839

‎sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import static io.sentry.android.core.ActivityLifecycleIntegration.STANDALONE_APP_START_OP;
88
import static io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP;
99

10-
import io.sentry.EventProcessor;
1110
import io.sentry.Hint;
1211
import io.sentry.ISentryLifecycleToken;
1312
import io.sentry.MeasurementUnit;
@@ -20,6 +19,7 @@
2019
import io.sentry.android.core.internal.util.AndroidThreadChecker;
2120
import io.sentry.android.core.performance.AppStartMetrics;
2221
import io.sentry.android.core.performance.TimeSpan;
22+
import io.sentry.internal.eventprocessor.SentryEventProcessor;
2323
import io.sentry.protocol.App;
2424
import io.sentry.protocol.MeasurementValue;
2525
import io.sentry.protocol.SentryId;
@@ -36,7 +36,7 @@
3636
import org.jetbrains.annotations.Nullable;
3737

3838
/** Event Processor responsible for adding Android metrics to transactions */
39-
final class PerformanceAndroidEventProcessor implements EventProcessor {
39+
final class PerformanceAndroidEventProcessor implements SentryEventProcessor {
4040

4141
private static final String APP_METRICS_ORIGIN = "auto.ui";
4242

‎sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import android.graphics.Bitmap;
99
import android.view.View;
1010
import io.sentry.Attachment;
11-
import io.sentry.EventProcessor;
1211
import io.sentry.Hint;
1312
import io.sentry.SentryEvent;
1413
import io.sentry.SentryLevel;
@@ -18,7 +17,9 @@
1817
import io.sentry.android.replay.util.MaskRenderer;
1918
import io.sentry.android.replay.util.ViewsKt;
2019
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode;
20+
import io.sentry.internal.eventprocessor.SentryEventProcessor;
2121
import io.sentry.protocol.SentryTransaction;
22+
import io.sentry.util.ExceptionUtils;
2223
import io.sentry.util.HintUtils;
2324
import io.sentry.util.Objects;
2425
import java.util.concurrent.CountDownLatch;
@@ -34,7 +35,7 @@
3435
* captured.
3536
*/
3637
@ApiStatus.Internal
37-
public final class ScreenshotEventProcessor implements EventProcessor {
38+
public final class ScreenshotEventProcessor implements SentryEventProcessor {
3839

3940
private final @NotNull SentryAndroidOptions options;
4041
private final @NotNull BuildInfoProvider buildInfoProvider;
@@ -112,7 +113,18 @@ private boolean isMaskingEnabled() {
112113
final @Nullable SentryAndroidOptions.BeforeCaptureCallback beforeCaptureCallback =
113114
options.getBeforeScreenshotCaptureCallback();
114115
if (beforeCaptureCallback != null) {
115-
if (!beforeCaptureCallback.execute(event, hint, shouldDebounce)) {
116+
try {
117+
if (!beforeCaptureCallback.execute(event, hint, shouldDebounce)) {
118+
return event;
119+
}
120+
} catch (Throwable e) {
121+
ExceptionUtils.rethrowIfFatal(e);
122+
options
123+
.getLogger()
124+
.log(
125+
SentryLevel.ERROR,
126+
"The beforeScreenshotCapture callback threw an exception. Skipping screenshot capture.",
127+
e);
116128
return event;
117129
}
118130
} else if (shouldDebounce) {

‎sentry-android-core/src/main/java/io/sentry/android/core/SentryUserFeedbackForm.java‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,37 @@ public class SentryUserFeedbackForm extends AlertDialog {
6262
final @Nullable SentryFeedbackOptions.OptionsConfigurator configurator) {
6363
super(context, themeResId);
6464
this.associatedEventId = associatedEventId;
65-
this.resolvedFeedbackOptions =
66-
new SentryFeedbackOptions(Sentry.getCurrentScopes().getOptions().getFeedbackOptions());
65+
final @NotNull SentryOptions options = Sentry.getCurrentScopes().getOptions();
66+
@NotNull
67+
SentryFeedbackOptions feedbackOptions = new SentryFeedbackOptions(options.getFeedbackOptions());
6768
if (configuration != null) {
68-
configuration.configure(context, resolvedFeedbackOptions);
69+
final @NotNull SentryFeedbackOptions configuredOptions =
70+
new SentryFeedbackOptions(feedbackOptions);
71+
try {
72+
configuration.configure(context, configuredOptions);
73+
feedbackOptions = configuredOptions;
74+
} catch (Throwable e) {
75+
ExceptionUtils.rethrowIfFatal(e);
76+
options
77+
.getLogger()
78+
.log(
79+
SentryLevel.ERROR, "Feedback OptionsConfiguration callback threw an exception.", e);
80+
}
6981
}
7082
if (configurator != null) {
71-
configurator.configure(resolvedFeedbackOptions);
83+
final @NotNull SentryFeedbackOptions configuredOptions =
84+
new SentryFeedbackOptions(feedbackOptions);
85+
try {
86+
configurator.configure(configuredOptions);
87+
feedbackOptions = configuredOptions;
88+
} catch (Throwable e) {
89+
ExceptionUtils.rethrowIfFatal(e);
90+
options
91+
.getLogger()
92+
.log(SentryLevel.ERROR, "Feedback OptionsConfigurator callback threw an exception.", e);
93+
}
7294
}
95+
this.resolvedFeedbackOptions = feedbackOptions;
7396
SentryIntegrationPackageStorage.getInstance().addIntegration("UserFeedbackWidget");
7497
maybeStartShakeDetection(context);
7598
}

‎sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import android.view.ViewGroup;
88
import android.view.Window;
99
import io.sentry.Attachment;
10-
import io.sentry.EventProcessor;
1110
import io.sentry.Hint;
1211
import io.sentry.ILogger;
1312
import io.sentry.ISerializer;
@@ -18,10 +17,12 @@
1817
import io.sentry.android.core.internal.util.AndroidThreadChecker;
1918
import io.sentry.android.core.internal.util.ClassUtil;
2019
import io.sentry.android.core.internal.util.Debouncer;
20+
import io.sentry.internal.eventprocessor.SentryEventProcessor;
2121
import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
2222
import io.sentry.protocol.SentryTransaction;
2323
import io.sentry.protocol.ViewHierarchy;
2424
import io.sentry.protocol.ViewHierarchyNode;
25+
import io.sentry.util.ExceptionUtils;
2526
import io.sentry.util.HintUtils;
2627
import io.sentry.util.JsonSerializationUtils;
2728
import io.sentry.util.Objects;
@@ -37,7 +38,7 @@
3738

3839
/** ViewHierarchyEventProcessor responsible for taking a snapshot of the current view hierarchy. */
3940
@ApiStatus.Internal
40-
public final class ViewHierarchyEventProcessor implements EventProcessor {
41+
public final class ViewHierarchyEventProcessor implements SentryEventProcessor {
4142

4243
private final @NotNull SentryAndroidOptions options;
4344
private final @NotNull Debouncer debouncer;
@@ -91,7 +92,18 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options)
9192
final @Nullable SentryAndroidOptions.BeforeCaptureCallback beforeCaptureCallback =
9293
options.getBeforeViewHierarchyCaptureCallback();
9394
if (beforeCaptureCallback != null) {
94-
if (!beforeCaptureCallback.execute(event, hint, shouldDebounce)) {
95+
try {
96+
if (!beforeCaptureCallback.execute(event, hint, shouldDebounce)) {
97+
return event;
98+
}
99+
} catch (Throwable e) {
100+
ExceptionUtils.rethrowIfFatal(e);
101+
options
102+
.getLogger()
103+
.log(
104+
SentryLevel.ERROR,
105+
"The beforeViewHierarchyCapture callback threw an exception. Skipping view hierarchy capture.",
106+
e);
95107
return event;
96108
}
97109
} else if (shouldDebounce) {

‎sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,30 @@ import androidx.compose.ui.text.style.TextOverflow
2929
import androidx.compose.ui.unit.dp
3030
import androidx.compose.ui.unit.sp
3131
import androidx.test.ext.junit.runners.AndroidJUnit4
32+
import com.google.common.truth.Truth.assertThat
3233
import io.sentry.Attachment
3334
import io.sentry.Hint
35+
import io.sentry.ILogger
3436
import io.sentry.MainEventProcessor
3537
import io.sentry.SentryEvent
3638
import io.sentry.SentryIntegrationPackageStorage
39+
import io.sentry.SentryLevel
3740
import io.sentry.TypeCheckHint.ANDROID_ACTIVITY
3841
import io.sentry.protocol.SentryException
3942
import io.sentry.util.thread.IThreadChecker
4043
import java.io.File
4144
import kotlin.test.BeforeTest
4245
import kotlin.test.Test
4346
import kotlin.test.assertEquals
47+
import kotlin.test.assertFailsWith
4448
import kotlin.test.assertFalse
4549
import kotlin.test.assertNotNull
4650
import kotlin.test.assertNull
4751
import kotlin.test.assertSame
4852
import kotlin.test.assertTrue
4953
import org.junit.runner.RunWith
5054
import org.mockito.kotlin.mock
55+
import org.mockito.kotlin.verify
5156
import org.mockito.kotlin.whenever
5257
import org.robolectric.Robolectric.buildActivity
5358
import org.robolectric.Shadows.shadowOf
@@ -310,11 +315,51 @@ class ScreenshotEventProcessorTest {
310315
assertNull(hint.screenshot)
311316
}
312317

318+
@Test
319+
fun `when capture callback throws a nonfatal throwable, skips screenshot and retains event`() {
320+
CurrentActivityHolder.getInstance().setActivity(fixture.activity)
321+
val logger = mock<ILogger>()
322+
fixture.options.isDebug = true
323+
fixture.options.setLogger(logger)
324+
val failure = AssertionError("callback failed")
325+
fixture.options.setBeforeScreenshotCaptureCallback { _, _, _ -> throw failure }
326+
val processor = fixture.getSut(true)
327+
val event = SentryEvent().apply { exceptions = listOf(SentryException()) }
328+
val hint = Hint()
329+
330+
assertThat(processor.process(event, hint)).isSameInstanceAs(event)
331+
assertThat(hint.screenshot).isNull()
332+
verify(logger)
333+
.log(
334+
SentryLevel.ERROR,
335+
"The beforeScreenshotCapture callback threw an exception. Skipping screenshot capture.",
336+
failure,
337+
)
338+
339+
fixture.options.setBeforeScreenshotCaptureCallback { _, _, _ -> true }
340+
val nextHint = Hint()
341+
assertThat(processor.process(event, nextHint)).isSameInstanceAs(event)
342+
assertThat(nextHint.screenshot).isNotNull()
343+
}
344+
345+
@Test
346+
fun `when capture callback throws a fatal error, it is not swallowed`() {
347+
CurrentActivityHolder.getInstance().setActivity(fixture.activity)
348+
val failure = OutOfMemoryError("callback failed")
349+
fixture.options.setBeforeScreenshotCaptureCallback { _, _, _ -> throw failure }
350+
val processor = fixture.getSut(true)
351+
val event = SentryEvent().apply { exceptions = listOf(SentryException()) }
352+
353+
val thrown = assertFailsWith<OutOfMemoryError> { processor.process(event, Hint()) }
354+
355+
assertThat(thrown).isSameInstanceAs(failure)
356+
}
357+
313358
@Test
314359
fun `when capture callback returns true, a screenshot should be captured`() {
315360
CurrentActivityHolder.getInstance().setActivity(fixture.activity)
316361

317-
fixture.options.setBeforeViewHierarchyCaptureCallback { _, _, _ -> true }
362+
fixture.options.setBeforeScreenshotCaptureCallback { _, _, _ -> true }
318363
val processor = fixture.getSut(true)
319364

320365
val event = SentryEvent().apply { exceptions = listOf(SentryException()) }

0 commit comments

Comments
 (0)