Skip to content
Open
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
2 changes: 1 addition & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (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
Expand Down Expand Up @@ -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 <init> (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 <init> ()V
public static fun identify (Ljava/util/List;)Lio/sentry/android/core/anr/AggregatedStackTrace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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> sentryException =
sentryExceptionFactory.getSentryExceptions(error);
final Mechanism mechanism = new Mechanism();
mechanism.setType("ANR");
final @Nullable List<SentryException> 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));
Expand Down Expand Up @@ -1057,32 +1048,6 @@ private SentryId captureAnrProfile(
}
}

private boolean hasOnlySystemFrames(@NotNull SentryEvent event) {
final List<SentryException> 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<SentryStackFrame> 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.
Expand Down
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;

Expand All @@ -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 {
Comment thread
sentry-warden[bot] marked this conversation as resolved.
Resolved
Comment thread
markushi marked this conversation as resolved.
Resolved

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 captureException() and thus could influence issue grouping.

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;
}
Expand Down
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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 AnrCulpritIdentifier. Just making sure we don't want to extend the latter instead?

(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()) })
}
Loading