Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- Skip replay capture when `beforeErrorSampling` throws while retaining the error event ([#6165](https://github.com/getsentry/sentry-java/pull/6165)).
- 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)).
- Keep the Android feedback form available when its options configurator throws ([#6239](https://github.com/getsentry/sentry-java/pull/6239)).
- Isolate nonfatal callback throwables while continuing to propagate fatal JVM errors ([#6251](https://github.com/getsentry/sentry-java/pull/6251)).

### Dependencies

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode;
import io.sentry.internal.eventprocessor.SentryEventProcessor;
import io.sentry.protocol.SentryTransaction;
import io.sentry.util.ExceptionUtils;
import io.sentry.util.HintUtils;
import io.sentry.util.Objects;
import java.util.concurrent.CountDownLatch;
Expand Down Expand Up @@ -116,7 +117,8 @@ private boolean isMaskingEnabled() {
if (!beforeCaptureCallback.execute(event, hint, shouldDebounce)) {
return event;
}
} catch (Exception e) {
} catch (Throwable e) {
ExceptionUtils.rethrowIfFatal(e);
options
.getLogger()
.log(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.sentry.protocol.SentryTransaction;
import io.sentry.protocol.ViewHierarchy;
import io.sentry.protocol.ViewHierarchyNode;
import io.sentry.util.ExceptionUtils;
import io.sentry.util.HintUtils;
import io.sentry.util.JsonSerializationUtils;
import io.sentry.util.Objects;
Expand Down Expand Up @@ -95,7 +96,8 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options)
if (!beforeCaptureCallback.execute(event, hint, shouldDebounce)) {
return event;
}
} catch (Exception e) {
} catch (Throwable e) {
ExceptionUtils.rethrowIfFatal(e);
options
.getLogger()
.log(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import java.io.File
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
Expand Down Expand Up @@ -315,12 +316,12 @@ class ScreenshotEventProcessorTest {
}

@Test
fun `when capture callback throws, skips screenshot and retains event`() {
fun `when capture callback throws a nonfatal throwable, 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")
val failure = AssertionError("callback failed")
fixture.options.setBeforeScreenshotCaptureCallback { _, _, _ -> throw failure }
val processor = fixture.getSut(true)
val event = SentryEvent().apply { exceptions = listOf(SentryException()) }
Expand All @@ -341,6 +342,19 @@ class ScreenshotEventProcessorTest {
assertThat(nextHint.screenshot).isNotNull()
}

@Test
fun `when capture callback throws a fatal error, it is not swallowed`() {
CurrentActivityHolder.getInstance().setActivity(fixture.activity)
val failure = OutOfMemoryError("callback failed")
fixture.options.setBeforeScreenshotCaptureCallback { _, _, _ -> throw failure }
val processor = fixture.getSut(true)
val event = SentryEvent().apply { exceptions = listOf(SentryException()) }

val thrown = assertFailsWith<OutOfMemoryError> { processor.process(event, Hint()) }

assertThat(thrown).isSameInstanceAs(failure)
}

@Test
fun `when capture callback returns true, a screenshot should be captured`() {
CurrentActivityHolder.getInstance().setActivity(fixture.activity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import java.io.Writer
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
Expand Down Expand Up @@ -345,10 +346,10 @@ class ViewHierarchyEventProcessorTest {
}

@Test
fun `when capture callback throws, skips view hierarchy and retains event`() {
fun `when capture callback throws a nonfatal throwable, skips view hierarchy and retains event`() {
fixture.options.isDebug = true
fixture.options.setLogger(fixture.logger)
val failure = IllegalStateException("callback failed")
val failure = AssertionError("callback failed")
fixture.options.setBeforeViewHierarchyCaptureCallback { _, _, _ -> throw failure }
val processor = fixture.getSut(true)
val event = SentryEvent().apply { exceptions = listOf(SentryException()) }
Expand All @@ -369,6 +370,18 @@ class ViewHierarchyEventProcessorTest {
assertThat(nextHint.viewHierarchy).isNotNull()
}

@Test
fun `when capture callback throws a fatal error, it is not swallowed`() {
val failure = OutOfMemoryError("callback failed")
fixture.options.setBeforeViewHierarchyCaptureCallback { _, _, _ -> throw failure }
val processor = fixture.getSut(true)
val event = SentryEvent().apply { exceptions = listOf(SentryException()) }

val thrown = assertFailsWith<OutOfMemoryError> { processor.process(event, Hint()) }

assertThat(thrown).isSameInstanceAs(failure)
}

@Test
fun `when capture callback returns true, a view hierarchy should be captured`() {
fixture.options.setBeforeViewHierarchyCaptureCallback { _, _, _ -> true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ import io.sentry.graphql.SentryGraphqlInstrumentation
import io.sentry.graphql.SentrySubscriptionHandler
import java.lang.RuntimeException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
Expand Down Expand Up @@ -210,8 +212,8 @@ class SentryInstrumentationTest {
}

@Test
fun `when beforeSpan throws, drops span and preserves result`() {
val failure = IllegalStateException("callback failed")
fun `when beforeSpan throws a nonfatal throwable, drops span and preserves result`() {
val failure = AssertionError("callback failed")
val logger = mock<ILogger>()
val sut =
fixture.getSut(
Expand Down Expand Up @@ -239,6 +241,18 @@ class SentryInstrumentationTest {
}
}

@Test
fun `fatal error from beforeSpan is not swallowed`() {
val failure = OutOfMemoryError("callback failed")
val sut = fixture.getSut(beforeSpan = { _, _, _ -> throw failure })

withMockScopes {
val thrown = assertFailsWith<CompletionException> { sut.execute("{ shows { id } }") }

assertThat(thrown.cause).isSameInstanceAs(failure)
}
}

@Test
fun `when beforeSpan throws, drops async span and preserves result`() {
val sut =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.sentry.SpanStatus;
import io.sentry.TypeCheckHint;
import io.sentry.clientreport.DiscardReason;
import io.sentry.util.ExceptionUtils;
import io.sentry.util.StringUtils;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -290,7 +291,8 @@ private void finish(
ISpan newSpan = span;
try {
newSpan = beforeSpan.execute(span, environment, result);
} catch (Exception e) {
} catch (Throwable e) {
ExceptionUtils.rethrowIfFatal(e);
span.getSpanContext().setSampled(false);
if (wasSampled) {
scopesFromContext(environment.getGraphQlContext())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ import io.sentry.TransactionContext
import io.sentry.clientreport.DiscardReason
import java.lang.RuntimeException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
Expand Down Expand Up @@ -206,8 +208,8 @@ class SentryInstrumentationTest {
}

@Test
fun `when beforeSpan throws, drops span and preserves result`() {
val failure = IllegalStateException("callback failed")
fun `when beforeSpan throws a nonfatal throwable, drops span and preserves result`() {
val failure = AssertionError("callback failed")
val logger = mock<ILogger>()
val sut =
fixture.getSut(
Expand Down Expand Up @@ -235,6 +237,18 @@ class SentryInstrumentationTest {
}
}

@Test
fun `fatal error from beforeSpan is not swallowed`() {
val failure = OutOfMemoryError("callback failed")
val sut = fixture.getSut(beforeSpan = { _, _, _ -> throw failure })

withMockScopes {
val thrown = assertFailsWith<CompletionException> { sut.execute("{ shows { id } }") }

assertThat(thrown.cause).isSameInstanceAs(failure)
}
}

@Test
fun `when beforeSpan throws, drops async span and preserves result`() {
val sut =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import io.sentry.SentryOptions
import io.sentry.SpanStatus
import io.sentry.clientreport.DiscardReason
import io.sentry.kotlin.SentryContext
import io.sentry.util.ExceptionUtils
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
import io.sentry.util.Platform
import io.sentry.util.PropagationTargetsUtils
Expand Down Expand Up @@ -193,7 +194,8 @@ public val SentryKtorClientPlugin: ClientPlugin<SentryKtorClientPluginConfig> =
result =
try {
beforeSpan.execute(span, request)
} catch (e: Exception) {
} catch (@Suppress("TooGenericExceptionCaught") e: Throwable) {
ExceptionUtils.rethrowIfFatal(e)
span.spanContext.sampled = false
if (wasSampled) {
(if (forceScopes) scopes else Sentry.getCurrentScopes())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import java.util.concurrent.TimeUnit
import kotlin.Unit
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
Expand Down Expand Up @@ -451,7 +452,7 @@ class SentryKtorClientPluginTest {
fixture.getSut(
beforeSpan = { span, _ ->
span.spanContext.sampled = false
throw IllegalStateException("callback failed")
error("callback failed")
}
)
fixture.sentryTracer.spanContext.sampled = sampled
Expand All @@ -469,7 +470,7 @@ class SentryKtorClientPluginTest {

@Test
fun `when beforeSpan throws, drops span and preserves response`(): Unit = runBlocking {
val failure = IllegalStateException("callback failed")
val failure = AssertionError("callback failed")
val logger = mock<ILogger>()
val sut =
fixture.getSut(
Expand Down Expand Up @@ -498,6 +499,19 @@ class SentryKtorClientPluginTest {
)
}

@Test
fun `fatal error from beforeSpan is not swallowed`(): Unit = runBlocking {
val failure = OutOfMemoryError("callback failed")
val sut = fixture.getSut(beforeSpan = { _, _ -> throw failure })

val thrown =
assertFailsWith<OutOfMemoryError> {
sut.use { sut.get(fixture.server.url("/hello").toString()) }
}

assertThat(generateSequence<Throwable>(thrown) { it.cause }.last()).isSameInstanceAs(failure)
}

@Test
fun `beforeSpan can drop span`(): Unit = runBlocking {
val sut = fixture.getSut(beforeSpan = { _, _ -> null })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import io.sentry.TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS
import io.sentry.clientreport.DiscardReason
import io.sentry.okhttp.SentryOkHttpInterceptor.BeforeSpanCallback
import io.sentry.transport.CurrentDateProvider
import io.sentry.util.ExceptionUtils
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
import io.sentry.util.Platform
import io.sentry.util.PropagationTargetsUtils
Expand Down Expand Up @@ -375,7 +376,8 @@ public open class SentryOkHttpInterceptor(
val result =
try {
beforeSpan.execute(span, request, response)
} catch (e: Exception) {
} catch (@Suppress("TooGenericExceptionCaught") e: Throwable) {
ExceptionUtils.rethrowIfFatal(e)
span.spanContext.sampled = false
if (wasSampled) {
scopes.options.clientReportRecorder.recordLostEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever

@Suppress("LargeClass")
class SentryOkHttpInterceptorTest {
class Fixture {
val scopes = mock<IScopes>()
Expand Down Expand Up @@ -463,7 +464,7 @@ class SentryOkHttpInterceptorTest {
fixture.getSut(
beforeSpan = { span, _, _ ->
span.spanContext.sampled = false
throw IllegalStateException("callback failed")
error("callback failed")
}
)
fixture.sentryTracer.spanContext.sampled = sampled
Expand All @@ -480,8 +481,8 @@ class SentryOkHttpInterceptorTest {
}

@Test
fun `when beforeSpan throws, drops span and preserves response`() {
val failure = IllegalStateException("callback failed")
fun `when beforeSpan throws a nonfatal throwable, drops span and preserves response`() {
val failure = AssertionError("callback failed")
val logger = mock<ILogger>()
val sut =
fixture.getSut(
Expand Down Expand Up @@ -509,6 +510,16 @@ class SentryOkHttpInterceptorTest {
)
}

@Test
fun `fatal error from beforeSpan is not swallowed`() {
val failure = OutOfMemoryError("callback failed")
val sut = fixture.getSut(beforeSpan = { _, _, _ -> throw failure })

val thrown = assertFailsWith<OutOfMemoryError> { sut.newCall(getRequest()).execute() }

assertThat(thrown).isSameInstanceAs(failure)
}

@Test
fun `when beforeSpan throws, event listener still finishes span`() {
val sut =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.sentry.SpanStatus;
import io.sentry.W3CTraceparentHeader;
import io.sentry.clientreport.DiscardReason;
import io.sentry.util.ExceptionUtils;
import io.sentry.util.Objects;
import io.sentry.util.SpanUtils;
import io.sentry.util.TracingUtils;
Expand Down Expand Up @@ -102,7 +103,8 @@ public Response execute(final @NotNull Request request, final @NotNull Request.O
ISpan result = span;
try {
result = beforeSpan.execute(span, request, response);
} catch (Exception e) {
} catch (Throwable e) {
ExceptionUtils.rethrowIfFatal(e);
span.getSpanContext().setSampled(false);
if (wasSampled) {
scopes
Expand Down
Loading
Loading