From 24561c8541c165a6e10bbcd0e9b74ea2cb717f31 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 19 Nov 2025 20:26:46 +0100 Subject: [PATCH 1/7] refactor(core): Decouple SentryThreadFactory from SentryOptions --- .../android/core/AnrIntegrationFactory.java | 3 +- .../java/io/sentry/MainEventProcessor.java | 9 ++-- .../java/io/sentry/SentryThreadFactory.java | 50 ++++++++++--------- .../java/io/sentry/MainEventProcessorTest.kt | 16 +++++- .../java/io/sentry/SentryThreadFactoryTest.kt | 35 +++++++------ 5 files changed, 66 insertions(+), 47 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java index cff8f72cd36..130f4d057d7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java @@ -13,7 +13,8 @@ public final class AnrIntegrationFactory { public static Integration create( final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider) { if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.R) { - return new AnrV2Integration(context); + // return new AnrV2Integration(context); + return new AnrIntegration(context); } else { return new AnrIntegration(context); } diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index ddf7b3828ec..5ad19c79e32 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -35,7 +35,7 @@ public MainEventProcessor(final @NotNull SentryOptions options) { new SentryStackTraceFactory(this.options); sentryExceptionFactory = new SentryExceptionFactory(sentryStackTraceFactory); - sentryThreadFactory = new SentryThreadFactory(sentryStackTraceFactory, this.options); + sentryThreadFactory = new SentryThreadFactory(sentryStackTraceFactory); } MainEventProcessor( @@ -255,17 +255,20 @@ private void setThreads(final @NotNull SentryEvent event, final @NotNull Hint hi if (options.isAttachThreads() || HintUtils.hasType(hint, AbnormalExit.class)) { final Object sentrySdkHint = HintUtils.getSentrySdkHint(hint); boolean ignoreCurrentThread = false; + boolean attachStacktrace = options.isAttachStacktrace(); if (sentrySdkHint instanceof AbnormalExit) { ignoreCurrentThread = ((AbnormalExit) sentrySdkHint).ignoreCurrentThread(); + attachStacktrace = true; } event.setThreads( - sentryThreadFactory.getCurrentThreads(mechanismThreadIds, ignoreCurrentThread)); + sentryThreadFactory.getCurrentThreads( + mechanismThreadIds, ignoreCurrentThread, attachStacktrace)); } else if (options.isAttachStacktrace() && (eventExceptions == null || eventExceptions.isEmpty()) && !isCachedHint(hint)) { // when attachStacktrace is enabled, we attach only the current thread and its stack traces, // if there are no exceptions, exceptions have its own stack traces. - event.setThreads(sentryThreadFactory.getCurrentThread()); + event.setThreads(sentryThreadFactory.getCurrentThread(options.isAttachStacktrace())); } } } diff --git a/sentry/src/main/java/io/sentry/SentryThreadFactory.java b/sentry/src/main/java/io/sentry/SentryThreadFactory.java index 8af5f0aa0c4..0b8f2584996 100644 --- a/sentry/src/main/java/io/sentry/SentryThreadFactory.java +++ b/sentry/src/main/java/io/sentry/SentryThreadFactory.java @@ -20,21 +20,14 @@ public final class SentryThreadFactory { /** the SentryStackTraceFactory */ private final @NotNull SentryStackTraceFactory sentryStackTraceFactory; - /** the SentryOptions. */ - private final @NotNull SentryOptions options; - /** * ctor SentryThreadFactory that takes a SentryStackTraceFactory * * @param sentryStackTraceFactory the SentryStackTraceFactory - * @param options the SentryOptions */ - public SentryThreadFactory( - final @NotNull SentryStackTraceFactory sentryStackTraceFactory, - final @NotNull SentryOptions options) { + public SentryThreadFactory(final @NotNull SentryStackTraceFactory sentryStackTraceFactory) { this.sentryStackTraceFactory = Objects.requireNonNull(sentryStackTraceFactory, "The SentryStackTraceFactory is required."); - this.options = Objects.requireNonNull(options, "The SentryOptions is required"); } /** @@ -44,12 +37,12 @@ public SentryThreadFactory( * @return a list of SentryThread with a single item or null */ @Nullable - List getCurrentThread() { + List getCurrentThread(final boolean attachStackTrace) { final Map threads = new HashMap<>(); final Thread currentThread = Thread.currentThread(); threads.put(currentThread, currentThread.getStackTrace()); - return getCurrentThreads(threads, null, false); + return getCurrentThreads(threads, null, false, attachStackTrace); } /** @@ -63,13 +56,18 @@ List getCurrentThread() { */ @Nullable List getCurrentThreads( - final @Nullable List mechanismThreadIds, final boolean ignoreCurrentThread) { - return getCurrentThreads(Thread.getAllStackTraces(), mechanismThreadIds, ignoreCurrentThread); + final @Nullable List mechanismThreadIds, + final boolean ignoreCurrentThread, + final boolean attachStackTrace) { + return getCurrentThreads( + Thread.getAllStackTraces(), mechanismThreadIds, ignoreCurrentThread, attachStackTrace); } @Nullable - List getCurrentThreads(final @Nullable List mechanismThreadIds) { - return getCurrentThreads(Thread.getAllStackTraces(), mechanismThreadIds, false); + List getCurrentThreads( + final @Nullable List mechanismThreadIds, final boolean attachStackTrace) { + return getCurrentThreads( + Thread.getAllStackTraces(), mechanismThreadIds, false, attachStackTrace); } /** @@ -87,7 +85,8 @@ List getCurrentThreads(final @Nullable List mechanismThreadI List getCurrentThreads( final @NotNull Map threads, final @Nullable List mechanismThreadIds, - final boolean ignoreCurrentThread) { + final boolean ignoreCurrentThread, + final boolean attachStackTrace) { List result = null; final Thread currentThread = Thread.currentThread(); @@ -109,7 +108,7 @@ List getCurrentThreads( && mechanismThreadIds.contains(thread.getId()) && !ignoreCurrentThread); - result.add(getSentryThread(crashed, item.getValue(), item.getKey())); + result.add(getSentryThread(crashed, item.getValue(), item.getKey(), attachStackTrace)); } } @@ -127,7 +126,8 @@ List getCurrentThreads( private @NotNull SentryThread getSentryThread( final boolean crashed, final @NotNull StackTraceElement[] stackFramesElements, - final @NotNull Thread thread) { + final @NotNull Thread thread, + final boolean attachStacktrace) { final SentryThread sentryThread = new SentryThread(); sentryThread.setName(thread.getName()); @@ -137,15 +137,17 @@ List getCurrentThreads( sentryThread.setState(thread.getState().name()); sentryThread.setCrashed(crashed); - final List frames = - sentryStackTraceFactory.getStackFrames(stackFramesElements, false); + if (attachStacktrace) { + final List frames = + sentryStackTraceFactory.getStackFrames(stackFramesElements, false); - if (options.isAttachStacktrace() && frames != null && !frames.isEmpty()) { - final SentryStackTrace sentryStackTrace = new SentryStackTrace(frames); - // threads are always gotten either via Thread.currentThread() or Thread.getAllStackTraces() - sentryStackTrace.setSnapshot(true); + if (frames != null && !frames.isEmpty()) { + final SentryStackTrace sentryStackTrace = new SentryStackTrace(frames); + // threads are always gotten either via Thread.currentThread() or Thread.getAllStackTraces() + sentryStackTrace.setSnapshot(true); - sentryThread.setStacktrace(sentryStackTrace); + sentryThread.setStacktrace(sentryStackTrace); + } } return sentryThread; diff --git a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt index 33393be2a2b..6fc2c8c243b 100644 --- a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt @@ -249,7 +249,7 @@ class MainEventProcessorTest { } @Test - fun `when attach threads is disabled, but the hint is Abnormal, still sets threads`() { + fun `when attach threads is disabled, but the hint is Abnormal, still sets threads and stacktraces`() { val sut = fixture.getSut(attachThreads = false, attachStackTrace = false) var event = SentryEvent(RuntimeException("error")) @@ -258,6 +258,20 @@ class MainEventProcessorTest { assertNotNull(event.threads) assertEquals(1, event.threads!!.count { it.isCrashed == true }) + assertNotNull(event.threads!!.first().stacktrace) + } + + @Test + fun `when attach threads is enabled, but stacktraces is not its reflected`() { + val sut = fixture.getSut(attachThreads = true, attachStackTrace = false) + + var event = SentryEvent(RuntimeException("error")) + val hint = Hint() + event = sut.process(event, hint) + + assertNotNull(event.threads) + assertEquals(1, event.threads!!.count { it.isCrashed == true }) + assertNull(event.threads!!.first().stacktrace) } @Test diff --git a/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt b/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt index 76a4743c791..c2eda19efc4 100644 --- a/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt @@ -10,13 +10,9 @@ import kotlin.test.assertTrue class SentryThreadFactoryTest { class Fixture { - internal fun getSut(attachStacktrace: Boolean = true) = + internal fun getSut() = SentryThreadFactory( - SentryStackTraceFactory(SentryOptions().apply { addInAppExclude("io.sentry") }), - with(SentryOptions()) { - isAttachStacktrace = attachStacktrace - this - }, + SentryStackTraceFactory(SentryOptions().apply { addInAppExclude("io.sentry") }) ) } @@ -25,26 +21,29 @@ class SentryThreadFactoryTest { @Test fun `when getCurrentThreads is called, not empty result`() { val sut = fixture.getSut() - val threads = sut.getCurrentThreads(null) + val threads = sut.getCurrentThreads(null, false) assertNotEquals(0, threads!!.count()) } @Test fun `when currentThreads is called, current thread is marked crashed`() { val sut = fixture.getSut() - assertEquals(1, sut.getCurrentThreads(null)!!.filter { it.isCrashed == true }.count()) + assertEquals(1, sut.getCurrentThreads(null, false)!!.filter { it.isCrashed == true }.count()) } @Test fun `when currentThreads is called with ignoreCurrentThread, current thread is not marked crashed`() { val sut = fixture.getSut() - assertEquals(0, sut.getCurrentThreads(null, true)!!.filter { it.isCrashed == true }.count()) + assertEquals( + 0, + sut.getCurrentThreads(null, true, false)!!.filter { it.isCrashed == true }.count(), + ) } @Test fun `when currentThreads is called, thread state is captured`() { val sut = fixture.getSut() - assertTrue(sut.getCurrentThreads(null)!!.all { it.state != null }) + assertTrue(sut.getCurrentThreads(null, false)!!.all { it.state != null }) } @Test @@ -52,7 +51,7 @@ class SentryThreadFactoryTest { val sut = fixture.getSut() assertTrue( sut - .getCurrentThreads(null)!! + .getCurrentThreads(null, true)!! .filter { it.stacktrace != null } .any { it.stacktrace!!.frames!!.count() > 0 } ) @@ -63,7 +62,7 @@ class SentryThreadFactoryTest { val sut = fixture.getSut() assertTrue( sut - .getCurrentThreads(null)!! + .getCurrentThreads(null, true)!! .filter { it.stacktrace != null } .any { it.stacktrace!!.snapshot == true } ) @@ -71,10 +70,10 @@ class SentryThreadFactoryTest { @Test fun `when currentThreads and attachStacktrace is disabled, stack frames are not captured`() { - val sut = fixture.getSut(false) + val sut = fixture.getSut() assertFalse( sut - .getCurrentThreads(null)!! + .getCurrentThreads(null, false)!! .filter { it.stacktrace != null } .any { it.stacktrace!!.frames!!.count() > 0 } ) @@ -87,7 +86,7 @@ class SentryThreadFactoryTest { val currentThread = Thread.currentThread() stackTraces.remove(currentThread) - val threads = sut.getCurrentThreads(stackTraces, null, false) + val threads = sut.getCurrentThreads(stackTraces, null, false, false) assertNotNull(threads!!.firstOrNull { it.id == currentThread.id }) } @@ -95,7 +94,7 @@ class SentryThreadFactoryTest { @Test fun `When passing empty param to getCurrentThreads, returns null`() { val sut = fixture.getSut() - val threads = sut.getCurrentThreads(mapOf(), null, false) + val threads = sut.getCurrentThreads(mapOf(), null, false, false) assertNull(threads) } @@ -108,7 +107,7 @@ class SentryThreadFactoryTest { val stacktraces = emptyArray() val threadList = mutableMapOf(thread to stacktraces) - val threads = sut.getCurrentThreads(threadList, threadIds, false) + val threads = sut.getCurrentThreads(threadList, threadIds, false, false) assertNotNull(threads!!.firstOrNull { it.isCrashed == true }) } @@ -116,7 +115,7 @@ class SentryThreadFactoryTest { @Test fun `when getCurrentThread is called, returns current thread`() { val sut = fixture.getSut() - val threads = sut.currentThread + val threads = sut.getCurrentThread(false) assertEquals(1, threads!!.count()) } } From 656c9fda126a9d2417cb2d9389b761b91c78d48b Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 19 Nov 2025 20:27:47 +0100 Subject: [PATCH 2/7] docs: Add PR #4918 to CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af3c34a3a9..07d09f0080c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Improvements +- Decouple SentryThreadFactory from SentryOptions ([#4918](https://github.com/getsentry/sentry-java/pull/4918)) - Do not send manual log origin ([#4897](https://github.com/getsentry/sentry-java/pull/4897)) ## 8.26.0 From c62ce5caf378d261b643f32a7d21b0ede669347e Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 19 Nov 2025 20:28:26 +0100 Subject: [PATCH 3/7] refactor(core): Update API dump after thread factory changes --- sentry/api/sentry.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a638df4ebef..35562902cb6 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3864,7 +3864,7 @@ public final class io/sentry/SentryStackTraceFactory { } public final class io/sentry/SentryThreadFactory { - public fun (Lio/sentry/SentryStackTraceFactory;Lio/sentry/SentryOptions;)V + public fun (Lio/sentry/SentryStackTraceFactory;)V } public final class io/sentry/SentryTraceHeader { From 00442b261f19efe112575363b43a4928e15fe897 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 19 Nov 2025 20:32:09 +0100 Subject: [PATCH 4/7] Address PR feedback --- .../java/io/sentry/android/core/AnrIntegrationFactory.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java index 130f4d057d7..cff8f72cd36 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java @@ -13,8 +13,7 @@ public final class AnrIntegrationFactory { public static Integration create( final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider) { if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.R) { - // return new AnrV2Integration(context); - return new AnrIntegration(context); + return new AnrV2Integration(context); } else { return new AnrIntegration(context); } From e1bbde436bd0f9e3498b9c8daf0c51cce6a4a500 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 24 Nov 2025 13:10:32 +0100 Subject: [PATCH 5/7] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d09f0080c..6febaf07c05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,11 @@ ### Improvements -- Decouple SentryThreadFactory from SentryOptions ([#4918](https://github.com/getsentry/sentry-java/pull/4918)) - Do not send manual log origin ([#4897](https://github.com/getsentry/sentry-java/pull/4897)) +### Fixes +- Fix missing thread stacks for ANRv1 events ([#4918](https://github.com/getsentry/sentry-java/pull/4918)) + ## 8.26.0 ### Features From fd123d2d000e99de7bccea3ce2763f396c0b4222 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 24 Nov 2025 13:10:54 +0100 Subject: [PATCH 6/7] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6febaf07c05..2a99adcb8de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Do not send manual log origin ([#4897](https://github.com/getsentry/sentry-java/pull/4897)) ### Fixes + - Fix missing thread stacks for ANRv1 events ([#4918](https://github.com/getsentry/sentry-java/pull/4918)) ## 8.26.0 From b54acd5bc32c2d236650b6db72a93c931db13485 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 25 Nov 2025 17:43:10 +0100 Subject: [PATCH 7/7] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b155fcc700a..48568805a36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,15 @@ - Avoid forking `rootScopes` for Reactor if current thread has `NoOpScopes` ([#4793](https://github.com/getsentry/sentry-java/pull/4793)) - This reduces the SDKs overhead by avoiding unnecessary scope forks +### Fixes + +- Fix missing thread stacks for ANRv1 events ([#4918](https://github.com/getsentry/sentry-java/pull/4918)) + ## 8.27.1 ### Fixes - Do not log if `sentry.properties` in rundir has not been found ([#4929](https://github.com/getsentry/sentry-java/pull/4929)) -- Fix missing thread stacks for ANRv1 events ([#4918](https://github.com/getsentry/sentry-java/pull/4918)) - ## 8.27.0