diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71c649cc8c..c09fc9bf38 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## TBD
+
+* Capture and report Thread.state for Android Runtime threads
+ [#1367](https://github.com/bugsnag/bugsnag-android/pull/1367)
+
## 5.13.0 (2021-09-22)
* Capture breadcrumbs for OkHttp network requests
diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml
index b2c62871dd..d867422231 100644
--- a/bugsnag-android-core/detekt-baseline.xml
+++ b/bugsnag-android-core/detekt-baseline.xml
@@ -20,7 +20,9 @@
MagicNumber:DefaultDelivery.kt$DefaultDelivery$429
MagicNumber:DefaultDelivery.kt$DefaultDelivery$499
MagicNumber:LastRunInfoStore.kt$LastRunInfoStore$3
+ MaxLineLength:EventSerializationTest.kt$EventSerializationTest.Companion$it.threads.add(Thread(5, "main", ThreadType.ANDROID, true, Thread.State.RUNNABLE, stacktrace, NoopLogger))
MaxLineLength:LastRunInfo.kt$LastRunInfo$return "LastRunInfo(consecutiveLaunchCrashes=$consecutiveLaunchCrashes, crashed=$crashed, crashedDuringLaunch=$crashedDuringLaunch)"
+ MaxLineLength:ThreadState.kt$ThreadState$Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, Thread.State.forThread(thread), stacktrace, logger)
ProtectedMemberInFinalClass:ConfigInternal.kt$ConfigInternal$protected val plugins = HashSet<Plugin>()
ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun isAnr(event: Event): Boolean
ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun shouldDiscardClass(): Boolean
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Thread.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Thread.java
index 90daa50e49..d6d5ee73fe 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Thread.java
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Thread.java
@@ -1,6 +1,7 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import java.io.IOException;
import java.util.List;
@@ -19,9 +20,10 @@ public class Thread implements JsonStream.Streamable {
@NonNull String name,
@NonNull ThreadType type,
boolean errorReportingThread,
+ @NonNull Thread.State state,
@NonNull Stacktrace stacktrace,
@NonNull Logger logger) {
- this.impl = new ThreadInternal(id, name, type, errorReportingThread, stacktrace);
+ this.impl = new ThreadInternal(id, name, type, errorReportingThread, state, stacktrace);
this.logger = logger;
}
@@ -81,6 +83,25 @@ public ThreadType getType() {
return impl.getType();
}
+ /**
+ * Sets the state of thread (from {@link java.lang.Thread})
+ */
+ public void setState(@NonNull Thread.State threadState) {
+ if (threadState != null) {
+ impl.setState(threadState);
+ } else {
+ logNull("state");
+ }
+ }
+
+ /**
+ * Gets the state of the thread (from {@link java.lang.Thread})
+ */
+ @NonNull
+ public Thread.State getState() {
+ return impl.getState();
+ }
+
/**
* Gets whether the thread was the thread that caused the event
*/
@@ -111,4 +132,79 @@ public List getStacktrace() {
public void toStream(@NonNull JsonStream stream) throws IOException {
impl.toStream(stream);
}
+
+ /**
+ * The state of a reported {@link Thread}. These states correspond directly to
+ * {@link java.lang.Thread.State}, except for {@code UNKNOWN} which indicates that
+ * a state could not be captured or mapped.
+ */
+ public enum State {
+ NEW("NEW"),
+ BLOCKED("BLOCKED"),
+ RUNNABLE("RUNNABLE"),
+ TERMINATED("TERMINATED"),
+ TIMED_WAITING("TIMED_WAITING"),
+ WAITING("WAITING"),
+ UNKNOWN("UNKNOWN");
+
+ private final String descriptor;
+
+ State(String descriptor) {
+ this.descriptor = descriptor;
+ }
+
+ @NonNull
+ public String getDescriptor() {
+ return descriptor;
+ }
+
+ @NonNull
+ public static State forThread(@NonNull java.lang.Thread thread) {
+ java.lang.Thread.State state = thread.getState();
+ return getState(state);
+ }
+
+ /**
+ * Lookup the {@code State} for a given {@link #getDescriptor() descriptor} code. Unlike
+ * {@link #valueOf(String) valueOf}, this method will return {@link #UNKNOWN} is no
+ * matching {@code State} constant can be found.
+ *
+ * @param descriptor a consistent descriptor of the state constant to lookup
+ * @return the requested {@link State} or {@link #UNKNOWN}
+ */
+ @NonNull
+ public static State byDescriptor(@Nullable String descriptor) {
+ if (descriptor == null) {
+ return UNKNOWN;
+ }
+
+ for (State state : values()) {
+ if (state.getDescriptor().equals(descriptor)) {
+ return state;
+ }
+ }
+
+ return UNKNOWN;
+ }
+
+ @NonNull
+ private static State getState(java.lang.Thread.State state) {
+ switch (state) {
+ case NEW:
+ return NEW;
+ case BLOCKED:
+ return BLOCKED;
+ case RUNNABLE:
+ return RUNNABLE;
+ case TERMINATED:
+ return TERMINATED;
+ case TIMED_WAITING:
+ return TIMED_WAITING;
+ case WAITING:
+ return WAITING;
+ default:
+ return UNKNOWN;
+ }
+ }
+ }
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadInternal.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadInternal.kt
index 459c17f87e..efacfb5644 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadInternal.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadInternal.kt
@@ -7,6 +7,7 @@ class ThreadInternal internal constructor(
var name: String,
var type: ThreadType,
val isErrorReportingThread: Boolean,
+ var state: Thread.State,
stacktrace: Stacktrace
) : JsonStream.Streamable {
@@ -18,6 +19,7 @@ class ThreadInternal internal constructor(
writer.name("id").value(id)
writer.name("name").value(name)
writer.name("type").value(type.desc)
+ writer.name("state").value(state.descriptor)
writer.name("stacktrace")
writer.beginArray()
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt
index ea27eb92a8..06e00268ac 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ThreadState.kt
@@ -67,7 +67,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc
if (trace != null) {
val stacktrace = Stacktrace(trace, projectPackages, logger)
val errorThread = thread.id == currentThreadId
- Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, stacktrace, logger)
+ Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, Thread.State.forThread(thread), stacktrace, logger)
} else {
null
}
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt
index a06f233667..fcc4e48c72 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt
@@ -40,7 +40,7 @@ internal class EventSerializationTest {
createEvent {
val stacktrace = Stacktrace(arrayOf(), emptySet(), NoopLogger)
it.threads.clear()
- it.threads.add(Thread(5, "main", ThreadType.ANDROID, true, stacktrace, NoopLogger))
+ it.threads.add(Thread(5, "main", ThreadType.ANDROID, true, Thread.State.RUNNABLE, stacktrace, NoopLogger))
},
// threads included
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadFacadeTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadFacadeTest.java
index 1f4c3b5cb6..e0c747b409 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadFacadeTest.java
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadFacadeTest.java
@@ -27,7 +27,15 @@ public void setUp() {
logger = new InterceptingLogger();
List frames = Collections.emptyList();
stacktrace = new Stacktrace(frames);
- thread = new Thread(1, "thread-2", ThreadType.ANDROID, false, stacktrace, logger);
+ thread = new Thread(
+ 1,
+ "thread-2",
+ ThreadType.ANDROID,
+ false,
+ Thread.State.RUNNABLE,
+ stacktrace,
+ logger
+ );
}
@Test
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt
index c60b0bf50d..41e2cc0506 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ThreadSerializationTest.kt
@@ -24,6 +24,7 @@ internal class ThreadSerializationTest {
"main-one",
ThreadType.ANDROID,
true,
+ Thread.State.RUNNABLE,
Stacktrace(
stacktrace,
emptySet(),
@@ -43,6 +44,7 @@ internal class ThreadSerializationTest {
"main-one",
ThreadType.ANDROID,
false,
+ Thread.State.RUNNABLE,
Stacktrace(
stacktrace1,
emptySet(),
@@ -76,6 +78,7 @@ internal class ThreadSerializationTest {
"main-one",
ThreadType.ANDROID,
true,
+ Thread.State.RUNNABLE,
trace,
NoopLogger
)
@@ -98,6 +101,7 @@ internal class ThreadSerializationTest {
"main-one",
ThreadType.ANDROID,
false,
+ Thread.State.RUNNABLE,
trace,
NoopLogger
)
diff --git a/bugsnag-android-core/src/test/resources/event_serialization_5.json b/bugsnag-android-core/src/test/resources/event_serialization_5.json
index 3b016294e0..b4e2dc7a76 100644
--- a/bugsnag-android-core/src/test/resources/event_serialization_5.json
+++ b/bugsnag-android-core/src/test/resources/event_serialization_5.json
@@ -37,6 +37,7 @@
"id": 5,
"name": "main",
"type": "android",
+ "state": "RUNNABLE",
"stacktrace": [],
"errorReportingThread": true
}
diff --git a/bugsnag-android-core/src/test/resources/thread_serialization_0.json b/bugsnag-android-core/src/test/resources/thread_serialization_0.json
index ee01654e3e..d6a34a802d 100644
--- a/bugsnag-android-core/src/test/resources/thread_serialization_0.json
+++ b/bugsnag-android-core/src/test/resources/thread_serialization_0.json
@@ -2,6 +2,7 @@
"id": 24,
"name": "main-one",
"type": "android",
+ "state": "RUNNABLE",
"stacktrace": [
{
"method": "run_func",
diff --git a/bugsnag-android-core/src/test/resources/thread_serialization_1.json b/bugsnag-android-core/src/test/resources/thread_serialization_1.json
index 654bd10fe4..ec5ffc71b9 100644
--- a/bugsnag-android-core/src/test/resources/thread_serialization_1.json
+++ b/bugsnag-android-core/src/test/resources/thread_serialization_1.json
@@ -2,6 +2,7 @@
"id": 24,
"name": "main-one",
"type": "android",
+ "state": "RUNNABLE",
"stacktrace": [
{
"method": "run_func",
diff --git a/bugsnag-android-core/src/test/resources/thread_serialization_2.json b/bugsnag-android-core/src/test/resources/thread_serialization_2.json
index ee01654e3e..d6a34a802d 100644
--- a/bugsnag-android-core/src/test/resources/thread_serialization_2.json
+++ b/bugsnag-android-core/src/test/resources/thread_serialization_2.json
@@ -2,6 +2,7 @@
"id": 24,
"name": "main-one",
"type": "android",
+ "state": "RUNNABLE",
"stacktrace": [
{
"method": "run_func",
diff --git a/bugsnag-android-core/src/test/resources/thread_serialization_3.json b/bugsnag-android-core/src/test/resources/thread_serialization_3.json
index 654bd10fe4..ec5ffc71b9 100644
--- a/bugsnag-android-core/src/test/resources/thread_serialization_3.json
+++ b/bugsnag-android-core/src/test/resources/thread_serialization_3.json
@@ -2,6 +2,7 @@
"id": 24,
"name": "main-one",
"type": "android",
+ "state": "RUNNABLE",
"stacktrace": [
{
"method": "run_func",
diff --git a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java
index 4cddfc3672..6259e5f62c 100644
--- a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java
+++ b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadDeserializer.java
@@ -32,6 +32,7 @@ public Thread deserialize(Map map) {
MapUtils.getOrThrow(map, "name"),
ThreadType.valueOf(type.toUpperCase(Locale.US)),
errorReportingThread,
+ Thread.State.byDescriptor(MapUtils.getOrThrow(map, "state")),
new Stacktrace(frames),
logger
);
diff --git a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadSerializer.kt b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadSerializer.kt
index 25e3c1fd6f..fd046df89c 100644
--- a/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadSerializer.kt
+++ b/bugsnag-plugin-react-native/src/main/java/com/bugsnag/android/ThreadSerializer.kt
@@ -8,6 +8,7 @@ internal class ThreadSerializer : MapSerializer {
map["name"] = thread.name
map["type"] = thread.type.toString().toLowerCase(Locale.US)
map["errorReportingThread"] = thread.errorReportingThread
+ map["state"] = thread.state.descriptor
map["stacktrace"] = thread.stacktrace.map {
val frame = mutableMapOf()
diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt
index 0f4141c3a2..767b6f33f4 100644
--- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt
+++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/EventDeserializerTest.kt
@@ -56,6 +56,7 @@ class EventDeserializerTest {
"id" to 52L,
"type" to "reactnativejs",
"name" to "thread-worker-02",
+ "state" to "RUNNABLE",
"errorReportingThread" to true
)
diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt
index 71e4ac1db0..f2018e03b4 100644
--- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt
+++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadDeserializerTest.kt
@@ -25,6 +25,7 @@ class ThreadDeserializerTest {
map["id"] = 52L
map["type"] = "reactnativejs"
map["name"] = "thread-worker-02"
+ map["state"] = "RUNNABLE"
map["errorReportingThread"] = true
}
diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadSerializerTest.java b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadSerializerTest.java
index 319cfadd22..2060ba0ecc 100644
--- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadSerializerTest.java
+++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/ThreadSerializerTest.java
@@ -37,7 +37,7 @@ public void setup() {
List frames = Collections.singletonList(stackframe);
Stacktrace stacktrace = new Stacktrace(frames);
thread = new Thread(1, "fake-thread", ThreadType.ANDROID,
- true, stacktrace, NoopLogger.INSTANCE);
+ true, Thread.State.RUNNABLE, stacktrace, NoopLogger.INSTANCE);
}
@Test