Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
468abd9
collection: callback error handling
adinauer Sep 22, 2026
28af8f2
fix(core): [Callback Errors 1] Report callback failures
adinauer Sep 22, 2026
d689526
fix(core): [Callback Errors 2] Drop failed processor data
adinauer Sep 22, 2026
fa3ca45
fix(core): [Callback Errors 3] Drop failed breadcrumbs
adinauer Sep 22, 2026
434e1db
ref(core): [Callback Errors 2] Mark SDK-owned event processors
adinauer Sep 24, 2026
8b3c3fa
Merge SDK processor marker into processor callback handling
adinauer Sep 24, 2026
aa17ea5
Merge processor stack update into breadcrumb callback handling
adinauer Sep 24, 2026
6d07426
fix(core): [Callback Errors 3] Preserve SDK processor data
adinauer Sep 24, 2026
8a06fde
chore(core): Sync processor fixes into breadcrumb stack
adinauer Sep 24, 2026
9320f73
fix(core): [Callback Errors 5] Handle tracesSampler failures
adinauer Sep 24, 2026
60d1e15
changelog
adinauer Sep 24, 2026
2c84527
fix(core): [Callback Errors 6] Handle profilesSampler failures
adinauer Sep 24, 2026
aa15164
changelog
adinauer Sep 24, 2026
49f8b9c
fix(core): [Callback Errors 7] Handle replay sampling failures
adinauer Sep 24, 2026
b62c094
changelog
adinauer Sep 24, 2026
efb5dfb
fix(core): [Callback Errors 8] Report dropped profiles
adinauer Sep 25, 2026
7a471df
changelog
adinauer Sep 25, 2026
8845736
fix(integrations): [Callback Errors 9] Guard custom callbacks
adinauer Sep 25, 2026
44ed7c6
changelog
adinauer Sep 25, 2026
fcbf95f
fix(core): Drop transactions when tracesSampler fails
adinauer Sep 25, 2026
154d94e
fix(core): Sync trace sampler handling into profile sampling
adinauer Sep 25, 2026
d11680e
chore: Sync sampler fixes into replay sampling branch
adinauer Sep 25, 2026
425a6b1
chore: Sync sampler fixes into profile accounting branch
adinauer Sep 25, 2026
835c0d2
chore: Sync sampler fixes into custom callback branch
adinauer Sep 25, 2026
2a38eeb
fix(integrations): Report beforeSpan callback losses
adinauer Sep 25, 2026
0414429
docs(core): Document SDK backfilling processor marker
adinauer Oct 5, 2026
a2b4847
fix: Report profiles dropped with transactions
adinauer Oct 8, 2026
9ef9efd
Merge pull request #6141 from getsentry/fix/callback-error-handling-d…
adinauer Oct 8, 2026
518d48a
fix(core): Report replay sampling callback errors
adinauer Oct 8, 2026
7fd012e
chore: Merge main into callback error stack
adinauer Oct 8, 2026
4916a15
chore: Merge callback error base into processor marker
adinauer Oct 8, 2026
38bf38c
chore: Merge processor marker into event processor handling
adinauer Oct 8, 2026
4d44b53
chore: Merge event processor handling into breadcrumbs
adinauer Oct 8, 2026
15cc695
chore: Merge breadcrumb handling into trace sampling
adinauer Oct 8, 2026
97cd7e2
chore: Merge trace sampling into profile sampling
adinauer Oct 8, 2026
287418b
chore: Merge profile sampling into replay sampling
adinauer Oct 8, 2026
92968ab
chore: Merge replay sampling into profile accounting
adinauer Oct 8, 2026
e87fddf
chore: Merge profile accounting into custom callbacks
adinauer Oct 8, 2026
0b922a6
Merge pull request #6162 from getsentry/fix/callback-error-handling-p…
adinauer Oct 8, 2026
dea8dbc
Merge pull request #6142 from getsentry/fix/callback-error-handling-e…
adinauer Oct 8, 2026
64c1c55
Merge pull request #6167 from getsentry/fix/callback-error-handling-c…
adinauer Oct 8, 2026
75338f5
Merge pull request #6143 from getsentry/fix/callback-error-handling-b…
adinauer Oct 8, 2026
b692e97
Merge pull request #6163 from getsentry/fix/callback-error-handling-t…
adinauer Oct 8, 2026
5b0d221
Merge pull request #6164 from getsentry/fix/callback-error-handling-p…
adinauer Oct 8, 2026
136073f
Merge pull request #6165 from getsentry/fix/callback-error-handling-r…
adinauer Oct 8, 2026
dfb67ed
Merge pull request #6166 from getsentry/fix/callback-error-handling-p…
adinauer Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@

