Skip to content

Commit d99fe33

Browse files
markushiclaude
andauthored
fix(anr): Detect main thread from tombstone via pid matching thread id (#5742)
* 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) <noreply@anthropic.com> * changelog * Clarify main thread detection comments * Fix typo in main thread detection comment * Remove redundant comments in tombstone parser test * Fix failing tests --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d12e9b5 commit d99fe33

4 files changed

Lines changed: 87 additions & 1 deletion

File tree

CHANGELOG.md

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

33
## Unreleased
44

5+
### Fixes
6+
7+
- Fix main thread identification for tombstone (native crash) events ([#5742](https://github.com/getsentry/sentry-java/pull/5742))
8+
59
### Dependencies
610

711
- Bump Native SDK from v0.15.2 to v0.15.3 ([#5728](https://github.com/getsentry/sentry-java/pull/5728))

sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ private List<SentryThread> createThreads(
114114
// the backend currently requires a stack-trace in exception
115115
exc.setStacktrace(stacktrace);
116116
}
117+
118+
// thread id always equals the process id,
119+
// so we use it to reliably detect the main thread
120+
if (tombstone.pid == threadEntryValue.id) {
121+
// the OS may provide a (truncated) process name; normalize it
122+
// back to "main" so downstream consumers see a consistent name
123+
thread.setName("main");
124+
thread.setMain(true);
125+
}
117126
threads.add(thread);
118127
}
119128

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ class TombstoneIntegrationTest : ApplicationExitIntegrationTestBase<TombstoneHin
7575
val crashedThreadId = 21891L
7676
assertEquals(crashedThreadId, event.exceptions!![0].threadId)
7777
val crashedThread = event.threads!!.find { thread -> thread.id == crashedThreadId }
78-
assertEquals("samples.android", crashedThread!!.name)
78+
assertEquals("main", crashedThread!!.name)
79+
assertTrue(crashedThread.isMain!!)
7980
assertTrue(crashedThread.isCrashed!!)
8081

8182
// Verify that frames from the app's native library are marked as in-app

sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import java.io.StringWriter
1212
import java.util.zip.GZIPInputStream
1313
import kotlin.test.Test
1414
import kotlin.test.assertEquals
15+
import kotlin.test.assertNotEquals
1516
import kotlin.test.assertNotNull
1617
import org.mockito.kotlin.mock
1718

@@ -99,6 +100,10 @@ class TombstoneParserTest {
99100

100101
// threads
101102
assertEquals(62, event.threads!!.size)
103+
val mainThread = event.threads!!.single { it.isMain == true }
104+
assertEquals(21891, mainThread.id)
105+
assertEquals("main", mainThread.name)
106+
102107
for (thread in event.threads!!) {
103108
assertNotNull(thread.id)
104109
if (thread.id == crashedThreadId) {
@@ -397,6 +402,73 @@ class TombstoneParserTest {
397402
assertEquals(expectedJson, actualJson)
398403
}
399404

405+
@Test
406+
fun `identifies the main thread via pid matching the thread id and normalizes its name`() {
407+
val tombstone =
408+
Tombstone.Builder()
409+
.pid(1000)
410+
.tid(2000)
411+
.signal(Signal(11, "SIGSEGV", 1, "SEGV_MAPERR", false, 0, 0, false, 0, null))
412+
// main thread: id == pid, but the OS renamed it to the process name
413+
.addThread(
414+
TombstoneThread(
415+
1000,
416+
"io.sentry.samples.android",
417+
emptyList(),
418+
emptyList(),
419+
emptyList(),
420+
listOf(BacktraceFrame(0, 0x100, 0, "main", 0, "/system/lib64/libc.so", 0, "")),
421+
emptyList(),
422+
0,
423+
0,
424+
)
425+
)
426+
.addThread(
427+
TombstoneThread(
428+
2000,
429+
"crashed-worker",
430+
emptyList(),
431+
emptyList(),
432+
emptyList(),
433+
listOf(BacktraceFrame(0, 0x200, 0, "crash", 0, "/system/lib64/libc.so", 0, "")),
434+
emptyList(),
435+
0,
436+
0,
437+
)
438+
)
439+
.addThread(
440+
TombstoneThread(
441+
3000,
442+
"Thread-3",
443+
emptyList(),
444+
emptyList(),
445+
emptyList(),
446+
listOf(BacktraceFrame(0, 0x300, 0, "work", 0, "/system/lib64/libc.so", 0, "")),
447+
emptyList(),
448+
0,
449+
0,
450+
)
451+
)
452+
.build()
453+
454+
val event = parser.parse(tombstone)
455+
val threads = event.threads!!
456+
457+
val main = threads.single { it.isMain == true }
458+
assertEquals(1000, main.id)
459+
assertEquals("main", main.name)
460+
461+
val crashed = threads.single { it.isCrashed == true }
462+
assertEquals(2000, crashed.id)
463+
assertNotEquals(true, crashed.isMain)
464+
assertEquals("crashed-worker", crashed.name)
465+
466+
val background = threads.single { it.id == 3000L }
467+
assertNotEquals(true, background.isMain)
468+
assertNotEquals(true, background.isCrashed)
469+
assertEquals("Thread-3", background.name)
470+
}
471+
400472
@Test
401473
fun `parses tombstone when nativeLibraryDir is null`() {
402474
val tombstoneStream =

0 commit comments

Comments
 (0)