diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d0cec6fc1..c5193dc883f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,9 @@ - On a busy device during cold start this could stall the launcher activity's `onCreate` for hundreds of milliseconds. - Do not inspect the response body of streaming Apollo 3 and 4 responses (`multipart/mixed`, `text/event-stream`) for failed requests, which blocked `@defer` and `@stream` ([#6432](https://github.com/getsentry/sentry-java/pull/6432)) - Set Apollo 3, 4, and 5 HTTP spans to `CANCELLED` instead of `INTERNAL_ERROR` when the request is cancelled ([#6284](https://github.com/getsentry/sentry-java/pull/6284)) +- Stop swallowing fatal errors such as `OutOfMemoryError` in the SDK code that loads native libraries and optional dependencies ([#6562](https://github.com/getsentry/sentry-java/pull/6562)) + - The offline cache no longer deletes its queue file when it runs out of memory while reading it. + - The Ktor client integration no longer swallows coroutine cancellation while reading the response body of a failed request. ### Improvements diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java index 74522f7aaac..228a5909ae8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java @@ -112,7 +112,8 @@ private void ensureLifecycleObserver(final @NotNull ILogger logger) { SentryLevel.WARNING, "androidx.lifecycle is not available, some features might not be properly working," + "e.g. Session Tracking, Network and System Events breadcrumbs, etc."); - } catch (Throwable e) { + } catch (LinkageError e) { + // androidx.lifecycle has an incompatible version logger.log(SentryLevel.ERROR, "AppState could not register lifecycle observer", e); } } @@ -124,7 +125,7 @@ private void addObserverInternal(final @NotNull ILogger logger) { if (observerRef != null) { ProcessLifecycleOwner.get().getLifecycle().addObserver(observerRef); } - } catch (Throwable e) { + } catch (LinkageError e) { // This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in // connection with conflicting dependencies of the androidx.lifecycle. // //See the issue here: https://github.com/getsentry/sentry-java/pull/2228 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java b/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java index ba08e71342a..8f85a1ef4f1 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java @@ -44,7 +44,7 @@ public static String id(final @NotNull Context context) throws RuntimeException return deviceId; } deviceId = readInstallationFile(installation); - } catch (Throwable e) { + } catch (IOException e) { throw new RuntimeException(e); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java index 8353a32d65d..7bac0d63a68 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java @@ -62,7 +62,9 @@ public final void register(final @NotNull IScopes scopes, final @NotNull SentryO this.options .getLogger() .log(SentryLevel.ERROR, "Failed to invoke the SentryNdk.init method.", e); - } catch (Throwable e) { + } catch (ReflectiveOperationException e) { + // InvocationTargetException wraps whatever SentryNdk.init threw, such as an + // UnsatisfiedLinkError when the native library failed to load disableNdkIntegration(this.options); this.options.getLogger().log(SentryLevel.ERROR, "Failed to initialize SentryNdk.", e); } @@ -94,7 +96,7 @@ public void close() throws IOException { options .getLogger() .log(SentryLevel.ERROR, "Failed to invoke the SentryNdk.close method.", e); - } catch (Throwable e) { + } catch (ReflectiveOperationException e) { options.getLogger().log(SentryLevel.ERROR, "Failed to close SentryNdk.", e); } finally { disableNdkIntegration(this.options); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java index ffd3323e601..096bd26858e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java @@ -196,7 +196,14 @@ public static ViewHierarchy snapshotViewHierarchy( try { viewHierarchy.set(snapshotViewHierarchy(decorView, exporters)); latch.countDown(); + } catch (LinkageError e) { + // The Compose exporter calls Compose internals, which may not match the app's + // Compose version + logger.log(SentryLevel.ERROR, "Failed to process view hierarchy.", e); } catch (Throwable t) { + // The snapshot calls into the app's own views, so we can't know what it throws. + // The event is sent without a view hierarchy. + ExceptionUtils.rethrowIfFatal(t); logger.log(SentryLevel.ERROR, "Failed to process view hierarchy.", t); } }); @@ -204,7 +211,14 @@ public static ViewHierarchy snapshotViewHierarchy( return viewHierarchy.get(); } } + } catch (LinkageError e) { + // The Compose exporter calls Compose internals, which may not match the app's Compose + // version + logger.log(SentryLevel.ERROR, "Failed to process view hierarchy.", e); } catch (Throwable t) { + // The snapshot calls into the app's own views, so we can't know what it throws. The event + // is sent without a view hierarchy. + ExceptionUtils.rethrowIfFatal(t); logger.log(SentryLevel.ERROR, "Failed to process view hierarchy.", t); } return null; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java index 612eb97946e..7a3e6b42302 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java @@ -6,6 +6,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.SpanStatus; +import io.sentry.util.ExceptionUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -56,7 +57,16 @@ public boolean dispatchTouchEvent(final @Nullable MotionEvent motionEvent) { final MotionEvent copy = motionEventObtainer.obtain(motionEvent); try { handleTouchEvent(copy); + } catch (LinkageError e) { + // The Compose gesture locator calls Compose internals, which may not match the app's + // Compose version + if (options != null) { + options.getLogger().log(SentryLevel.ERROR, "Error dispatching touch event", e); + } } catch (Throwable e) { + // Locating the touched view calls into the app's own views, so we can't know what it + // throws. The gesture is not recorded, and the touch still reaches the app. + ExceptionUtils.rethrowIfFatal(e); if (options != null) { options.getLogger().log(SentryLevel.ERROR, "Error dispatching touch event", e); } diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java index 602981c1d01..af817abe722 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java @@ -8,6 +8,7 @@ import io.sentry.ndk.NativeModuleListLoader; import io.sentry.protocol.DebugImage; import io.sentry.util.AutoClosableReentrantLock; +import io.sentry.util.ExceptionUtils; import io.sentry.util.Objects; import java.util.ArrayList; import java.util.HashSet; @@ -71,7 +72,13 @@ public DebugImagesLoader( .getLogger() .log(SentryLevel.DEBUG, "Debug images loaded: %d", debugImages.size()); } + } catch (LinkageError e) { + // e.g. UnsatisfiedLinkError when the native library failed to load + options.getLogger().log(SentryLevel.ERROR, e, "Failed to load debug images."); } catch (Throwable e) { + // A JNI call into sentry-native, so we can't know what it throws. The event is sent + // without debug images. + ExceptionUtils.rethrowIfFatal(e); options.getLogger().log(SentryLevel.ERROR, e, "Failed to load debug images."); } } @@ -172,7 +179,13 @@ public void clearDebugImages() { moduleListLoader.clearModuleList(); options.getLogger().log(SentryLevel.INFO, "Debug images cleared."); + } catch (LinkageError e) { + // e.g. UnsatisfiedLinkError when the native library failed to load + options.getLogger().log(SentryLevel.ERROR, e, "Failed to clear debug images."); } catch (Throwable e) { + // A JNI call into sentry-native, so we can't know what it throws. The cached list is + // cleared either way. + ExceptionUtils.rethrowIfFatal(e); options.getLogger().log(SentryLevel.ERROR, e, "Failed to clear debug images."); } debugImages = null; diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java index fae652f7a64..c182ba6ca7e 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java @@ -24,9 +24,9 @@ private SentryNdk() {} try { //noinspection UnstableApiUsage io.sentry.ndk.SentryNdk.loadNativeLibraries(); - } catch (Throwable t) { - // ignored - // if loadLibrary() fails, the later init() will throw an exception anyway + } catch (LinkageError ignored) { + // e.g. UnsatisfiedLinkError. The later init() throws too, which disables the NDK + // integration. } finally { loadLibraryLatch.countDown(); } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt index 4f8f6cabf58..e1586738935 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt @@ -53,7 +53,7 @@ internal object WindowSpy { lazy(NONE) { try { Class.forName("com.android.internal.policy.DecorView") - } catch (ignored: Throwable) { + } catch (ignored: ClassNotFoundException) { Log.d("WindowSpy", "Unexpected exception loading DecorView on API $SDK_INT", ignored) null } @@ -171,7 +171,7 @@ internal object WindowManagerSpy { val className = "android.view.WindowManagerGlobal" try { Class.forName(className) - } catch (ignored: Throwable) { + } catch (ignored: ClassNotFoundException) { Log.w("WindowManagerSpy", ignored) null } @@ -198,7 +198,10 @@ internal object WindowManagerSpy { mViewsField[windowManagerInstance] = swap(mViews) } } - } catch (ignored: Throwable) { + } catch (ignored: ReflectiveOperationException) { + Log.w("WindowManagerSpy", ignored) + } catch (ignored: ClassCastException) { + // mViews is not an ArrayList on this device Log.w("WindowManagerSpy", ignored) } } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt index 704260cf311..66372e7ba77 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.findRootCoordinates import androidx.compose.ui.node.LayoutNode import androidx.compose.ui.text.TextLayoutResult +import io.sentry.util.ExceptionUtils import kotlin.math.ceil import kotlin.math.floor import kotlin.math.roundToInt @@ -93,7 +94,7 @@ internal fun LayoutNode.findPainter(): Painter? { return try { modifier::class.java.getDeclaredField("painter").apply { isAccessible = true }.get(modifier) as? Painter - } catch (e: Throwable) { + } catch (_: ReflectiveOperationException) { null } } @@ -136,7 +137,12 @@ internal fun LayoutNode.findTextColor(): Color? { (modifier::class.java.getDeclaredField("color").apply { isAccessible = true }.get(modifier) as? ColorProducer) ?.invoke() + } catch (_: LinkageError) { + // older Compose versions don't have ColorProducer + null } catch (e: Throwable) { + // ColorProducer.invoke() runs the app's color lambda, so we can't know what it throws + ExceptionUtils.rethrowIfFatal(e) null } } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt index 2e40144e2de..cf8b8c69eac 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt @@ -32,6 +32,7 @@ import io.sentry.android.replay.util.toRect import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode +import io.sentry.util.ExceptionUtils import java.lang.ref.WeakReference import java.lang.reflect.Method @@ -45,7 +46,7 @@ internal object ComposeViewHierarchyNode { .java .getDeclaredMethod("getCollapsedSemantics\$ui_release") .apply { isAccessible = true } - } catch (_: Throwable) { + } catch (_: NoSuchMethodException) { // ignore, as this method may not be available } return@lazy null @@ -57,7 +58,7 @@ internal object ComposeViewHierarchyNode { internal fun retrieveSemanticsConfiguration(node: LayoutNode): SemanticsConfiguration? { return try { node.semanticsConfiguration - } catch (t: Throwable) { + } catch (t: NoSuchMethodError) { // for backwards compatibility // Jetpack Compose 1.8 or older if (getCollapsedSemanticsMethod != null) { @@ -128,16 +129,27 @@ internal object ComposeViewHierarchyNode { } val visibleRect = node.coordinates.boundsInWindow(_rootCoordinates?.get()) - val semantics: SemanticsConfiguration? + var semanticsFailure: Throwable? = null + val semantics: SemanticsConfiguration? = + try { + retrieveSemanticsConfiguration(node) + } catch (e: LinkageError) { + // an unsupported Compose version, which must not crash the app + semanticsFailure = e + null + } catch (t: Throwable) { + // semantics modifiers run app code, so we can't know what this throws + ExceptionUtils.rethrowIfFatal(t) + semanticsFailure = t + null + } - try { - semantics = retrieveSemanticsConfiguration(node) - } catch (t: Throwable) { + if (semanticsFailure != null) { if (!semanticsRetrievalErrorLogged) { semanticsRetrievalErrorLogged = true logger.log( SentryLevel.ERROR, - t, + semanticsFailure, """ Error retrieving semantics information from Compose tree. Most likely you're using an unsupported version of androidx.compose.ui:ui. The supported @@ -152,7 +164,7 @@ internal object ComposeViewHierarchyNode { // fail fast in our own sample/UI-test apps (see SentryReplayDebug), so regressions surface // as crashes instead of silently degrading masking if (SentryReplayDebug.failFast) { - throw t + throw semanticsFailure } // If we're unable to retrieve the semantics configuration @@ -284,13 +296,23 @@ internal object ComposeViewHierarchyNode { return false } + var traversalFailure: Throwable? = null try { val rootNode = (view as? Owner)?.root ?: return false rootNode.traverse(parent, isComposeRoot = true, options, logger) + } catch (e: LinkageError) { + // an unsupported Compose version, which must not crash the app + traversalFailure = e } catch (e: Throwable) { + // the traversal runs app code, so we can't know what it throws + ExceptionUtils.rethrowIfFatal(e) + traversalFailure = e + } + + if (traversalFailure != null) { logger.log( SentryLevel.ERROR, - e, + traversalFailure, """ Error traversing Compose tree. Most likely you're using an unsupported version of androidx.compose.ui:ui. The minimum supported version is 1.5.0. If it's a newer @@ -302,7 +324,7 @@ internal object ComposeViewHierarchyNode { // fail fast in our own sample/UI-test apps (see SentryReplayDebug), so regressions surface // as crashes instead of silently skipping the whole Compose subtree (i.e. not masking it) if (SentryReplayDebug.failFast) { - throw e + throw traversalFailure } return false } diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeHelper.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeHelper.kt index 10d02103ba1..39325bda05e 100644 --- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeHelper.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeHelper.kt @@ -35,8 +35,10 @@ internal class SentryComposeHelper(logger: ILogger) { val value = sentryTagElementField.get(modifier) return value as String? } - } catch (e: Throwable) { - // ignored + } catch (_: IllegalAccessException) { + // fall back to SemanticsModifier below + } catch (_: ClassCastException) { + // the tag is not a String in this Compose version } // Older versions use SemanticsModifier diff --git a/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt b/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt index 6632efce93f..339e12c052c 100644 --- a/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt +++ b/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt @@ -17,8 +17,10 @@ import io.sentry.exception.ExceptionMechanismException import io.sentry.exception.SentryHttpClientException import io.sentry.protocol.Mechanism import io.sentry.util.CookieUtils +import io.sentry.util.ExceptionUtils import io.sentry.util.HttpUtils import io.sentry.util.UrlUtils +import kotlin.coroutines.cancellation.CancellationException internal object SentryKtorClientUtils { internal suspend fun captureClientError( @@ -51,7 +53,12 @@ internal object SentryKtorClientUtils { statusCode = response.status.value try { bodySize = response.bodyAsBytes().size.toLong() - } catch (_: Throwable) {} + } catch (e: CancellationException) { + throw e + } catch (@Suppress("TooGenericExceptionCaught") e: Throwable) { + // The HTTP engine can throw anything while reading the body. The body size is optional. + ExceptionUtils.rethrowIfFatal(e) + } } event.request = sentryRequest diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorageProvider.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorageProvider.java index 16599595866..45dd6026874 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorageProvider.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorageProvider.java @@ -2,6 +2,7 @@ import io.opentelemetry.context.ContextStorage; import io.opentelemetry.context.ContextStorageProvider; +import io.sentry.util.ExceptionUtils; import java.util.Iterator; import java.util.ServiceLoader; import org.jetbrains.annotations.NotNull; @@ -23,8 +24,11 @@ public ContextStorage get() { return contextStorageProvider.get(); } } + } catch (LinkageError ignored) { + // a provider needs a class that is missing or has an incompatible version, use fallback } catch (Throwable t) { - // ignore and use fallback + // Other providers are third-party code, so we can't know what they throw. Use fallback. + ExceptionUtils.rethrowIfFatal(t); } // using default / fallback storage diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 932569177b4..f83f6667f6d 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -21,6 +21,7 @@ import io.sentry.transport.NoOpEnvelopeCache; import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.DebugMetaPropertiesApplier; +import io.sentry.util.ExceptionUtils; import io.sentry.util.FileUtils; import io.sentry.util.InitUtil; import io.sentry.util.LoadClass; @@ -362,7 +363,21 @@ private static void init(final @NotNull SentryOptions options, final boolean glo for (final Integration integration : options.getIntegrations()) { try { integration.register(ScopesAdapter.getInstance(), options); + } catch (LinkageError e) { + // The integration needs an optional dependency that is missing or has an incompatible + // version. It is skipped and init continues. + options + .getLogger() + .log( + SentryLevel.WARNING, + "Failed to register the integration " + + integration.getClass().getName() + + ", a dependency it needs is missing or incompatible", + e); } catch (Throwable t) { + // Integrations can be user-provided, so we can't know what they throw. A failing + // integration is skipped and init continues. + ExceptionUtils.rethrowIfFatal(t); options .getLogger() .log( diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 71dd4588e33..c9f143df377 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -737,8 +737,8 @@ public void activate() { try { final Class clazz = Class.forName("io.sentry.spotlight.SpotlightIntegration"); integrations.add((Integration) clazz.getConstructor().newInstance()); - } catch (Throwable ignored) { - // SpotlightIntegration not available + } catch (ReflectiveOperationException | LinkageError ignored) { + // sentry-spotlight is missing or has an incompatible version } } } diff --git a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java index 07e9b6357d7..6be630a0c7a 100644 --- a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java +++ b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java @@ -144,6 +144,9 @@ public void uncaughtException(Thread thread, Throwable thrown) { } } } catch (Throwable e) { + // Deliberately broad and without ExceptionUtils.rethrowIfFatal: this thread is already + // dying, and rethrowing would skip the chained handler below, which is what makes the app + // crash as it normally would (on Android, it kills the process). The crash event is lost. options .getLogger() .log(SentryLevel.ERROR, "Error sending uncaught exception to Sentry.", e); diff --git a/sentry/src/main/java/io/sentry/cache/tape/QueueFile.java b/sentry/src/main/java/io/sentry/cache/tape/QueueFile.java index 137f1cd9156..c93d19ebf75 100644 --- a/sentry/src/main/java/io/sentry/cache/tape/QueueFile.java +++ b/sentry/src/main/java/io/sentry/cache/tape/QueueFile.java @@ -379,7 +379,7 @@ boolean ringRead(long position, byte[] buffer, int offset, int count) throws IOE resetFile(); } catch (IOException e) { throw e; - } catch (Throwable e) { + } catch (IndexOutOfBoundsException e) { // most likely the file is corrupt, so we delete it and recreate, accepting data loss resetFile(); } diff --git a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java index cf86a13fc53..683eea8a866 100644 --- a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java +++ b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java @@ -9,6 +9,7 @@ import io.sentry.ScopesAdapter; import io.sentry.SentryLevel; import java.util.Iterator; +import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -38,7 +39,8 @@ public final class ProfilingServiceLoader { logger.log( SentryLevel.DEBUG, "No continuous profiler provider found, using NoOpContinuousProfiler"); return NoOpContinuousProfiler.getInstance(); - } catch (Throwable t) { + } catch (ServiceConfigurationError | LinkageError t) { + // The provider is broken, or async-profiler's native library failed to load logger.log( SentryLevel.ERROR, "Failed to load continuous profiler provider, using NoOpContinuousProfiler", @@ -68,7 +70,8 @@ public final class ProfilingServiceLoader { SentryLevel.DEBUG, "No profile converter provider found, using NoOpProfileConverter"); return NoOpProfileConverter.getInstance(); } - } catch (Throwable t) { + } catch (ServiceConfigurationError | LinkageError t) { + // The provider is broken, or a class it needs is missing logger.log( SentryLevel.ERROR, "Failed to load profile converter provider, using NoOpProfileConverter", diff --git a/sentry/src/main/java/io/sentry/util/LoadClass.java b/sentry/src/main/java/io/sentry/util/LoadClass.java index bf179fad459..aedd27a14be 100644 --- a/sentry/src/main/java/io/sentry/util/LoadClass.java +++ b/sentry/src/main/java/io/sentry/util/LoadClass.java @@ -41,7 +41,8 @@ public class LoadClass { if (logger != null) { logger.log(SentryLevel.ERROR, "Failed to load (UnsatisfiedLinkError) " + clazz, e); } - } catch (Throwable e) { + } catch (LinkageError e) { + // e.g. ExceptionInInitializerError, or NoClassDefFoundError for a dependency of the class if (logger != null) { logger.log(SentryLevel.ERROR, "Failed to initialize " + clazz, e); } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 8b87e67f9f5..8d0e2c54e0d 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -57,6 +57,7 @@ import org.mockito.kotlin.argThat import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.check +import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock @@ -680,6 +681,49 @@ class SentryTest { verify(logger).log(eq(SentryLevel.ERROR), any(), eq(initException)) } + @Test + fun `init skips an integration whose dependency is missing and registers the rest`() { + val failing = mock() + doThrow(NoClassDefFoundError("missing")).whenever(failing).register(any(), any()) + val next = mock() + + initForTest { + it.dsn = dsn + it.addIntegration(failing) + it.addIntegration(next) + } + + verify(next).register(any(), any()) + } + + @Test + fun `init skips an integration that throws on register and registers the rest`() { + val failing = mock() + doThrow(IllegalStateException("register")).whenever(failing).register(any(), any()) + val next = mock() + + initForTest { + it.dsn = dsn + it.addIntegration(failing) + it.addIntegration(next) + } + + verify(next).register(any(), any()) + } + + @Test + fun `init rethrows a fatal error from an integration's register`() { + val failing = mock() + doThrow(OutOfMemoryError("register")).whenever(failing).register(any(), any()) + + assertThrows(OutOfMemoryError::class.java) { + initForTest { + it.dsn = dsn + it.addIntegration(failing) + } + } + } + @Test fun `overrides envelope cache if it's not set`() { var sentryOptions: SentryOptions? = null diff --git a/sentry/src/test/java/io/sentry/util/LoadClassTest.kt b/sentry/src/test/java/io/sentry/util/LoadClassTest.kt index 4cb921feff8..3dcc478bb9f 100644 --- a/sentry/src/test/java/io/sentry/util/LoadClassTest.kt +++ b/sentry/src/test/java/io/sentry/util/LoadClassTest.kt @@ -74,6 +74,17 @@ class LoadClassTest { assertTrue(LoadClassInitFlag.initialized) } + + @Test + fun `loadClass returns null when the static initializer of the class throws`() { + assertNull(LoadClass().loadClass(LoadClassFailingInitProbe::class.java.name, null)) + } +} + +private object LoadClassFailingInitProbe { + init { + throw IllegalStateException("init") + } } private object IsClassAvailableNoInitFlag {