Skip to content

Commit 66136b0

Browse files
authored
Merge pull request #2369 from bugsnag/release/v6.22.0
Release v6.22.0
2 parents 0be61c3 + ba5392f commit 66136b0

14 files changed

Lines changed: 390 additions & 172 deletions

File tree

.github/workflows/codeql.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ jobs:
6464

6565
# Initializes the CodeQL tools for scanning.
6666
- name: Initialize CodeQL
67-
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e #v4.31.8
67+
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 #v4.31.10
6868
with:
6969
languages: ${{ matrix.language }}
7070
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -83,6 +83,6 @@ jobs:
8383
./gradlew --no-daemon assemble
8484
8585
- name: Perform CodeQL Analysis
86-
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e #v4.31.8
86+
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 #v4.31.10
8787
with:
8888
category: "/language:${{matrix.language}}"

.github/workflows/scorecard.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
# Upload the results to GitHub's code scanning dashboard (optional).
6969
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
7070
- name: "Upload to code-scanning"
71-
uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
71+
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
7272
with:
7373
sarif_file: results.sarif
7474

CHANGELOG.md

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

3+
## 6.22.0 (2026-01-19)
4+
5+
### Enhancements
6+
7+
* Added support for Turbo Module native stacktraces in ``bugsnag-plugin-react-native`
8+
[#2367](https://github.com/bugsnag/bugsnag-android/pull/2367)
9+
10+
### Bug fixes
11+
12+
* Replaced the heartbeat lock with park/unpark in the [bugsnag-plugin-android-apphang](bugsnag-plugin-android-apphang) so that the main/monitor threads are not interdependant
13+
[#2363](https://github.com/bugsnag/bugsnag-android/pull/2363)
14+
315
## 6.21.0 (2026-01-05)
416

517
### Enhancements

bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import java.io.IOException
77
*/
88
class Notifier @JvmOverloads constructor(
99
var name: String = "Android Bugsnag Notifier",
10-
var version: String = "6.21.0",
10+
var version: String = "6.22.0",
1111
var url: String = "https://bugsnag.com"
1212
) : JsonStream.Streamable {
1313

bugsnag-plugin-android-apphang/src/main/java/com/bugsnag/android/internal/LooperMonitorThread.kt

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import android.os.Looper
55
import android.os.SystemClock
66
import java.util.concurrent.TimeUnit
77
import java.util.concurrent.atomic.AtomicBoolean
8-
import java.util.concurrent.locks.ReentrantLock
8+
import java.util.concurrent.locks.LockSupport
99

1010
internal class LooperMonitorThread(
1111
watchedLooper: Looper,
@@ -14,15 +14,13 @@ internal class LooperMonitorThread(
1414
) : Thread("Bugsnag AppHang Monitor: ${watchedLooper.thread.name}") {
1515
private val handler: Handler = Handler(watchedLooper)
1616

17+
@Volatile
1718
private var lastHeartbeatTimestamp = 0L
1819

1920
private val isRunning = AtomicBoolean(false)
2021

2122
private var isAppHangDetected = false
2223

23-
private val heartbeatLock = ReentrantLock(false)
24-
private val heartbeatCondition = heartbeatLock.newCondition()
25-
2624
private val heartbeat: Runnable = Heartbeat()
2725

2826
private fun calculateTimeToAppHang(now: Long): Long =
@@ -36,22 +34,17 @@ internal class LooperMonitorThread(
3634

3735
fun stopMonitoring() {
3836
if (isRunning.compareAndSet(true, false)) {
39-
interrupt()
37+
handler.removeCallbacks(heartbeat)
38+
LockSupport.unpark(this)
4039
}
4140
}
4241

4342
internal fun resetHeartbeatTimer() {
44-
heartbeatLock.lock()
45-
try {
46-
heartbeatCondition.signalAll()
47-
} finally {
48-
heartbeatLock.unlock()
49-
}
43+
LockSupport.unpark(this)
5044
}
5145

5246
private fun reportAppHang(timeSinceLastHeartbeat: Long) {
5347
if (isAppHangDetected) {
54-
// avoid reporting duplicate AppHangs
5548
return
5649
}
5750

@@ -63,37 +56,34 @@ internal class LooperMonitorThread(
6356
handler.post(heartbeat)
6457

6558
while (isRunning.get()) {
66-
heartbeatLock.lock()
67-
try {
68-
val waitThreshold =
69-
if (lastHeartbeatTimestamp <= 0L) appHangThresholdMillis
70-
else calculateTimeToAppHang(SystemClock.elapsedRealtime())
71-
heartbeatCondition.await(waitThreshold, TimeUnit.MILLISECONDS)
72-
73-
val timeSinceLastHeartbeat = SystemClock.elapsedRealtime() - lastHeartbeatTimestamp
74-
75-
if (timeSinceLastHeartbeat >= appHangThresholdMillis) {
76-
reportAppHang(timeSinceLastHeartbeat)
77-
}
78-
} catch (_: InterruptedException) {
79-
// continue loop and check isRunning
80-
} finally {
81-
heartbeatLock.unlock()
59+
val waitThreshold =
60+
if (lastHeartbeatTimestamp <= 0L) appHangThresholdMillis
61+
else calculateTimeToAppHang(SystemClock.uptimeMillis())
62+
63+
val waitThresholdNanos = TimeUnit.MILLISECONDS.toNanos(waitThreshold)
64+
LockSupport.parkNanos(waitThresholdNanos)
65+
66+
if (!isRunning.get()) break
67+
68+
val timeSinceLastHeartbeat = SystemClock.uptimeMillis() - lastHeartbeatTimestamp
69+
70+
if (timeSinceLastHeartbeat >= appHangThresholdMillis) {
71+
reportAppHang(timeSinceLastHeartbeat)
72+
}
73+
74+
if (!handler.post(heartbeat)) {
75+
// handler.post returning false means the Looper has likely quit
76+
isRunning.set(false)
8277
}
8378
}
8479
}
8580

8681
private inner class Heartbeat : Runnable {
8782
override fun run() {
88-
lastHeartbeatTimestamp = SystemClock.elapsedRealtime()
89-
// mark the hang as "recovered" and start the detection again
83+
lastHeartbeatTimestamp = SystemClock.uptimeMillis()
9084
isAppHangDetected = false
91-
resetHeartbeatTimer()
9285

93-
// only post the Heartbeat messages if the monitor is still running
94-
if (isRunning.get()) {
95-
handler.post(this)
96-
}
86+
resetHeartbeatTimer()
9787
}
9888

9989
override fun toString(): String {

bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ErrorDeserializer.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
class ErrorDeserializer implements MapDeserializer<Error> {
99

1010
private final StackframeDeserializer stackframeDeserializer;
11+
private final NativeStackDeserializer nativeStackDeserializer;
1112
private final Logger logger;
1213

13-
ErrorDeserializer(StackframeDeserializer stackframeDeserializer, Logger logger) {
14+
ErrorDeserializer(StackframeDeserializer stackframeDeserializer,
15+
NativeStackDeserializer nativeStackDeserializer,
16+
Logger logger) {
1417
this.stackframeDeserializer = stackframeDeserializer;
18+
this.nativeStackDeserializer = nativeStackDeserializer;
1519
this.logger = logger;
1620
}
1721

@@ -31,6 +35,14 @@ public Error deserialize(Map<String, Object> map) {
3135
new Stacktrace(frames),
3236
ErrorType.valueOf(type.toUpperCase(Locale.US))
3337
);
34-
return new Error(impl, logger);
38+
39+
Error error = new Error(impl, logger);
40+
41+
if (map.containsKey("nativeStack")) {
42+
List<Stackframe> nativeStack = nativeStackDeserializer.deserialize(map);
43+
error.getStacktrace().addAll(0, nativeStack);
44+
}
45+
46+
return error;
3547
}
3648
}

bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/EventDeserializer.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ internal class EventDeserializer(
1010
private val appDeserializer = AppDeserializer()
1111
private val deviceDeserializer = DeviceDeserializer()
1212
private val stackframeDeserializer = StackframeDeserializer()
13-
private val errorDeserializer = ErrorDeserializer(stackframeDeserializer, client.getLogger())
13+
private val nativeStackDeserializer = NativeStackDeserializer(projectPackages, client.config)
14+
private val errorDeserializer = ErrorDeserializer(
15+
stackframeDeserializer,
16+
nativeStackDeserializer,
17+
client.getLogger()
18+
)
1419
private val threadDeserializer = ThreadDeserializer(stackframeDeserializer, client.getLogger())
1520
private val breadcrumbDeserializer = BreadcrumbDeserializer(client.getLogger())
1621

@@ -67,8 +72,6 @@ internal class EventDeserializer(
6772
if (map.containsKey("nativeStack") && event.errors.isNotEmpty()) {
6873
runCatching {
6974
val jsError = event.errors.first()
70-
val nativeStackDeserializer =
71-
NativeStackDeserializer(projectPackages, client.config)
7275
val nativeStack = nativeStackDeserializer.deserialize(map)
7376
jsError.stacktrace.addAll(0, nativeStack)
7477
}

bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/MapUtils.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
class MapUtils {
66

77
@SuppressWarnings("unchecked")
8-
static <T> T getOrNull(Map<String, Object> map, String key) {
9-
Object id = map.get(key);
10-
return id != null ? (T) id : null;
8+
static <T> T getOrNull(Map<String, Object> map, String... keys) {
9+
for (String key : keys) {
10+
Object value = map.get(key);
11+
if (value != null) {
12+
return (T) value;
13+
}
14+
}
15+
return null;
1116
}
1217

1318
@SuppressWarnings("unchecked")

bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/NativeStackDeserializer.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ private Stackframe deserializeStackframe(Map<String, Object> map,
4545
methodName = "";
4646
}
4747

48-
String clz = MapUtils.getOrNull(map, "class");
48+
String clz = MapUtils.getOrNull(map, "className", "class");
4949
String method = clz + "." + methodName;
5050

5151
// RN <0.63.2 doesn't add class, gracefully fallback by only reporting
@@ -54,9 +54,11 @@ private Stackframe deserializeStackframe(Map<String, Object> map,
5454
clz = "";
5555
method = methodName;
5656
}
57+
58+
String file = MapUtils.getOrNull(map, "fileName", "file");
5759
Stackframe stackframe = new Stackframe(
5860
method,
59-
MapUtils.<String>getOrNull(map, "file"),
61+
file,
6062
MapUtils.<Integer>getOrNull(map, "lineNumber"),
6163
Stacktrace.Companion.inProject(clz, projectPackages)
6264
);
Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,103 @@
11
package com.bugsnag.android
22

33
import org.junit.Assert.assertEquals
4+
import org.junit.Assert.assertNull
45
import org.junit.Assert.assertTrue
5-
import org.junit.Before
66
import org.junit.Test
77
import java.util.HashMap
88

99
class ErrorDeserializerTest {
1010

11-
private val map = HashMap<String, Any>()
12-
13-
/**
14-
* Generates a map for verifying the serializer
15-
*/
16-
@Before
17-
fun setup() {
11+
private fun createErrorMap() = HashMap<String, Any>().apply {
1812
val frame = HashMap<String, Any>()
1913
frame["method"] = "foo()"
2014
frame["file"] = "Bar.kt"
2115
frame["lineNumber"] = 29
2216
frame["inProject"] = true
23-
map["stacktrace"] = listOf(frame)
24-
map["errorClass"] = "BrowserException"
25-
map["errorMessage"] = "whoops!"
26-
map["type"] = "reactnativejs"
17+
this["stacktrace"] = listOf(frame)
18+
this["errorClass"] = "BrowserException"
19+
this["errorMessage"] = "whoops!"
20+
this["type"] = "reactnativejs"
2721
}
2822

23+
private fun createNativeStackFrames(): List<Map<String, Any>> = listOf(
24+
mapOf(
25+
"methodName" to "nativeMethod1",
26+
"lineNumber" to 100,
27+
"fileName" to "Native.java",
28+
"className" to "com.reactnativetest.Native"
29+
),
30+
mapOf(
31+
"methodName" to "nativeMethod2",
32+
"lineNumber" to 200,
33+
"fileName" to "NativeHelper.kt",
34+
"className" to "com.example.NativeHelper"
35+
)
36+
)
37+
2938
@Test
30-
fun deserialize() {
31-
val error = ErrorDeserializer(StackframeDeserializer(), object : Logger {}).deserialize(map)
39+
fun deserializeWithoutNativeStack() {
40+
val map = createErrorMap()
41+
val packages = listOf("com.reactnativetest")
42+
val cfg = TestData.generateConfig()
43+
val nativeStackDeserializer = NativeStackDeserializer(packages, cfg)
44+
val errorDeserializer = ErrorDeserializer(
45+
StackframeDeserializer(),
46+
nativeStackDeserializer,
47+
object : Logger {}
48+
)
49+
val error = errorDeserializer.deserialize(map)
50+
3251
assertEquals("BrowserException", error.errorClass)
3352
assertEquals("whoops!", error.errorMessage)
3453
assertEquals(ErrorType.REACTNATIVEJS, error.type)
54+
assertEquals(1, error.stacktrace.size)
55+
56+
val jsFrame = error.stacktrace[0]
57+
assertEquals("foo()", jsFrame.method)
58+
assertEquals("Bar.kt", jsFrame.file)
59+
assertEquals(29, jsFrame.lineNumber)
60+
assertTrue(jsFrame.inProject as Boolean)
61+
}
62+
63+
@Test
64+
fun deserializeWithNativeStack() {
65+
val map = createErrorMap()
66+
map["nativeStack"] = createNativeStackFrames()
67+
68+
val packages = listOf("com.reactnativetest")
69+
val cfg = TestData.generateConfig()
70+
val nativeStackDeserializer = NativeStackDeserializer(packages, cfg)
71+
val errorDeserializer = ErrorDeserializer(StackframeDeserializer(), nativeStackDeserializer, object : Logger {})
72+
val error = errorDeserializer.deserialize(map)
73+
74+
assertEquals("BrowserException", error.errorClass)
75+
assertEquals("whoops!", error.errorMessage)
76+
assertEquals(ErrorType.REACTNATIVEJS, error.type)
77+
78+
// Should have 3 frames total: 2 native frames + 1 JS frame
79+
assertEquals(3, error.stacktrace.size)
80+
81+
// Native frames should be at the start (indices 0 and 1)
82+
val firstNativeFrame = error.stacktrace[0]
83+
assertEquals("com.reactnativetest.Native.nativeMethod1", firstNativeFrame.method)
84+
assertEquals("Native.java", firstNativeFrame.file)
85+
assertEquals(100, firstNativeFrame.lineNumber)
86+
assertTrue(firstNativeFrame.inProject!!)
87+
assertEquals(ErrorType.ANDROID, firstNativeFrame.type)
88+
89+
val secondNativeFrame = error.stacktrace[1]
90+
assertEquals("com.example.NativeHelper.nativeMethod2", secondNativeFrame.method)
91+
assertEquals("NativeHelper.kt", secondNativeFrame.file)
92+
assertEquals(200, secondNativeFrame.lineNumber)
93+
assertNull(secondNativeFrame.inProject)
94+
assertEquals(ErrorType.ANDROID, secondNativeFrame.type)
3595

36-
val frame = error.stacktrace[0]
37-
assertEquals("foo()", frame.method)
38-
assertEquals("Bar.kt", frame.file)
39-
assertEquals(29, frame.lineNumber)
40-
assertTrue(frame.inProject as Boolean)
96+
// Original JS frame should now be at index 2
97+
val jsFrame = error.stacktrace[2]
98+
assertEquals("foo()", jsFrame.method)
99+
assertEquals("Bar.kt", jsFrame.file)
100+
assertEquals(29, jsFrame.lineNumber)
101+
assertTrue(jsFrame.inProject as Boolean)
41102
}
42103
}

0 commit comments

Comments
 (0)