Skip to content

Commit 3b859ab

Browse files
authored
Merge pull request #2332 from bugsnag/PLAT-15149/app-hang-plugin
AppHang Plugin
2 parents 0097bf8 + 06a3385 commit 3b859ab

21 files changed

Lines changed: 515 additions & 0 deletions

File tree

.buildkite/pipeline.full.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ steps:
451451
- bugsnag-android-core/build/outputs/aar/bugsnag-android-core-release.aar
452452
- bugsnag-benchmarks/build/outputs/aar/bugsnag-benchmarks-release.aar
453453
- bugsnag-plugin-android-anr/build/outputs/aar/bugsnag-plugin-android-anr-release.aar
454+
- bugsnag-plugin-android-apphang/build/outputs/aar/bugsnag-plugin-android-apphang-release.aar
454455
- bugsnag-plugin-android-exitinfo/build/outputs/aar/bugsnag-plugin-android-exitinfo-release.aar
455456
- bugsnag-plugin-android-ndk/build/outputs/aar/bugsnag-plugin-android-ndk-release.aar
456457
- bugsnag-plugin-android-okhttp/build/outputs/aar/bugsnag-plugin-android-okhttp-release.aar

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## TBD
44

5+
### Enhancements
6+
7+
* Introduced [bugsnag-plugin-android-apphang](bugsnag-plugin-android-apphang) as a configurable alternative to ANR reporting based on heartbeat monitoring
8+
[#2332](https://github.com/bugsnag/bugsnag-android/pull/2332)
9+
510
### Bug fixes
611

712
* Fixed the consumer proguard rules for `ErrorType` (affects Kotlin Multiplatform apps), and added a `dontwarn` for apps with compileSdk < 36

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
bugsnag-android/ @lemnik @YYChen01988
22
bugsnag-android-core/ @lemnik @YYChen01988
33
bugsnag-plugin-android-anr/ @lemnik @YYChen01988
4+
bugsnag-plugin-android-apphang/ @lemnik @YYChen01988
45
bugsnag-plugin-android-exitinfo/ @lemnik @YYChen01988
56
bugsnag-plugin-android-ndk/ @lemnik @YYChen01988
67
bugsnag-plugin-android-okhttp/ @lemnik @YYChen01988
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# bugsnag-plugin-android-apphang
2+
3+
An alternative to Application Not Responding (ANR) reporting with configurable timeouts.
4+
5+
## High-level Overview
6+
7+
AppHangs can be considered an alternative to ANR reporting. While
8+
[bugsnag-plugin-android-anr](../bugsnag-plugin-android-anr) reports are based on the system
9+
ANR signal, the AppHang plugin can be configured to a specific threshold.
10+
11+
AppHangs are only detected while the app is in the foreground and background detection is not
12+
currently supported.
13+
14+
## ANR and AppHang Reporting
15+
16+
AppHang reporting can be combined with ANR reporting safely, both signals will be detected and
17+
reported. AppHang error reports will typically be visible as breadcrumbs in ANR reports in these
18+
configurations.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
public final class com/bugsnag/android/AppHangConfiguration {
2+
public fun <init> ()V
3+
public fun <init> (JLandroid/os/Looper;)V
4+
public synthetic fun <init> (JLandroid/os/Looper;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
5+
public final fun getAppHangThresholdMillis ()J
6+
public final fun getWatchedLooper ()Landroid/os/Looper;
7+
public final fun setAppHangThresholdMillis (J)V
8+
public final fun setWatchedLooper (Landroid/os/Looper;)V
9+
}
10+
11+
public final class com/bugsnag/android/AppHangException : java/lang/RuntimeException {
12+
public fun <init> (Ljava/lang/String;[Ljava/lang/StackTraceElement;)V
13+
public fun fillInStackTrace ()Ljava/lang/Throwable;
14+
public fun getStackTrace ()[Ljava/lang/StackTraceElement;
15+
}
16+
17+
public final class com/bugsnag/android/BugsnagAppHangPlugin : com/bugsnag/android/Plugin {
18+
public fun <init> ()V
19+
public fun <init> (Lcom/bugsnag/android/AppHangConfiguration;)V
20+
public synthetic fun <init> (Lcom/bugsnag/android/AppHangConfiguration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
21+
public fun load (Lcom/bugsnag/android/Client;)V
22+
public fun unload ()V
23+
}
24+
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
plugins {
2+
alias(libs.plugins.android.library)
3+
alias(libs.plugins.kotlin.android)
4+
alias(libs.plugins.kotlin.compatibility)
5+
alias(libs.plugins.detekt)
6+
alias(libs.plugins.dokka)
7+
alias(libs.plugins.ktlint)
8+
alias(libs.plugins.licenseCheck)
9+
checkstyle
10+
}
11+
12+
android {
13+
compileSdk = libs.versions.android.compileSdk.get().toInt()
14+
namespace = "com.bugsnag.android.apphang"
15+
16+
configureRelease()
17+
18+
defaultConfig {
19+
minSdk = libs.versions.android.minSdk.get().toInt()
20+
ndkVersion = libs.versions.android.ndk.get()
21+
22+
consumerProguardFiles("proguard-rules.pro")
23+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
24+
}
25+
26+
lint {
27+
abortOnError = true
28+
warningsAsErrors = true
29+
checkAllWarnings = true
30+
baseline = File(project.projectDir, "lint-baseline.xml")
31+
disable += setOf("GradleDependency", "NewerVersionAvailable")
32+
}
33+
34+
buildFeatures {
35+
aidl = false
36+
renderScript = false
37+
shaders = false
38+
resValues = false
39+
buildConfig = false
40+
}
41+
42+
compileOptions {
43+
sourceCompatibility = Versions.java
44+
targetCompatibility = Versions.java
45+
}
46+
47+
kotlinOptions {
48+
jvmTarget = Versions.java.toString()
49+
}
50+
51+
testOptions {
52+
unitTests {
53+
isReturnDefaultValues = true
54+
}
55+
}
56+
57+
sourceSets {
58+
named("test") {
59+
java.srcDir(SHARED_TEST_SRC_DIR)
60+
}
61+
}
62+
}
63+
64+
dependencies {
65+
api(libs.bundles.common.api)
66+
add("api", project(":bugsnag-android-core"))
67+
68+
testImplementation(libs.bundles.test.jvm)
69+
androidTestImplementation(libs.bundles.test.android)
70+
}
71+
72+
apply(from = rootProject.file("gradle/detekt.gradle"))
73+
apply(from = rootProject.file("gradle/license-check.gradle"))
74+
apply(from = rootProject.file("gradle/release.gradle"))
75+
76+
configureCheckstyle()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pomName=Bugsnag Android AppHang
2+
artefactId=bugsnag-plugin-android-apphang
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<issues format="6" by="lint 8.12.2" type="baseline" client="gradle" dependencies="false" name="AGP (8.12.2)" variant="all" version="8.12.2">
3+
4+
</issues>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-keepattributes LineNumberTable,SourceFile
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.bugsnag.android
2+
3+
import android.os.Handler
4+
import android.os.HandlerThread
5+
import org.junit.After
6+
import org.junit.Before
7+
import org.junit.Test
8+
import org.mockito.ArgumentMatchers.any
9+
import org.mockito.Mockito
10+
import org.mockito.Mockito.times
11+
import org.mockito.Mockito.verify
12+
import org.mockito.Mockito.verifyNoInteractions
13+
import java.util.concurrent.CountDownLatch
14+
import java.lang.Thread as JThread
15+
16+
private const val APP_HANG_THRESHOLD = 100L
17+
18+
class BugsnagAppHangPluginTest {
19+
private lateinit var handlerThread: HandlerThread
20+
private lateinit var plugin: BugsnagAppHangPlugin
21+
private lateinit var client: Client
22+
private lateinit var handler: Handler
23+
24+
@Before
25+
fun setup() {
26+
handlerThread = HandlerThread("Test Thread")
27+
handlerThread.start()
28+
handler = Handler(handlerThread.looper)
29+
30+
plugin = BugsnagAppHangPlugin(
31+
AppHangConfiguration(
32+
appHangThresholdMillis = APP_HANG_THRESHOLD,
33+
watchedLooper = handlerThread.looper
34+
)
35+
)
36+
37+
client = Mockito.mock()
38+
plugin.load(client)
39+
plugin.startMonitoring()
40+
}
41+
42+
@After
43+
fun shutdown() {
44+
plugin.unload()
45+
handlerThread.quit()
46+
}
47+
48+
@Test
49+
fun testIdleHandlerThread() {
50+
JThread.sleep(APP_HANG_THRESHOLD * 5)
51+
verifyNoInteractions(client)
52+
}
53+
54+
@Test
55+
fun testBelowThresholdEvents() {
56+
val countDownLatch = CountDownLatch(10)
57+
repeat(countDownLatch.count.toInt()) {
58+
handler.post {
59+
JThread.sleep(APP_HANG_THRESHOLD / 2)
60+
countDownLatch.countDown()
61+
}
62+
}
63+
64+
verifyNoInteractions(client)
65+
}
66+
67+
@Test
68+
fun appHang() {
69+
val countDownLatch = CountDownLatch(1)
70+
handler.post {
71+
// wait long enough for 2+ AppHang triggers to happen
72+
JThread.sleep(APP_HANG_THRESHOLD * 3)
73+
countDownLatch.countDown()
74+
}
75+
76+
countDownLatch.await()
77+
78+
// we should have reported exactly 1 AppHang
79+
verify(client, times(1))
80+
.notify(any(AppHangException::class.java), any())
81+
}
82+
83+
@Test
84+
fun appHangRecoverHang() {
85+
val countDownLatch = CountDownLatch(2)
86+
87+
handler.post {
88+
// wait long enough for 2+ AppHang triggers to happen
89+
JThread.sleep(APP_HANG_THRESHOLD * 3)
90+
countDownLatch.countDown()
91+
92+
handler.postDelayed({
93+
JThread.sleep(APP_HANG_THRESHOLD * 3)
94+
countDownLatch.countDown()
95+
}, APP_HANG_THRESHOLD)
96+
}
97+
98+
countDownLatch.await()
99+
100+
// we should have reported exactly 1 AppHangs
101+
verify(client, times(2))
102+
.notify(any(AppHangException::class.java), any())
103+
}
104+
}

0 commit comments

Comments
 (0)