diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index b4c58711dd..62d80f721b 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -455,7 +455,7 @@ limitations under the License. ### Scope -The Sentry Java SDK includes an adapted version of the ANR-WatchDog library for Application Not Responding (ANR) detection on Android. The code resides in `io.sentry.android.core.ANRWatchDog`. +The Sentry Java SDK includes an adapted version of the ANR-WatchDog library for Application Not Responding (ANR) detection on Android. The code resides in `io.sentry.android.core.ANRWatchDog` and `io.sentry.android.core.ApplicationNotResponding` (adapted from `ANRError`). ``` MIT License diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 712f981ecd..5ab0634a49 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -244,6 +244,11 @@ public final class io/sentry/android/core/ApplicationExitInfoEventProcessor : io public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } +public final class io/sentry/android/core/ApplicationNotResponding : java/lang/RuntimeException { + public fun (Ljava/lang/String;)V + public fun getThread ()Ljava/lang/Thread; +} + public final class io/sentry/android/core/BuildConfig { public static final field BUILD_TYPE Ljava/lang/String; public static final field DEBUG Z @@ -716,6 +721,12 @@ public final class io/sentry/android/core/anr/AggregatedStackTrace { public fun getStack ()[Ljava/lang/StackTraceElement; } +public final class io/sentry/android/core/anr/AnrCulpritEventHelper { + public fun (Lio/sentry/SentryExceptionFactory;)V + public static fun hasOnlySystemFrames (Ljava/util/List;)Z + public fun toSentryExceptions (Lio/sentry/android/core/anr/AggregatedStackTrace;Lio/sentry/protocol/Mechanism;)Ljava/util/List; +} + public final class io/sentry/android/core/anr/AnrCulpritIdentifier { public fun ()V public static fun identify (Ljava/util/List;)Lio/sentry/android/core/anr/AggregatedStackTrace; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java index a82a6de5db..b96fca8b0e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java @@ -40,6 +40,7 @@ import io.sentry.SentryStackTraceFactory; import io.sentry.SpanContext; import io.sentry.android.core.anr.AggregatedStackTrace; +import io.sentry.android.core.anr.AnrCulpritEventHelper; import io.sentry.android.core.anr.AnrCulpritIdentifier; import io.sentry.android.core.anr.AnrProfile; import io.sentry.android.core.anr.AnrProfileManager; @@ -48,7 +49,6 @@ import io.sentry.android.core.internal.util.CpuInfoUtils; import io.sentry.cache.PersistingOptionsObserver; import io.sentry.cache.PersistingScopeObserver; -import io.sentry.exception.ExceptionMechanismException; import io.sentry.hints.AbnormalExit; import io.sentry.hints.Backfillable; import io.sentry.hints.NativeCrashExit; @@ -64,7 +64,6 @@ import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryException; import io.sentry.protocol.SentryId; -import io.sentry.protocol.SentryStackFrame; import io.sentry.protocol.SentryStackTrace; import io.sentry.protocol.SentryThread; import io.sentry.protocol.SentryTransaction; @@ -100,6 +99,7 @@ public final class ApplicationExitInfoEventProcessor private final @NotNull BuildInfoProvider buildInfoProvider; private final @NotNull SentryExceptionFactory sentryExceptionFactory; + private final @NotNull AnrCulpritEventHelper culpritEventHelper; private final @Nullable PersistingScopeObserver persistingScopeObserver; @@ -121,6 +121,7 @@ public ApplicationExitInfoEventProcessor( new SentryStackTraceFactory(this.options); sentryExceptionFactory = new SentryExceptionFactory(sentryStackTraceFactory); + culpritEventHelper = new AnrCulpritEventHelper(sentryExceptionFactory); } private @Nullable HintEnricher findEnricher(final @NotNull Object hint) { @@ -859,7 +860,8 @@ private void setDefaultAnrFingerprint( return; } - if (options.isEnableAnrFingerprinting() && hasOnlySystemFrames(event)) { + if (options.isEnableAnrFingerprinting() + && AnrCulpritEventHelper.hasOnlySystemFrames(event.getExceptions())) { // If the stacktrace only contains system frames, we want to statically group these events // to avoid ANR noise due to {{ default }} stacktrace grouping event.setFingerprints( @@ -1002,28 +1004,17 @@ private void applyAnrProfile( // Capture profile chunk final @Nullable SentryId profilerId = captureAnrProfile(anrTimestamp, anrProfile, optionsSource); - final @NotNull StackTraceElement[] stack = culprit.getStack(); - - if (stack.length > 0) { - final StackTraceElement stackTraceElement = stack[0]; - final String message = - stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName(); - final ApplicationNotResponding exception = new ApplicationNotResponding(message); - exception.setStackTrace(stack); - - final Mechanism mechanism = new Mechanism(); - mechanism.setType("ANR"); - final ExceptionMechanismException error = - new ExceptionMechanismException(mechanism, exception, null, false); - - final @NotNull List sentryException = - sentryExceptionFactory.getSentryExceptions(error); + final Mechanism mechanism = new Mechanism(); + mechanism.setType("ANR"); + final @Nullable List sentryExceptions = + culpritEventHelper.toSentryExceptions(culprit, mechanism); + if (sentryExceptions != null) { // Replace the original ANR exception with the profile-derived one, // as we assume the profiling culprit identification is more valuable // the event threads are kept as-is, so the original main thread stacktrace is still // available - event.setExceptions(sentryException); + event.setExceptions(sentryExceptions); if (profilerId != null) { event.getContexts().setProfile(new ProfileContext(profilerId)); @@ -1057,32 +1048,6 @@ private SentryId captureAnrProfile( } } - private boolean hasOnlySystemFrames(@NotNull SentryEvent event) { - final List exceptions = event.getExceptions(); - if (exceptions == null || exceptions.isEmpty()) { - return false; - } - - for (final SentryException exception : exceptions) { - final SentryStackTrace stacktrace = exception.getStacktrace(); - if (stacktrace != null) { - final List frames = stacktrace.getFrames(); - if (frames != null && !frames.isEmpty()) { - for (final SentryStackFrame frame : frames) { - if (frame.isInApp() != null && frame.isInApp()) { - return false; - } - final String module = frame.getModule(); - if (module != null && !AnrCulpritIdentifier.isSystemFrame(module)) { - return false; - } - } - } - } - } - return true; - } - /** * Creates debug metadata for an ANR profile chunk using the build metadata selected for the ANR * event. diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationNotResponding.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationNotResponding.java index 120f2e0bef..18279fbeaf 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationNotResponding.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationNotResponding.java @@ -1,8 +1,12 @@ +// Adapted from ANR-WatchDog. +// Copyright (c) 2016 Salomon BRYS. +// Licensed under the MIT License. // https://github.com/SalomonBrys/ANR-WatchDog/blob/1969075f75f5980e9000eaffbaa13b0daf282dcb/anr-watchdog/src/main/java/com/github/anrwatchdog/ANRError.java -// Based on the class above. The API unnecessary here was removed. +// The API unnecessary here was removed. package io.sentry.android.core; import io.sentry.util.Objects; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -14,14 +18,15 @@ * really the cause of the exception. Each "Caused by" is the stack trace of a running thread. Note * that the main thread always comes first. */ -final class ApplicationNotResponding extends RuntimeException { +@ApiStatus.Internal +public final class ApplicationNotResponding extends RuntimeException { private static final long serialVersionUID = 252541144579117016L; // temp fix for JDK 21: Never serialized. @SuppressWarnings("serial") private final @Nullable Thread thread; - ApplicationNotResponding(final @Nullable String message) { + public ApplicationNotResponding(final @Nullable String message) { super(message); this.thread = null; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrCulpritEventHelper.java b/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrCulpritEventHelper.java new file mode 100644 index 0000000000..b9671bb883 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrCulpritEventHelper.java @@ -0,0 +1,75 @@ +package io.sentry.android.core.anr; + +import io.sentry.SentryExceptionFactory; +import io.sentry.android.core.ApplicationNotResponding; +import io.sentry.exception.ExceptionMechanismException; +import io.sentry.protocol.Mechanism; +import io.sentry.protocol.SentryException; +import io.sentry.protocol.SentryStackFrame; +import io.sentry.protocol.SentryStackTrace; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class AnrCulpritEventHelper { + + private final @NotNull SentryExceptionFactory exceptionFactory; + + public AnrCulpritEventHelper(final @NotNull SentryExceptionFactory exceptionFactory) { + this.exceptionFactory = exceptionFactory; + } + + /** + * The exception message is the class and method of the top frame of the culprit stack. + * + * @return the exceptions, or {@code null} if the culprit stack is empty + */ + public @Nullable List toSentryExceptions( + final @NotNull AggregatedStackTrace culprit, final @NotNull Mechanism mechanism) { + final @NotNull StackTraceElement[] stack = culprit.getStack(); + if (stack.length == 0) { + return null; + } + final @NotNull StackTraceElement top = stack[0]; + final @NotNull ApplicationNotResponding exception = + new ApplicationNotResponding(top.getClassName() + "." + top.getMethodName()); + exception.setStackTrace(stack); + + return exceptionFactory.getSentryExceptions( + new ExceptionMechanismException(mechanism, exception, null, false)); + } + + /** + * A frame is a system frame if it is not in app and its module is a Java, Kotlin, or Android + * framework package, see {@link AnrCulpritIdentifier#isSystemFrame(String)}. + * + * @return {@code true} if there is at least one exception and none of their frames is an app + * frame + */ + public static boolean hasOnlySystemFrames(final @Nullable List exceptions) { + if (exceptions == null || exceptions.isEmpty()) { + return false; + } + + for (final @NotNull SentryException exception : exceptions) { + final @Nullable SentryStackTrace stacktrace = exception.getStacktrace(); + if (stacktrace != null) { + final @Nullable List frames = stacktrace.getFrames(); + if (frames != null && !frames.isEmpty()) { + for (final @NotNull SentryStackFrame frame : frames) { + if (frame.isInApp() != null && frame.isInApp()) { + return false; + } + final @Nullable String module = frame.getModule(); + if (module != null && !AnrCulpritIdentifier.isSystemFrame(module)) { + return false; + } + } + } + } + } + return true; + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrCulpritEventHelperTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrCulpritEventHelperTest.kt new file mode 100644 index 0000000000..c2d82ba8e3 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/anr/AnrCulpritEventHelperTest.kt @@ -0,0 +1,82 @@ +package io.sentry.android.core.anr + +import com.google.common.truth.Truth.assertThat +import io.sentry.SentryExceptionFactory +import io.sentry.SentryOptions +import io.sentry.SentryStackTraceFactory +import io.sentry.protocol.Mechanism +import io.sentry.protocol.SentryException +import io.sentry.protocol.SentryStackFrame +import io.sentry.protocol.SentryStackTrace +import kotlin.test.Test + +class AnrCulpritEventHelperTest { + + private val helper = + AnrCulpritEventHelper(SentryExceptionFactory(SentryStackTraceFactory(SentryOptions()))) + + @Test + fun `creates exception with class and method of the top frame as message`() { + val stack = + arrayOf( + StackTraceElement("com.example.Foo", "bar", "Foo.java", 12), + StackTraceElement("android.os.Looper", "loop", "Looper.java", 200), + ) + val culprit = AggregatedStackTrace(stack, 0, stack.size - 1, 1000L, 0.5f) + + val exceptions = helper.toSentryExceptions(culprit, Mechanism().apply { type = "ANR" }) + + assertThat(exceptions).isNotNull() + assertThat(exceptions!!).hasSize(1) + val exception = exceptions[0] + assertThat(exception.type).isEqualTo("ApplicationNotResponding") + assertThat(exception.module).isEqualTo("io.sentry.android.core") + assertThat(exception.value).isEqualTo("com.example.Foo.bar") + assertThat(exception.mechanism!!.type).isEqualTo("ANR") + assertThat(exception.stacktrace!!.frames!!.map { it.function }).containsExactly("loop", "bar") + } + + @Test + fun `returns null for an empty culprit stack`() { + val culprit = AggregatedStackTrace(emptyArray(), 0, -1, 1000L, 0f) + + assertThat(helper.toSentryExceptions(culprit, Mechanism())).isNull() + } + + @Test + fun `hasOnlySystemFrames is true for java and android frames only`() { + val exceptions = exceptionsWithFrames(frame("java.lang.Object"), frame("android.os.Looper")) + + assertThat(AnrCulpritEventHelper.hasOnlySystemFrames(exceptions)).isTrue() + } + + @Test + fun `hasOnlySystemFrames is false if one frame is in app`() { + val exceptions = + exceptionsWithFrames(frame("android.os.Looper"), frame("android.os.Handler", inApp = true)) + + assertThat(AnrCulpritEventHelper.hasOnlySystemFrames(exceptions)).isFalse() + } + + @Test + fun `hasOnlySystemFrames is false if one frame is not a system frame`() { + val exceptions = exceptionsWithFrames(frame("android.os.Looper"), frame("com.example.Foo")) + + assertThat(AnrCulpritEventHelper.hasOnlySystemFrames(exceptions)).isFalse() + } + + @Test + fun `hasOnlySystemFrames is false without exceptions`() { + assertThat(AnrCulpritEventHelper.hasOnlySystemFrames(null)).isFalse() + assertThat(AnrCulpritEventHelper.hasOnlySystemFrames(emptyList())).isFalse() + } + + private fun frame(module: String, inApp: Boolean? = null) = + SentryStackFrame().apply { + this.module = module + this.isInApp = inApp + } + + private fun exceptionsWithFrames(vararg frames: SentryStackFrame) = + listOf(SentryException().apply { stacktrace = SentryStackTrace(frames.toList()) }) +}