Repository navigation
ref(android): Extract ANR culprit exceptions into AnrCulpritEventHelper #6593
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
302ba2a
2887ad2
a6bda16
7effbd9
9c363b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
markushi marked this conversation as resolved.
Resolved
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. had to change the visibility, otherwise the helper could not have accessed it. Moving this class probably isn't an option, as it's used for |
||
| 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; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I imagine I'll learn more as more PRs emerge, but (for now) it looks like there's a lot of overlap between this class an the existing (Happy to defer to you.) |
||
|
|
||
| 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<SentryException> 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<SentryException> 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<SentryStackFrame> 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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()) }) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.