From 4330409a2d6406f5c8a009da64f29997b53b7a9e Mon Sep 17 00:00:00 2001 From: "markus.hintersteiner" Date: Tue, 7 Jul 2026 10:01:45 +0000 Subject: [PATCH 1/9] fix(anr): detect main thread when OS names it after the process On some Android versions/OEMs, AppExitInfo's thread dump names the main thread after the process name (e.g. 'com.example.app') instead of 'main'. ThreadDumpParser previously only matched on 'main', causing the thread to not be flagged as main/crashed and resulting in ANR events with no usable stack trace. Pass the process name from ApplicationExitInfo.getProcessName() to ThreadDumpParser and treat a thread as main if its name equals 'main' OR the process name. Fixes JAVA-635 Co-Authored-By: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> --- .../sentry/android/core/AnrV2Integration.java | 3 ++- .../internal/threaddump/ThreadDumpParser.java | 14 ++++++++++- .../threaddump/ThreadDumpParserTest.kt | 23 ++++++++++++++++++ .../thread_dump_process_name_main.txt | 24 +++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 sentry-android-core/src/test/resources/thread_dump_process_name_main.txt diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 285c3b77ade..2ad3b788097 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -208,7 +208,8 @@ public boolean shouldReportHistorical() { new BufferedReader(new InputStreamReader(new ByteArrayInputStream(dump)))) { final Lines lines = Lines.readLines(reader); - final ThreadDumpParser threadDumpParser = new ThreadDumpParser(options, isBackground); + final ThreadDumpParser threadDumpParser = + new ThreadDumpParser(options, isBackground, exitInfo.getProcessName()); threadDumpParser.parse(lines); final @NotNull List threads = threadDumpParser.getThreads(); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java index f5ce8a745ce..3fb0109a4d9 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java @@ -104,6 +104,8 @@ public class ThreadDumpParser { private final boolean isBackground; + private final @Nullable String processName; + private final @NotNull SentryStackTraceFactory stackTraceFactory; private final @NotNull Map debugImages; @@ -113,8 +115,16 @@ public class ThreadDumpParser { private final @NotNull ArtContextParser artContextParser = new ArtContextParser(); public ThreadDumpParser(final @NotNull SentryOptions options, final boolean isBackground) { + this(options, isBackground, null); + } + + public ThreadDumpParser( + final @NotNull SentryOptions options, + final boolean isBackground, + final @Nullable String processName) { this.options = options; this.isBackground = isBackground; + this.processName = processName; this.stackTraceFactory = new SentryStackTraceFactory(options); this.debugImages = new HashMap<>(); this.threads = new ArrayList<>(); @@ -209,7 +219,9 @@ private SentryThread parseThread(final @NotNull Lines lines) { final String threadName = sentryThread.getName(); if (threadName != null) { - final boolean isMain = threadName.equals("main"); + final boolean isMain = + threadName.equals("main") + || (processName != null && threadName.equals(processName)); sentryThread.setMain(isMain); // since it's an ANR, the crashed thread will always be main sentryThread.setCrashed(isMain); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt index b7db35b63ce..f7bc27072d7 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt @@ -182,6 +182,29 @@ class ThreadDumpParserTest { assertEquals(8.054, artContext.gcWaitingTime) } + @Test + fun `detects main thread when OS names it after the process`() { + val lines = + Lines.readLines(File("src/test/resources/thread_dump_process_name_main.txt")) + val parser = + ThreadDumpParser( + SentryOptions().apply { addInAppInclude("io.sentry.samples") }, + false, + "io.sentry.samples.android", + ) + parser.parse(lines) + val threads = parser.threads + val main = threads.find { it.name == "io.sentry.samples.android" } + assertNotNull(main) + assertEquals(true, main!!.isMain) + assertEquals(true, main.isCrashed) + assertEquals(true, main.isCurrent) + val background = threads.find { it.name == "Thread-2" } + assertNotNull(background) + assertEquals(false, background!!.isMain) + assertEquals(false, background.isCrashed) + } + @Test fun `thread dump garbage`() { val lines = Lines.readLines(File("src/test/resources/thread_dump_bad_data.txt")) diff --git a/sentry-android-core/src/test/resources/thread_dump_process_name_main.txt b/sentry-android-core/src/test/resources/thread_dump_process_name_main.txt new file mode 100644 index 00000000000..80b864aae73 --- /dev/null +++ b/sentry-android-core/src/test/resources/thread_dump_process_name_main.txt @@ -0,0 +1,24 @@ + +----- pid 12345 at 2024-01-01 10:00:00.000000000+0000 ----- +Cmd line: io.sentry.samples.android +Build fingerprint: 'google/sdk_gphone64_arm64/emu64a:13/TE1A.220922.012/9302419:userdebug/dev-keys' +ABI: 'arm64' + +DALVIK THREADS (2): +"io.sentry.samples.android" prio=5 tid=1 Runnable + | group="main" sCount=0 ucsCount=0 flags=0 obj=0x72a985e0 self=0xb400007cabc57380 + | sysTid=12345 nice=-10 cgrp=top-app sched=0/0 handle=0x7deceb74f8 + | state=R schedstat=( 324804784 183300334 997 ) utm=23 stm=8 core=3 HZ=100 + | stack=0x7ff93a9000-0x7ff93ab000 stackSize=8188KB + | held mutexes= + at io.sentry.samples.android.MainActivity.onCreate(MainActivity.java:42) + +"Thread-2" prio=5 tid=2 Sleeping + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0518 self=0xb400007cabc82ad0 + | sysTid=12346 nice=0 cgrp=top-app sched=0/0 handle=0x7ace0a9cb0 + | state=S schedstat=( 574039 4838087 11 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7acdfb2000-0x7acdfb4000 stackSize=991KB + | held mutexes= + at java.lang.Thread.sleep(Native method) + at io.sentry.samples.android.BackgroundWorker.run(BackgroundWorker.java:20) + From afcbab44ddde0fb464ab5b92a2aeb8e2affa09a8 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 7 Jul 2026 10:06:07 +0000 Subject: [PATCH 2/9] Format code --- .../android/core/internal/threaddump/ThreadDumpParser.java | 3 +-- .../android/core/internal/threaddump/ThreadDumpParserTest.kt | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java index 3fb0109a4d9..376fa2ba605 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java @@ -220,8 +220,7 @@ private SentryThread parseThread(final @NotNull Lines lines) { final String threadName = sentryThread.getName(); if (threadName != null) { final boolean isMain = - threadName.equals("main") - || (processName != null && threadName.equals(processName)); + threadName.equals("main") || (processName != null && threadName.equals(processName)); sentryThread.setMain(isMain); // since it's an ANR, the crashed thread will always be main sentryThread.setCrashed(isMain); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt index f7bc27072d7..ea6623d41bb 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt @@ -184,8 +184,7 @@ class ThreadDumpParserTest { @Test fun `detects main thread when OS names it after the process`() { - val lines = - Lines.readLines(File("src/test/resources/thread_dump_process_name_main.txt")) + val lines = Lines.readLines(File("src/test/resources/thread_dump_process_name_main.txt")) val parser = ThreadDumpParser( SentryOptions().apply { addInAppInclude("io.sentry.samples") }, From fd4f335100a4ce2c912c4652d04aa6dcc024e3ad Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 7 Jul 2026 12:17:04 +0200 Subject: [PATCH 3/9] ref(anr): Detect main thread via sysTid matching process id The main thread can be renamed by the OS to the (potentially truncated) process name, so relying on the process name is fragile. On Linux/Android the main thread's kernel thread id (sysTid) always equals the process id, so use that as the authoritative signal instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../sentry/android/core/AnrV2Integration.java | 3 +- .../internal/threaddump/ThreadDumpParser.java | 48 ++++++++++++------- .../threaddump/ThreadDumpParserTest.kt | 10 ++-- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 2ad3b788097..285c3b77ade 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -208,8 +208,7 @@ public boolean shouldReportHistorical() { new BufferedReader(new InputStreamReader(new ByteArrayInputStream(dump)))) { final Lines lines = Lines.readLines(reader); - final ThreadDumpParser threadDumpParser = - new ThreadDumpParser(options, isBackground, exitInfo.getProcessName()); + final ThreadDumpParser threadDumpParser = new ThreadDumpParser(options, isBackground); threadDumpParser.parse(lines); final @NotNull List threads = threadDumpParser.getThreads(); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java index 376fa2ba605..762513292c1 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java @@ -45,6 +45,12 @@ public class ThreadDumpParser { private static final Pattern BEGIN_UNMANAGED_NATIVE_THREAD_RE = Pattern.compile("\"(.*)\" (.*) ?sysTid=(\\d+)"); + // e.g. "----- pid 12345 at 2024-01-01 10:00:00.000000000+0000 -----" + private static final Pattern PID_RE = Pattern.compile("----- pid (\\d+) at .*"); + + // e.g. " | sysTid=12345 nice=-10 cgrp=top-app sched=0/0 handle=0x7deceb74f8" + private static final Pattern SYS_TID_RE = Pattern.compile("\\s*\\|\\s*sysTid=(\\d+).*"); + // For reference, see native_stack_dump.cc and tombstone_proto_to_text.cpp in Android sources // Groups // 0:entire regex @@ -104,7 +110,10 @@ public class ThreadDumpParser { private final boolean isBackground; - private final @Nullable String processName; + // the process id parsed from the thread dump header; on Linux/Android the main thread's kernel + // thread id (sysTid) always equals the process id, so we use it to reliably detect the main + // thread + private @Nullable Long processId; private final @NotNull SentryStackTraceFactory stackTraceFactory; @@ -115,16 +124,8 @@ public class ThreadDumpParser { private final @NotNull ArtContextParser artContextParser = new ArtContextParser(); public ThreadDumpParser(final @NotNull SentryOptions options, final boolean isBackground) { - this(options, isBackground, null); - } - - public ThreadDumpParser( - final @NotNull SentryOptions options, - final boolean isBackground, - final @Nullable String processName) { this.options = options; this.isBackground = isBackground; - this.processName = processName; this.stackTraceFactory = new SentryStackTraceFactory(options); this.debugImages = new HashMap<>(); this.threads = new ArrayList<>(); @@ -149,6 +150,7 @@ public void parse(final @NotNull Lines lines) { final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE.matcher(""); final Matcher beginUnmanagedNativeThreadRe = BEGIN_UNMANAGED_NATIVE_THREAD_RE.matcher(""); + final Matcher pidRe = PID_RE.matcher(""); while (lines.hasNext()) { final Line line = lines.next(); @@ -166,6 +168,8 @@ public void parse(final @NotNull Lines lines) { if (thread != null) { threads.add(thread); } + } else if (matches(pidRe, text)) { + processId = getLong(pidRe, 1, null); } else { artContextParser.parseLine(text); } @@ -219,12 +223,10 @@ private SentryThread parseThread(final @NotNull Lines lines) { final String threadName = sentryThread.getName(); if (threadName != null) { - final boolean isMain = - threadName.equals("main") || (processName != null && threadName.equals(processName)); - sentryThread.setMain(isMain); - // since it's an ANR, the crashed thread will always be main - sentryThread.setCrashed(isMain); - sentryThread.setCurrent(isMain && !isBackground); + // the OS usually names the main thread "main", but it may also rename it to the process name + // (which can be truncated), so this is only a first guess - the sysTid==processId check in + // parseStacktrace is the authoritative signal and may still mark another thread as main + setMainThread(sentryThread, threadName.equals("main")); } // thread stacktrace @@ -249,6 +251,7 @@ private SentryStackTrace parseStacktrace( final Matcher waitingToLockRe = WAITING_TO_LOCK_RE.matcher(""); final Matcher waitingToLockUnknownRe = WAITING_TO_LOCK_UNKNOWN_RE.matcher(""); final Matcher blankRe = BLANK_RE.matcher(""); + final Matcher sysTidRe = SYS_TID_RE.matcher(""); while (lines.hasNext()) { final Line line = lines.next(); @@ -257,7 +260,13 @@ private SentryStackTrace parseStacktrace( break; } final String text = line.text; - if (matches(javaRe, text)) { + if (matches(sysTidRe, text)) { + // on Linux/Android the kernel thread id of the main thread always equals the process id + final Long sysTid = getLong(sysTidRe, 1, null); + if (sysTid != null && sysTid.equals(processId)) { + setMainThread(thread, true); + } + } else if (matches(javaRe, text)) { final SentryStackFrame frame = new SentryStackFrame(); final String packageName = javaRe.group(1); final String className = javaRe.group(2); @@ -376,6 +385,13 @@ private SentryStackTrace parseStacktrace( return stackTrace; } + private void setMainThread(final @NotNull SentryThread thread, final boolean isMain) { + thread.setMain(isMain); + // since it's an ANR, the crashed thread will always be main + thread.setCrashed(isMain); + thread.setCurrent(isMain && !isBackground); + } + private boolean matches(final @NotNull Matcher matcher, final @NotNull String text) { matcher.reset(text); return matcher.matches(); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt index ea6623d41bb..31a50652f77 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt @@ -183,16 +183,14 @@ class ThreadDumpParserTest { } @Test - fun `detects main thread when OS names it after the process`() { + fun `detects main thread via sysTid matching the process id when OS renames it`() { val lines = Lines.readLines(File("src/test/resources/thread_dump_process_name_main.txt")) val parser = - ThreadDumpParser( - SentryOptions().apply { addInAppInclude("io.sentry.samples") }, - false, - "io.sentry.samples.android", - ) + ThreadDumpParser(SentryOptions().apply { addInAppInclude("io.sentry.samples") }, false) parser.parse(lines) val threads = parser.threads + // the main thread has been renamed to the (truncated) process name, but its sysTid equals the + // process id, which is how we detect it val main = threads.find { it.name == "io.sentry.samples.android" } assertNotNull(main) assertEquals(true, main!!.isMain) From b3f6ab008bd9368f03e56b9d33215ae0dea75ae0 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 7 Jul 2026 12:31:53 +0200 Subject: [PATCH 4/9] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 107b916808a..6b140cdcd4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes +- Detect the main thread in AnrV2 thread dumps when the OS renames it to the process name, by matching the thread whose `sysTid` equals the process id ([#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)) - Name the device-info caching thread `SentryDeviceInfoCache` so all threads spawned by the SDK are identifiable ([#5684](https://github.com/getsentry/sentry-java/pull/5684)) - Apply byte-category rate limits to log and trace metric envelope items ([#5716](https://github.com/getsentry/sentry-java/pull/5716)) From e62aea4711676a83df2b96a99195e4093ec87546 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 7 Jul 2026 12:33:31 +0200 Subject: [PATCH 5/9] Simplify changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b140cdcd4d..53eed4410f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ### Fixes -- Detect the main thread in AnrV2 thread dumps when the OS renames it to the process name, by matching the thread whose `sysTid` equals the process id ([#5733](https://github.com/getsentry/sentry-java/pull/5733)) +- Fix main thread identification parsing 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)) - Name the device-info caching thread `SentryDeviceInfoCache` so all threads spawned by the SDK are identifiable ([#5684](https://github.com/getsentry/sentry-java/pull/5684)) - Apply byte-category rate limits to log and trace metric envelope items ([#5716](https://github.com/getsentry/sentry-java/pull/5716)) From e844d0aa9bffb0ed4a02e096c3d5ca2895bb36b6 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 7 Jul 2026 14:45:48 +0200 Subject: [PATCH 6/9] ref(anr): Normalize main thread name and skip threads without a stacktrace Normalize the main thread's name back to "main" once it is identified (the OS may have renamed it to the process name), and drop threads that have no stacktrace as they are not actionable. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../internal/threaddump/ThreadDumpParser.java | 10 +++++++++ .../threaddump/ThreadDumpParserTest.kt | 18 +++++++++++++--- .../resources/thread_dump_no_stacktrace.txt | 21 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 sentry-android-core/src/test/resources/thread_dump_no_stacktrace.txt diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java index 762513292c1..6e87d976d91 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java @@ -231,6 +231,11 @@ private SentryThread parseThread(final @NotNull Lines lines) { // thread stacktrace final SentryStackTrace stackTrace = parseStacktrace(lines, sentryThread); + final List frames = stackTrace.getFrames(); + if (frames == null || frames.isEmpty()) { + // skip threads without a stacktrace, they are not actionable + return null; + } sentryThread.setStacktrace(stackTrace); return sentryThread; } @@ -390,6 +395,11 @@ private void setMainThread(final @NotNull SentryThread thread, final boolean isM // since it's an ANR, the crashed thread will always be main thread.setCrashed(isMain); thread.setCurrent(isMain && !isBackground); + if (isMain) { + // the OS may have renamed the main thread to the (truncated) process name; normalize it back + // to "main" so downstream consumers see a consistent name + thread.setName("main"); + } } private boolean matches(final @NotNull Matcher matcher, final @NotNull String text) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt index 31a50652f77..2b314863cfd 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt @@ -190,10 +190,10 @@ class ThreadDumpParserTest { parser.parse(lines) val threads = parser.threads // the main thread has been renamed to the (truncated) process name, but its sysTid equals the - // process id, which is how we detect it - val main = threads.find { it.name == "io.sentry.samples.android" } + // process id, which is how we detect it - its name is then normalized back to "main" + val main = threads.find { it.isMain == true } assertNotNull(main) - assertEquals(true, main!!.isMain) + assertEquals("main", main!!.name) assertEquals(true, main.isCrashed) assertEquals(true, main.isCurrent) val background = threads.find { it.name == "Thread-2" } @@ -202,6 +202,18 @@ class ThreadDumpParserTest { assertEquals(false, background.isCrashed) } + @Test + fun `skips threads without a stacktrace`() { + val lines = Lines.readLines(File("src/test/resources/thread_dump_no_stacktrace.txt")) + val parser = + ThreadDumpParser(SentryOptions().apply { addInAppInclude("io.sentry.samples") }, false) + parser.parse(lines) + val threads = parser.threads + // the thread without any frames is skipped, only the one with a stacktrace remains + assertEquals(1, threads.size) + assertEquals("main", threads.first().name) + } + @Test fun `thread dump garbage`() { val lines = Lines.readLines(File("src/test/resources/thread_dump_bad_data.txt")) diff --git a/sentry-android-core/src/test/resources/thread_dump_no_stacktrace.txt b/sentry-android-core/src/test/resources/thread_dump_no_stacktrace.txt new file mode 100644 index 00000000000..e0411327546 --- /dev/null +++ b/sentry-android-core/src/test/resources/thread_dump_no_stacktrace.txt @@ -0,0 +1,21 @@ + +----- pid 12345 at 2024-01-01 10:00:00.000000000+0000 ----- +Cmd line: io.sentry.samples.android +Build fingerprint: 'google/sdk_gphone64_arm64/emu64a:13/TE1A.220922.012/9302419:userdebug/dev-keys' +ABI: 'arm64' + +DALVIK THREADS (2): +"main" prio=5 tid=1 Runnable + | group="main" sCount=0 ucsCount=0 flags=0 obj=0x72a985e0 self=0xb400007cabc57380 + | sysTid=12345 nice=-10 cgrp=top-app sched=0/0 handle=0x7deceb74f8 + | state=R schedstat=( 324804784 183300334 997 ) utm=23 stm=8 core=3 HZ=100 + | stack=0x7ff93a9000-0x7ff93ab000 stackSize=8188KB + | held mutexes= + at io.sentry.samples.android.MainActivity.onCreate(MainActivity.java:42) + +"Thread-2" prio=5 tid=2 Sleeping + | group="main" sCount=1 ucsCount=0 flags=1 obj=0x136c0518 self=0xb400007cabc82ad0 + | sysTid=12346 nice=0 cgrp=top-app sched=0/0 handle=0x7ace0a9cb0 + | state=S schedstat=( 574039 4838087 11 ) utm=0 stm=0 core=1 HZ=100 + | stack=0x7acdfb2000-0x7acdfb4000 stackSize=991KB + | held mutexes= From 6ecc3b3d31816c882b04b2ef0c45aa09b0a28edf Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 7 Jul 2026 15:53:55 +0200 Subject: [PATCH 7/9] ref(anr): Detect main thread via sysTid and skip stackless threads Identify the main thread by matching the thread whose kernel thread id (sysTid) equals the process id, falling back to the conventional "main" thread name, and skip threads that have no stacktrace as they are not actionable. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../internal/threaddump/ThreadDumpParser.java | 54 ++++++++++--------- .../threaddump/ThreadDumpParserTest.kt | 11 ++-- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java index 6e87d976d91..c200353faa6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java @@ -174,6 +174,8 @@ public void parse(final @NotNull Lines lines) { artContextParser.parseLine(text); } } + + markThreads(); } private SentryThread parseThread(final @NotNull Lines lines) { @@ -199,7 +201,11 @@ private SentryThread parseThread(final @NotNull Lines lines) { return null; } sentryThread.setId(tid); - sentryThread.setName(beginManagedThreadRe.group(1)); + final String name = beginManagedThreadRe.group(1); + sentryThread.setName(name); + if ("main".equals(name)) { + sentryThread.setMain(true); + } final String state = beginManagedThreadRe.group(5); // sanitizing thread that have more details after their actual state, e.g. // "Native (still starting up)" <- we just need "Native" here @@ -211,25 +217,20 @@ private SentryThread parseThread(final @NotNull Lines lines) { } } } else if (matches(beginUnmanagedNativeThreadRe, line.text)) { - final Long sysTid = getLong(beginUnmanagedNativeThreadRe, 3, null); - if (sysTid == null) { + final Long parsedSysTid = getLong(beginUnmanagedNativeThreadRe, 3, null); + if (parsedSysTid == null) { options.getLogger().log(SentryLevel.DEBUG, "No thread id in the dump, skipping thread."); // tid is required by our protocol return null; } - sentryThread.setId(sysTid); + sentryThread.setId(parsedSysTid); sentryThread.setName(beginUnmanagedNativeThreadRe.group(1)); + if (parsedSysTid.equals(processId)) { + sentryThread.setMain(true); + } } - final String threadName = sentryThread.getName(); - if (threadName != null) { - // the OS usually names the main thread "main", but it may also rename it to the process name - // (which can be truncated), so this is only a first guess - the sysTid==processId check in - // parseStacktrace is the authoritative signal and may still mark another thread as main - setMainThread(sentryThread, threadName.equals("main")); - } - - // thread stacktrace + // thread stacktrace (also captures parsedSysTid for managed threads from the "| sysTid=" line) final SentryStackTrace stackTrace = parseStacktrace(lines, sentryThread); final List frames = stackTrace.getFrames(); if (frames == null || frames.isEmpty()) { @@ -266,10 +267,9 @@ private SentryStackTrace parseStacktrace( } final String text = line.text; if (matches(sysTidRe, text)) { - // on Linux/Android the kernel thread id of the main thread always equals the process id final Long sysTid = getLong(sysTidRe, 1, null); if (sysTid != null && sysTid.equals(processId)) { - setMainThread(thread, true); + thread.setMain(true); } } else if (matches(javaRe, text)) { final SentryStackFrame frame = new SentryStackFrame(); @@ -390,15 +390,21 @@ private SentryStackTrace parseStacktrace( return stackTrace; } - private void setMainThread(final @NotNull SentryThread thread, final boolean isMain) { - thread.setMain(isMain); - // since it's an ANR, the crashed thread will always be main - thread.setCrashed(isMain); - thread.setCurrent(isMain && !isBackground); - if (isMain) { - // the OS may have renamed the main thread to the (truncated) process name; normalize it back - // to "main" so downstream consumers see a consistent name - thread.setName("main"); + private void markThreads() { + for (final @NotNull SentryThread thread : threads) { + if (Boolean.TRUE.equals(thread.isMain())) { + // the OS may have renamed the main thread to the (truncated) process name; normalize it + // back to "main" so downstream consumers see a consistent name + thread.setName("main"); + + // since it's an ANR, the crashed thread will always be main + thread.setCrashed(true); + thread.setCurrent(!isBackground); + } else { + thread.setCrashed(false); + thread.setCurrent(false); + thread.setMain(false); + } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt index 2b314863cfd..ec5dbd58902 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt @@ -100,12 +100,15 @@ class ThreadDumpParserTest { parser.parse(lines) val threads = parser.threads // just verifying a few important threads, as there are many - val thread = threads.find { it.name == "samples.android" } + // the OS named the main thread after the process; it is detected via sysTid==processId (9955) + // and its name is normalized back to "main" + val thread = threads.find { it.isMain == true } assertEquals(9955, thread!!.id) + assertEquals("main", thread.name) assertNull(thread.state) - assertEquals(false, thread.isCrashed) - assertEquals(false, thread.isMain) - assertEquals(false, thread.isCurrent) + assertEquals(true, thread.isCrashed) + assertEquals(true, thread.isMain) + assertEquals(true, thread.isCurrent) // Reverse frames so we can index them with the active frame at index 0 val frames = thread.stacktrace!!.frames!!.reversed() From 99cc520b1f42c8ec90bc733ada39bad4af7aeb4f Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 7 Jul 2026 15:57:19 +0200 Subject: [PATCH 8/9] ref(anr): Clean up ThreadDumpParser Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/internal/threaddump/ThreadDumpParser.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java index c200353faa6..08432d2f41d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/threaddump/ThreadDumpParser.java @@ -217,20 +217,20 @@ private SentryThread parseThread(final @NotNull Lines lines) { } } } else if (matches(beginUnmanagedNativeThreadRe, line.text)) { - final Long parsedSysTid = getLong(beginUnmanagedNativeThreadRe, 3, null); - if (parsedSysTid == null) { + final Long sysTid = getLong(beginUnmanagedNativeThreadRe, 3, null); + if (sysTid == null) { options.getLogger().log(SentryLevel.DEBUG, "No thread id in the dump, skipping thread."); // tid is required by our protocol return null; } - sentryThread.setId(parsedSysTid); + sentryThread.setId(sysTid); sentryThread.setName(beginUnmanagedNativeThreadRe.group(1)); - if (parsedSysTid.equals(processId)) { + if (sysTid.equals(processId)) { sentryThread.setMain(true); } } - // thread stacktrace (also captures parsedSysTid for managed threads from the "| sysTid=" line) + // thread stacktrace final SentryStackTrace stackTrace = parseStacktrace(lines, sentryThread); final List frames = stackTrace.getFrames(); if (frames == null || frames.isEmpty()) { From 512563bf26fe18daf418fbaa437a5af0a412c694 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 7 Jul 2026 21:56:18 +0200 Subject: [PATCH 9/9] Update CHANGELOG.md Co-authored-by: Roman Zavarnitsyn --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53eed4410f6..47b6ffae764 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes - 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)) - Name the device-info caching thread `SentryDeviceInfoCache` so all threads spawned by the SDK are identifiable ([#5684](https://github.com/getsentry/sentry-java/pull/5684)) - Apply byte-category rate limits to log and trace metric envelope items ([#5716](https://github.com/getsentry/sentry-java/pull/5716))