### Fixes

- Fix SDK callback error handling ([#6140](https://github.com/getsentry/sentry-java/pull/6140))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 🚫 The changelog entry seems to be part of an already released section ## 8.59.0.
    Consider moving the entry to the ## Unreleased section, please.

- Add `DiscardReason.CALLBACK_ERROR` and use it for telemetry dropped when a `beforeSend*` callback throws. `OnDiscardCallback` can now receive this value.
- 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))
- 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 without a `callback_error` client report.
- Drop breadcrumbs when `beforeBreadcrumb` throws instead of storing exception details on the breadcrumb.
- Skip Android screenshot or view hierarchy capture when its capture callback throws, while retaining the error event ([#6167](https://github.com/getsentry/sentry-java/pull/6167))
- Drop spans when `beforeSpan` throws in OkHttp, OpenFeign, GraphQL, Ktor, or Apollo, without disrupting the request. Report lost sampled spans as `callback_error` in client reports and `OnDiscardCallback` ([#6167](https://github.com/getsentry/sentry-java/pull/6167))
- Skip replay capture when `beforeErrorSampling` throws, while still sending the error event ([#6165](https://github.com/getsentry/sentry-java/pull/6165))
- When `tracesSampler` throws, drop the transaction and record `callback_error` instead of inheriting the parent sampling decision or falling back to `tracesSampleRate` ([#6163](https://github.com/getsentry/sentry-java/pull/6163))
- When `profilesSampler` throws, disable profiling instead of falling back to `profilesSampleRate` or inheriting the parent's profiling decision. Trace sampling is unchanged ([#6164](https://github.com/getsentry/sentry-java/pull/6164))
- Disable URL caching when reading `META-INF/MANIFEST.MF` files during version detection so that the SDK no longer keeps jar file handles open for the life of the process ([#6124](https://github.com/getsentry/sentry-java/pull/6124)
- Keep the `EventListener` wrapped by `SentryOkHttpEventListener` per `Call` ([#6003](https://github.com/getsentry/sentry-java/pull/6003))

Expand Down
6 changes: 3 additions & 3 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ public final class io/sentry/android/core/AppState$LifecycleObserver : androidx/
public fun onStop (Landroidx/lifecycle/LifecycleOwner;)V
}

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

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

public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentry/EventProcessor {
public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentry/internal/eventprocessor/SentryEventProcessor {
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
public fun getOrder ()Ljava/lang/Long;
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import io.sentry.hints.AbnormalExit;
import io.sentry.hints.Backfillable;
import io.sentry.hints.NativeCrashExit;
import io.sentry.internal.eventprocessor.SentryEventProcessor;
import io.sentry.protocol.App;
import io.sentry.protocol.Contexts;
import io.sentry.protocol.DebugImage;
Expand Down Expand Up @@ -89,7 +90,8 @@
*/
@ApiStatus.Internal
@WorkerThread
public final class ApplicationExitInfoEventProcessor implements BackfillingEventProcessor {
public final class ApplicationExitInfoEventProcessor
implements BackfillingEventProcessor, SentryEventProcessor {

private final @NotNull Context context;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.sentry.android.core.internal.util.AndroidThreadChecker;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.core.performance.TimeSpan;
import io.sentry.internal.eventprocessor.SentryEventProcessor;
import io.sentry.protocol.App;
import io.sentry.protocol.OperatingSystem;
import io.sentry.protocol.SentryException;
Expand All @@ -32,7 +33,7 @@
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

final class DefaultAndroidEventProcessor implements EventProcessor {
final class DefaultAndroidEventProcessor implements SentryEventProcessor {

@TestOnly final Context context;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import static io.sentry.android.core.ActivityLifecycleIntegration.STANDALONE_APP_START_OP;
import static io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP;

import io.sentry.EventProcessor;
import io.sentry.Hint;
import io.sentry.ISentryLifecycleToken;
import io.sentry.MeasurementUnit;
Expand All @@ -20,6 +19,7 @@
import io.sentry.android.core.internal.util.AndroidThreadChecker;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.core.performance.TimeSpan;
import io.sentry.internal.eventprocessor.SentryEventProcessor;
import io.sentry.protocol.App;
import io.sentry.protocol.MeasurementValue;
import io.sentry.protocol.SentryId;
Expand All @@ -36,7 +36,7 @@
import org.jetbrains.annotations.Nullable;

/** Event Processor responsible for adding Android metrics to transactions */
final class PerformanceAndroidEventProcessor implements EventProcessor {
final class PerformanceAndroidEventProcessor implements SentryEventProcessor {

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import android.graphics.Bitmap;
import android.view.View;
import io.sentry.Attachment;
import io.sentry.EventProcessor;
import io.sentry.Hint;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
Expand All @@ -18,6 +17,7 @@
import io.sentry.android.replay.util.MaskRenderer;
import io.sentry.android.replay.util.ViewsKt;
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode;
import io.sentry.internal.eventprocessor.SentryEventProcessor;
import io.sentry.protocol.SentryTransaction;
import io.sentry.util.HintUtils;
import io.sentry.util.Objects;
Expand All @@ -34,7 +34,7 @@
* captured.
*/
@ApiStatus.Internal
public final class ScreenshotEventProcessor implements EventProcessor {
public final class ScreenshotEventProcessor implements SentryEventProcessor {

private final @NotNull SentryAndroidOptions options;
private final @NotNull BuildInfoProvider buildInfoProvider;
Expand Down Expand Up @@ -112,7 +112,17 @@ private boolean isMaskingEnabled() {
final @Nullable SentryAndroidOptions.BeforeCaptureCallback beforeCaptureCallback =
options.getBeforeScreenshotCaptureCallback();
if (beforeCaptureCallback != null) {
if (!beforeCaptureCallback.execute(event, hint, shouldDebounce)) {
try {
if (!beforeCaptureCallback.execute(event, hint, shouldDebounce)) {
return event;
}
} catch (Exception e) {
options
.getLogger()
.log(
SentryLevel.ERROR,
"The beforeScreenshotCapture callback threw an exception. Skipping screenshot capture.",
e);
return event;
}
} else if (shouldDebounce) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import android.view.ViewGroup;
import android.view.Window;
import io.sentry.Attachment;
import io.sentry.EventProcessor;
import io.sentry.Hint;
import io.sentry.ILogger;
import io.sentry.ISerializer;
Expand All @@ -18,6 +17,7 @@
import io.sentry.android.core.internal.util.AndroidThreadChecker;
import io.sentry.android.core.internal.util.ClassUtil;
import io.sentry.android.core.internal.util.Debouncer;
import io.sentry.internal.eventprocessor.SentryEventProcessor;
import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
import io.sentry.protocol.SentryTransaction;
import io.sentry.protocol.ViewHierarchy;
Expand All @@ -37,7 +37,7 @@

/** ViewHierarchyEventProcessor responsible for taking a snapshot of the current view hierarchy. */
@ApiStatus.Internal
public final class ViewHierarchyEventProcessor implements EventProcessor {
public final class ViewHierarchyEventProcessor implements SentryEventProcessor {

private final @NotNull SentryAndroidOptions options;
private final @NotNull Debouncer debouncer;
Expand Down Expand Up @@ -91,7 +91,17 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options)
final @Nullable SentryAndroidOptions.BeforeCaptureCallback beforeCaptureCallback =
options.getBeforeViewHierarchyCaptureCallback();
if (beforeCaptureCallback != null) {
if (!beforeCaptureCallback.execute(event, hint, shouldDebounce)) {
try {
if (!beforeCaptureCallback.execute(event, hint, shouldDebounce)) {
return event;
}
} catch (Exception e) {
options
.getLogger()
.log(
SentryLevel.ERROR,
"The beforeViewHierarchyCapture callback threw an exception. Skipping view hierarchy capture.",
e);
return event;
}
} else if (shouldDebounce) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import io.sentry.Attachment
import io.sentry.Hint
import io.sentry.ILogger
import io.sentry.MainEventProcessor
import io.sentry.SentryEvent
import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SentryLevel
import io.sentry.TypeCheckHint.ANDROID_ACTIVITY
import io.sentry.protocol.SentryException
import io.sentry.util.thread.IThreadChecker
Expand All @@ -48,6 +51,7 @@ import kotlin.test.assertSame
import kotlin.test.assertTrue
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.robolectric.Robolectric.buildActivity
import org.robolectric.Shadows.shadowOf
Expand Down Expand Up @@ -310,11 +314,38 @@ class ScreenshotEventProcessorTest {
assertNull(hint.screenshot)
}

@Test
fun `when capture callback throws, skips screenshot and retains event`() {
CurrentActivityHolder.getInstance().setActivity(fixture.activity)
val logger = mock<ILogger>()
fixture.options.isDebug = true
fixture.options.setLogger(logger)
val failure = IllegalStateException("callback failed")
fixture.options.setBeforeScreenshotCaptureCallback { _, _, _ -> throw failure }
val processor = fixture.getSut(true)
val event = SentryEvent().apply { exceptions = listOf(SentryException()) }
val hint = Hint()

assertThat(processor.process(event, hint)).isSameInstanceAs(event)
assertThat(hint.screenshot).isNull()
verify(logger)
.log(
SentryLevel.ERROR,
"The beforeScreenshotCapture callback threw an exception. Skipping screenshot capture.",
failure,
)

fixture.options.setBeforeScreenshotCaptureCallback { _, _, _ -> true }
val nextHint = Hint()
assertThat(processor.process(event, nextHint)).isSameInstanceAs(event)
assertThat(nextHint.screenshot).isNotNull()
}

@Test
fun `when capture callback returns true, a screenshot should be captured`() {
CurrentActivityHolder.getInstance().setActivity(fixture.activity)

fixture.options.setBeforeViewHierarchyCaptureCallback { _, _, _ -> true }
fixture.options.setBeforeScreenshotCaptureCallback { _, _, _ -> true }
val processor = fixture.getSut(true)

val event = SentryEvent().apply { exceptions = listOf(SentryException()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import android.view.View
import android.view.ViewGroup
import android.view.Window
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import io.sentry.Hint
import io.sentry.JsonSerializable
import io.sentry.JsonSerializer
import io.sentry.SentryEvent
import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SentryLevel
import io.sentry.TypeCheckHint
import io.sentry.protocol.SentryException
import io.sentry.util.thread.IThreadChecker
Expand Down Expand Up @@ -342,6 +344,31 @@ class ViewHierarchyEventProcessorTest {
assertNull(hint.viewHierarchy)
}

@Test
fun `when capture callback throws, skips view hierarchy and retains event`() {
fixture.options.isDebug = true
fixture.options.setLogger(fixture.logger)
val failure = IllegalStateException("callback failed")
fixture.options.setBeforeViewHierarchyCaptureCallback { _, _, _ -> throw failure }
val processor = fixture.getSut(true)
val event = SentryEvent().apply { exceptions = listOf(SentryException()) }
val hint = Hint()

assertThat(processor.process(event, hint)).isSameInstanceAs(event)
assertThat(hint.viewHierarchy).isNull()
verify(fixture.logger)
.log(
SentryLevel.ERROR,
"The beforeViewHierarchyCapture callback threw an exception. Skipping view hierarchy capture.",
failure,
)

fixture.options.setBeforeViewHierarchyCaptureCallback { _, _, _ -> true }
val nextHint = Hint()
assertThat(processor.process(event, nextHint)).isSameInstanceAs(event)
assertThat(nextHint.viewHierarchy).isNotNull()
}

@Test
fun `when capture callback returns true, a view hierarchy should be captured`() {
fixture.options.setBeforeViewHierarchyCaptureCallback { _, _, _ -> true }
Expand Down
1 change: 1 addition & 0 deletions sentry-apollo-3/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies {
testImplementation(kotlin(Config.kotlinStdLib))
testImplementation(libs.apollo3.kotlin)
testImplementation(libs.kotlin.test.junit)
testImplementation(libs.google.truth)
testImplementation(libs.kotlinx.coroutines)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.mockito.inline)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.apollographql.apollo3.network.http.HttpInterceptor
import com.apollographql.apollo3.network.http.HttpInterceptorChain
import io.sentry.BaggageHeader
import io.sentry.Breadcrumb
import io.sentry.DataCategory
import io.sentry.Hint
import io.sentry.IScopes
import io.sentry.ISpan
Expand All @@ -23,6 +24,7 @@ import io.sentry.SpanDataConvention.HTTP_METHOD_KEY
import io.sentry.SpanStatus
import io.sentry.TypeCheckHint.APOLLO_REQUEST
import io.sentry.TypeCheckHint.APOLLO_RESPONSE
import io.sentry.clientreport.DiscardReason
import io.sentry.exception.ExceptionMechanismException
import io.sentry.protocol.Mechanism
import io.sentry.protocol.Request
Expand Down Expand Up @@ -216,13 +218,21 @@ constructor(
span.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, it)
}
if (beforeSpan != null) {
val wasSampled = span.isSampled == true
try {
val result = beforeSpan.execute(span, request, response)
if (result == null) {
// Span is dropped
span.spanContext.sampled = false
}
} catch (e: Throwable) {
span.spanContext.sampled = false
if (wasSampled) {
scopes.options.clientReportRecorder.recordLostEvent(
DiscardReason.CALLBACK_ERROR,
DataCategory.Span,
)
}
scopes.options.logger.log(
SentryLevel.ERROR,
"An error occurred while executing beforeSpan on ApolloInterceptor",
Expand Down
Loading
Loading