Skip to content

Commit b0f62c0

Browse files
authored
Add abnormal_mechanism to sessions for ANR rate calculation (#2475)
1 parent 9b6c37f commit b0f62c0

26 files changed

+530
-95
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Improve ANR implementation: ([#2475](https://github.com/getsentry/sentry-java/pull/2475))
8+
- Add `abnormal_mechanism` to sessions for ANR rate calculation
9+
- Always attach thread dump to ANR events
10+
- Distinguish between foreground and background ANRs
11+
312
## 6.12.1
413

514
### Fixes

sentry-android-core/api/sentry-android-core.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ public final class io/sentry/android/core/AppStartState {
6868
public fun setAppStartMillis (J)V
6969
}
7070

71+
public final class io/sentry/android/core/AppState {
72+
public static fun getInstance ()Lio/sentry/android/core/AppState;
73+
public fun isInBackground ()Ljava/lang/Boolean;
74+
}
75+
7176
public final class io/sentry/android/core/BuildConfig {
7277
public static final field BUILD_TYPE Ljava/lang/String;
7378
public static final field DEBUG Z

sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
package io.sentry.android.core;
22

3-
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
43
import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY;
54

65
import android.annotation.SuppressLint;
76
import android.app.Activity;
8-
import android.app.ActivityManager;
97
import android.app.Application;
10-
import android.content.Context;
118
import android.os.Build;
129
import android.os.Bundle;
1310
import android.os.Handler;
1411
import android.os.Looper;
15-
import android.os.Process;
1612
import android.view.View;
1713
import androidx.annotation.NonNull;
1814
import io.sentry.Breadcrumb;
@@ -36,7 +32,6 @@
3632
import java.io.IOException;
3733
import java.lang.ref.WeakReference;
3834
import java.util.Date;
39-
import java.util.List;
4035
import java.util.Map;
4136
import java.util.WeakHashMap;
4237
import org.jetbrains.annotations.NotNull;
@@ -93,7 +88,7 @@ public ActivityLifecycleIntegration(
9388

9489
// we only track app start for processes that will show an Activity (full launch).
9590
// Here we check the process importance which will tell us that.
96-
foregroundImportance = isForegroundImportance(this.application);
91+
foregroundImportance = ContextUtils.isForegroundImportance(this.application);
9792
}
9893

9994
@Override
@@ -505,38 +500,4 @@ private void setColdStart(final @Nullable Bundle savedInstanceState) {
505500
return APP_START_WARM;
506501
}
507502
}
508-
509-
/**
510-
* Check if the Started process has IMPORTANCE_FOREGROUND importance which means that the process
511-
* will start an Activity.
512-
*
513-
* @return true if IMPORTANCE_FOREGROUND and false otherwise
514-
*/
515-
private boolean isForegroundImportance(final @NotNull Context context) {
516-
try {
517-
final Object service = context.getSystemService(Context.ACTIVITY_SERVICE);
518-
if (service instanceof ActivityManager) {
519-
final ActivityManager activityManager = (ActivityManager) service;
520-
final List<ActivityManager.RunningAppProcessInfo> runningAppProcesses =
521-
activityManager.getRunningAppProcesses();
522-
523-
if (runningAppProcesses != null) {
524-
final int myPid = Process.myPid();
525-
for (final ActivityManager.RunningAppProcessInfo processInfo : runningAppProcesses) {
526-
if (processInfo.pid == myPid) {
527-
if (processInfo.importance == IMPORTANCE_FOREGROUND) {
528-
return true;
529-
}
530-
break;
531-
}
532-
}
533-
}
534-
}
535-
} catch (SecurityException ignored) {
536-
// happens for isolated processes
537-
} catch (Throwable ignored) {
538-
// should never happen
539-
}
540-
return false;
541-
}
542503
}

sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,10 @@ private static void installDefaultIntegrations(
203203
new SendFireAndForgetOutboxSender(() -> options.getOutboxPath()),
204204
hasStartupCrashMarker));
205205

206-
options.addIntegration(new AnrIntegration(context));
206+
// AppLifecycleIntegration has to be installed before AnrIntegration, because AnrIntegration
207+
// relies on AppState set by it
207208
options.addIntegration(new AppLifecycleIntegration());
209+
options.addIntegration(new AnrIntegration(context));
208210

209211
// registerActivityLifecycleCallbacks is only available if Context is an AppContext
210212
if (context instanceof Application) {

sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
import android.annotation.SuppressLint;
44
import android.content.Context;
5+
import io.sentry.Hint;
56
import io.sentry.IHub;
6-
import io.sentry.ILogger;
77
import io.sentry.Integration;
8+
import io.sentry.SentryEvent;
89
import io.sentry.SentryLevel;
910
import io.sentry.SentryOptions;
1011
import io.sentry.exception.ExceptionMechanismException;
12+
import io.sentry.hints.AbnormalExit;
1113
import io.sentry.protocol.Mechanism;
14+
import io.sentry.util.HintUtils;
1215
import io.sentry.util.Objects;
1316
import java.io.Closeable;
1417
import java.io.IOException;
@@ -64,7 +67,7 @@ private void register(final @NotNull IHub hub, final @NotNull SentryAndroidOptio
6467
new ANRWatchDog(
6568
options.getAnrTimeoutIntervalMillis(),
6669
options.isAnrReportInDebug(),
67-
error -> reportANR(hub, options.getLogger(), error),
70+
error -> reportANR(hub, options, error),
6871
options.getLogger(),
6972
context);
7073
anrWatchDog.start();
@@ -78,16 +81,40 @@ private void register(final @NotNull IHub hub, final @NotNull SentryAndroidOptio
7881
@TestOnly
7982
void reportANR(
8083
final @NotNull IHub hub,
81-
final @NotNull ILogger logger,
84+
final @NotNull SentryAndroidOptions options,
8285
final @NotNull ApplicationNotResponding error) {
83-
logger.log(SentryLevel.INFO, "ANR triggered with message: %s", error.getMessage());
86+
options.getLogger().log(SentryLevel.INFO, "ANR triggered with message: %s", error.getMessage());
8487

88+
// if LifecycleWatcher isn't available, we always assume the ANR is foreground
89+
final boolean isAppInBackground = Boolean.TRUE.equals(AppState.getInstance().isInBackground());
90+
91+
@SuppressWarnings("ThrowableNotThrown")
92+
final Throwable anrThrowable = buildAnrThrowable(isAppInBackground, options, error);
93+
94+
final SentryEvent event = new SentryEvent(anrThrowable);
95+
event.setLevel(SentryLevel.ERROR);
96+
97+
final AnrHint anrHint = new AnrHint(isAppInBackground);
98+
final Hint hint = HintUtils.createWithTypeCheckHint(anrHint);
99+
100+
hub.captureEvent(event, hint);
101+
}
102+
103+
private @NotNull Throwable buildAnrThrowable(
104+
final boolean isAppInBackground,
105+
final @NotNull SentryAndroidOptions options,
106+
final @NotNull ApplicationNotResponding anr) {
107+
108+
String message = "ANR for at least " + options.getAnrTimeoutIntervalMillis() + " ms.";
109+
if (isAppInBackground) {
110+
message = "Background " + message;
111+
}
112+
113+
final ApplicationNotResponding error = new ApplicationNotResponding(message, anr.getThread());
85114
final Mechanism mechanism = new Mechanism();
86115
mechanism.setType("ANR");
87-
final ExceptionMechanismException throwable =
88-
new ExceptionMechanismException(mechanism, error, error.getThread(), true);
89116

90-
hub.captureException(throwable);
117+
return new ExceptionMechanismException(mechanism, error, error.getThread(), true);
91118
}
92119

93120
@TestOnly
@@ -108,4 +135,30 @@ public void close() throws IOException {
108135
}
109136
}
110137
}
138+
139+
/**
140+
* ANR is an abnormal session exit, according to <a
141+
* href="https://develop.sentry.dev/sdk/sessions/#crashed-abnormal-vs-errored">Develop Docs</a>
142+
* because we don't know whether the app has recovered after it or not.
143+
*/
144+
static final class AnrHint implements AbnormalExit {
145+
146+
private final boolean isBackgroundAnr;
147+
148+
AnrHint(final boolean isBackgroundAnr) {
149+
this.isBackgroundAnr = isBackgroundAnr;
150+
}
151+
152+
@Override
153+
public String mechanism() {
154+
return isBackgroundAnr ? "anr_background" : "anr_foreground";
155+
}
156+
157+
// We don't want the current thread (watchdog) to be marked as crashed, otherwise the Sentry
158+
// Console prioritizes it over the main thread in the thread's list.
159+
@Override
160+
public boolean ignoreCurrentThread() {
161+
return true;
162+
}
163+
}
111164
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.sentry.android.core;
2+
3+
import org.jetbrains.annotations.ApiStatus;
4+
import org.jetbrains.annotations.NotNull;
5+
import org.jetbrains.annotations.Nullable;
6+
import org.jetbrains.annotations.TestOnly;
7+
8+
/** AppState holds the state of the App, e.g. whether the app is in background/foreground, etc. */
9+
@ApiStatus.Internal
10+
public final class AppState {
11+
private static @NotNull AppState instance = new AppState();
12+
13+
private AppState() {}
14+
15+
public static @NotNull AppState getInstance() {
16+
return instance;
17+
}
18+
19+
private @Nullable Boolean inBackground = null;
20+
21+
@TestOnly
22+
void resetInstance() {
23+
instance = new AppState();
24+
}
25+
26+
public @Nullable Boolean isInBackground() {
27+
return inBackground;
28+
}
29+
30+
synchronized void setInBackground(final boolean inBackground) {
31+
this.inBackground = inBackground;
32+
}
33+
}

sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package io.sentry.android.core;
22

3+
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
4+
35
import android.annotation.SuppressLint;
6+
import android.app.ActivityManager;
47
import android.content.Context;
58
import android.content.pm.ApplicationInfo;
69
import android.content.pm.PackageInfo;
710
import android.content.pm.PackageManager;
811
import android.os.Build;
12+
import android.os.Process;
913
import io.sentry.ILogger;
1014
import io.sentry.SentryLevel;
15+
import java.util.List;
1116
import org.jetbrains.annotations.NotNull;
1217
import org.jetbrains.annotations.Nullable;
1318

@@ -112,4 +117,38 @@ static String getVersionName(final @NotNull PackageInfo packageInfo) {
112117
private static @NotNull String getVersionCodeDep(final @NotNull PackageInfo packageInfo) {
113118
return Integer.toString(packageInfo.versionCode);
114119
}
120+
121+
/**
122+
* Check if the Started process has IMPORTANCE_FOREGROUND importance which means that the process
123+
* will start an Activity.
124+
*
125+
* @return true if IMPORTANCE_FOREGROUND and false otherwise
126+
*/
127+
static boolean isForegroundImportance(final @NotNull Context context) {
128+
try {
129+
final Object service = context.getSystemService(Context.ACTIVITY_SERVICE);
130+
if (service instanceof ActivityManager) {
131+
final ActivityManager activityManager = (ActivityManager) service;
132+
final List<ActivityManager.RunningAppProcessInfo> runningAppProcesses =
133+
activityManager.getRunningAppProcesses();
134+
135+
if (runningAppProcesses != null) {
136+
final int myPid = Process.myPid();
137+
for (final ActivityManager.RunningAppProcessInfo processInfo : runningAppProcesses) {
138+
if (processInfo.pid == myPid) {
139+
if (processInfo.importance == IMPORTANCE_FOREGROUND) {
140+
return true;
141+
}
142+
break;
143+
}
144+
}
145+
}
146+
}
147+
} catch (SecurityException ignored) {
148+
// happens for isolated processes
149+
} catch (Throwable ignored) {
150+
// should never happen
151+
}
152+
return false;
153+
}
115154
}

sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ final class LifecycleWatcher implements DefaultLifecycleObserver {
6767
public void onStart(final @NotNull LifecycleOwner owner) {
6868
startSession();
6969
addAppBreadcrumb("foreground");
70+
AppState.getInstance().setInBackground(false);
7071
}
7172

7273
private void startSession() {
@@ -106,6 +107,7 @@ public void onStop(final @NotNull LifecycleOwner owner) {
106107
scheduleEndSession();
107108
}
108109

110+
AppState.getInstance().setInBackground(true);
109111
addAppBreadcrumb("background");
110112
}
111113

sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,16 @@ class AndroidOptionsInitializerTest {
312312
assertNotNull(actual)
313313
}
314314

315+
@Test
316+
fun `AnrIntegration is added after AppLifecycleIntegration`() {
317+
fixture.initSut()
318+
319+
val appLifecycleIndex =
320+
fixture.sentryOptions.integrations.indexOfFirst { it is AppLifecycleIntegration }
321+
val anrIndex = fixture.sentryOptions.integrations.indexOfFirst { it is AnrIntegration }
322+
assertTrue { appLifecycleIndex < anrIndex }
323+
}
324+
315325
@Test
316326
fun `EnvelopeFileObserverIntegration added to integration list`() {
317327
fixture.initSut()

0 commit comments

Comments
 (0)