From 0db2a9f09869c092cd99f7fb656f3e0299641cb6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 27 Jan 2026 17:16:21 +0100 Subject: [PATCH 1/2] fix(android): Fix ConcurrentModificationException when disabling native crash handling When enableNativeCrashHandling is set to false, the code was iterating over the integrations list with a for-each loop while calling remove() directly, which causes a ConcurrentModificationException at runtime. Fixed by using Java 8's removeIf() method which safely handles iteration and removal in a single operation. This is more concise and follows modern Java best practices. Added unit tests to verify the fix and ensure integrations are properly removed without throwing exceptions. Co-Authored-By: Claude Sonnet 4.5 --- .../java/io/sentry/react/RNSentryStartTest.kt | 37 +++++++++++++++++++ .../java/io/sentry/react/RNSentryStart.java | 17 ++++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt index 160d0fd9b2..b37edb4847 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt @@ -236,4 +236,41 @@ class RNSentryStartTest { assertEquals("android", result?.getTag("event.origin")) assertEquals("java", result?.getTag("event.environment")) } + + @Test + fun `when enableNativeCrashHandling is false, native crash integrations are removed without ConcurrentModificationException`() { + val rnOptions = JavaOnlyMap.of("enableNativeCrashHandling", false) + val options = SentryAndroidOptions() + + // This should not throw ConcurrentModificationException + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) + + // Verify integrations were removed + val integrations = options.getIntegrations() + assertFalse( + "UncaughtExceptionHandlerIntegration should be removed", + integrations.any { it is io.sentry.UncaughtExceptionHandlerIntegration } + ) + assertFalse( + "AnrIntegration should be removed", + integrations.any { it is io.sentry.android.core.AnrIntegration } + ) + assertFalse( + "NdkIntegration should be removed", + integrations.any { it is io.sentry.android.core.NdkIntegration } + ) + } + + @Test + fun `when enableNativeCrashHandling is true, native crash integrations are kept`() { + val rnOptions = JavaOnlyMap.of("enableNativeCrashHandling", true) + val options = SentryAndroidOptions() + + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) + + // When enabled, the default integrations should still be present + // Note: This test verifies that we don't remove integrations when the flag is true + val integrations = options.getIntegrations() + assertNotNull("Integrations list should not be null", integrations) + } } diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java index b3eb4510c7..9c407529f4 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -6,7 +6,6 @@ import com.facebook.react.bridge.ReadableType; import com.facebook.react.common.JavascriptException; import io.sentry.ILogger; -import io.sentry.Integration; import io.sentry.ProfileLifecycle; import io.sentry.Sentry; import io.sentry.SentryEvent; @@ -25,7 +24,6 @@ import io.sentry.react.replay.RNSentryReplayUnmask; import java.net.URI; import java.net.URISyntaxException; -import java.util.List; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -183,14 +181,13 @@ static void getSentryAndroidOptions( if (rnOptions.hasKey("enableNativeCrashHandling") && !rnOptions.getBoolean("enableNativeCrashHandling")) { - final List integrations = options.getIntegrations(); - for (final Integration integration : integrations) { - if (integration instanceof UncaughtExceptionHandlerIntegration - || integration instanceof AnrIntegration - || integration instanceof NdkIntegration) { - integrations.remove(integration); - } - } + options + .getIntegrations() + .removeIf( + integration -> + integration instanceof UncaughtExceptionHandlerIntegration + || integration instanceof AnrIntegration + || integration instanceof NdkIntegration); } logger.log( SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations())); From 5e078e7a1e2dc80c9f57ff50315cd155968cc0e5 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 27 Jan 2026 17:36:01 +0100 Subject: [PATCH 2/2] Lint fix --- .../app/src/test/java/io/sentry/react/RNSentryStartTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt index b37edb4847..03c309d441 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt @@ -249,15 +249,15 @@ class RNSentryStartTest { val integrations = options.getIntegrations() assertFalse( "UncaughtExceptionHandlerIntegration should be removed", - integrations.any { it is io.sentry.UncaughtExceptionHandlerIntegration } + integrations.any { it is io.sentry.UncaughtExceptionHandlerIntegration }, ) assertFalse( "AnrIntegration should be removed", - integrations.any { it is io.sentry.android.core.AnrIntegration } + integrations.any { it is io.sentry.android.core.AnrIntegration }, ) assertFalse( "NdkIntegration should be removed", - integrations.any { it is io.sentry.android.core.NdkIntegration } + integrations.any { it is io.sentry.android.core.NdkIntegration }, ) }