From ba37e8604e79c1109ad4c03d1af048f9381d8a7c Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 8 Jul 2026 09:55:56 +0200 Subject: [PATCH 1/6] fix(anr): Detect main thread from tombstone via pid matching thread id On Linux/Android the main thread's kernel thread id always equals the process id, so match a tombstone thread whose id equals the pid as the main thread and normalize its name back to "main". This is more robust than relying on the OS-assigned thread name, which some versions/OEMs replace with the process name. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../internal/tombstone/TombstoneParser.java | 8 ++ .../internal/tombstone/TombstoneParserTest.kt | 82 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java index 1f142b52c9a..951cf33d085 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java @@ -114,6 +114,14 @@ private List createThreads( // the backend currently requires a stack-trace in exception exc.setStacktrace(stacktrace); } + + // Android/Linux convention: the pid of the process is the same as the tid of the main thread. + // The main thread is special in that it is the only thread that can create a Looper and thus + // handle messages on the main thread + if (tombstone.pid == threadEntryValue.id) { + thread.setName("main"); + thread.setMain(true); + } threads.add(thread); } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt index 34e704188c4..8644c214f8b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt @@ -12,6 +12,7 @@ import java.io.StringWriter import java.util.zip.GZIPInputStream import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import org.mockito.kotlin.mock @@ -99,6 +100,13 @@ class TombstoneParserTest { // threads assertEquals(62, event.threads!!.size) + + // exactly one thread is the main thread (its id equals the process id, 21891) and its name is + // normalized to "main" + val mainThread = event.threads!!.single { it.isMain == true } + assertEquals(21891, mainThread.id) + assertEquals("main", mainThread.name) + for (thread in event.threads!!) { assertNotNull(thread.id) if (thread.id == crashedThreadId) { @@ -397,6 +405,80 @@ class TombstoneParserTest { assertEquals(expectedJson, actualJson) } + @Test + fun `identifies the main thread via pid matching the thread id and normalizes its name`() { + // On Linux/Android the main thread's kernel thread id equals the process id (pid), so we use + // that to detect the main thread even when the OS renamed it to the (truncated) process name. + // The crashed thread (tid) is a different thread here to verify the two are handled + // independently. + val tombstone = + Tombstone.Builder() + .pid(1000) + .tid(2000) + .signal(Signal(11, "SIGSEGV", 1, "SEGV_MAPERR", false, 0, 0, false, 0, null)) + // main thread: id == pid, but the OS renamed it to the process name + .addThread( + TombstoneThread( + 1000, + "io.sentry.samples.android", + emptyList(), + emptyList(), + emptyList(), + listOf(BacktraceFrame(0, 0x100, 0, "main", 0, "/system/lib64/libc.so", 0, "")), + emptyList(), + 0, + 0, + ) + ) + // crashed thread: id == tid + .addThread( + TombstoneThread( + 2000, + "crashed-worker", + emptyList(), + emptyList(), + emptyList(), + listOf(BacktraceFrame(0, 0x200, 0, "crash", 0, "/system/lib64/libc.so", 0, "")), + emptyList(), + 0, + 0, + ) + ) + // background thread: neither main nor crashed + .addThread( + TombstoneThread( + 3000, + "Thread-3", + emptyList(), + emptyList(), + emptyList(), + listOf(BacktraceFrame(0, 0x300, 0, "work", 0, "/system/lib64/libc.so", 0, "")), + emptyList(), + 0, + 0, + ) + ) + .build() + + val event = parser.parse(tombstone) + val threads = event.threads!! + + val main = threads.single { it.isMain == true } + assertEquals(1000, main.id) + // the OS-assigned process name is normalized back to "main" + assertEquals("main", main.name) + + val crashed = threads.single { it.isCrashed == true } + assertEquals(2000, crashed.id) + assertNotEquals(true, crashed.isMain) + assertEquals("crashed-worker", crashed.name) + + val background = threads.single { it.id == 3000L } + assertNotEquals(true, background.isMain) + assertNotEquals(true, background.isCrashed) + assertEquals("Thread-3", background.name) + } + @Test fun `parses tombstone when nativeLibraryDir is null`() { val tombstoneStream = From 157346e281604b3eaa68d28822b0966398afc7a3 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 8 Jul 2026 09:56:52 +0200 Subject: [PATCH 2/6] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e05a7a1af38..4db5aed2677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ ### Fixes +- Fix main thread identification for tombstone (native crash) events ([#5742](https://github.com/getsentry/sentry-java/pull/5742)) - Fix main thread identification parsing for ApplicationExitInfo ANRs ([#5733](https://github.com/getsentry/sentry-java/pull/5733)) - Do not send threads without stacktraces for ApplicationExitInfo ANRs ([#5733](https://github.com/getsentry/sentry-java/pull/5733)) - Record byte-level client reports when event processors discard logs or trace metrics ([#5718](https://github.com/getsentry/sentry-java/pull/5718)) From c2d4bc580013d4d0834dae2b725f554e4f36bd02 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 8 Jul 2026 10:08:18 +0200 Subject: [PATCH 3/6] Clarify main thread detection comments --- .../android/core/internal/tombstone/TombstoneParser.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java index 951cf33d085..dab4902636b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java @@ -115,10 +115,11 @@ private List createThreads( exc.setStacktrace(stacktrace); } - // Android/Linux convention: the pid of the process is the same as the tid of the main thread. - // The main thread is special in that it is the only thread that can create a Looper and thus - // handle messages on the main thread + // thread id always equals the process id, + // so we use it to reliably detect the main thread if (tombstone.pid == threadEntryValue.id) { + // the OS may provides a (truncated) process name; normalize it + // back to "main" so downstream consumers see a consistent name thread.setName("main"); thread.setMain(true); } From ea41fe26d0e9cd3ac3bc34f9236756a414d436db Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 8 Jul 2026 10:09:55 +0200 Subject: [PATCH 4/6] Fix typo in main thread detection comment --- .../sentry/android/core/internal/tombstone/TombstoneParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java index dab4902636b..c3966615899 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java @@ -118,7 +118,7 @@ private List createThreads( // thread id always equals the process id, // so we use it to reliably detect the main thread if (tombstone.pid == threadEntryValue.id) { - // the OS may provides a (truncated) process name; normalize it + // the OS may provide a (truncated) process name; normalize it // back to "main" so downstream consumers see a consistent name thread.setName("main"); thread.setMain(true); From c1c007cb39cc815216652d519753b6351b6c3df8 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 8 Jul 2026 10:19:02 +0200 Subject: [PATCH 5/6] Remove redundant comments in tombstone parser test --- .../core/internal/tombstone/TombstoneParserTest.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt index 8644c214f8b..70fc48fd9be 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt @@ -100,9 +100,6 @@ class TombstoneParserTest { // threads assertEquals(62, event.threads!!.size) - - // exactly one thread is the main thread (its id equals the process id, 21891) and its name is - // normalized to "main" val mainThread = event.threads!!.single { it.isMain == true } assertEquals(21891, mainThread.id) assertEquals("main", mainThread.name) @@ -407,10 +404,6 @@ class TombstoneParserTest { @Test fun `identifies the main thread via pid matching the thread id and normalizes its name`() { - // On Linux/Android the main thread's kernel thread id equals the process id (pid), so we use - // that to detect the main thread even when the OS renamed it to the (truncated) process name. - // The crashed thread (tid) is a different thread here to verify the two are handled - // independently. val tombstone = Tombstone.Builder() .pid(1000) @@ -430,7 +423,6 @@ class TombstoneParserTest { 0, ) ) - // crashed thread: id == tid .addThread( TombstoneThread( 2000, @@ -444,7 +436,6 @@ class TombstoneParserTest { 0, ) ) - // background thread: neither main nor crashed .addThread( TombstoneThread( 3000, @@ -465,7 +456,6 @@ class TombstoneParserTest { val main = threads.single { it.isMain == true } assertEquals(1000, main.id) - // the OS-assigned process name is normalized back to "main" assertEquals("main", main.name) val crashed = threads.single { it.isCrashed == true } From 6d1e6479cfc857ee1c0bf1719fbe7ff048df00b2 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 8 Jul 2026 10:37:22 +0200 Subject: [PATCH 6/6] Fix failing tests --- .../java/io/sentry/android/core/TombstoneIntegrationTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt index 9890d553dbc..e3e88d04f7a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt @@ -75,7 +75,8 @@ class TombstoneIntegrationTest : ApplicationExitIntegrationTestBase thread.id == crashedThreadId } - assertEquals("samples.android", crashedThread!!.name) + assertEquals("main", crashedThread!!.name) + assertTrue(crashedThread.isMain!!) assertTrue(crashedThread.isCrashed!!) // Verify that frames from the app's native library are marked as in-app