diff --git a/CHANGELOG.md b/CHANGELOG.md
index 754646ddc5..2ce997f3b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@
- Don't wait on main thread when SDK restarts ([#3200](https://github.com/getsentry/sentry-java/pull/3200))
- Fix Jetpack Compose widgets are not being correctly identified for user interaction tracing ([#3209](https://github.com/getsentry/sentry-java/pull/3209))
+- Fix issue title on Android when a wrapping `RuntimeException` is thrown by the system ([#3212](https://github.com/getsentry/sentry-java/pull/3212))
+ - This will change grouping of the issues that were previously titled `RuntimeInit$MethodAndArgsCaller` to have them split up properly by the original root cause exception
## 7.3.0
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java
index 8dcfe196c2..45e4b78787 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java
@@ -15,11 +15,16 @@
import io.sentry.android.core.performance.TimeSpan;
import io.sentry.protocol.App;
import io.sentry.protocol.OperatingSystem;
+import io.sentry.protocol.SentryException;
+import io.sentry.protocol.SentryStackFrame;
+import io.sentry.protocol.SentryStackTrace;
import io.sentry.protocol.SentryThread;
import io.sentry.protocol.SentryTransaction;
import io.sentry.protocol.User;
import io.sentry.util.HintUtils;
import io.sentry.util.Objects;
+import java.util.Collections;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
@@ -68,9 +73,51 @@ public DefaultAndroidEventProcessor(
setCommons(event, true, applyScopeData);
+ fixExceptionOrder(event);
+
return event;
}
+ /**
+ * The last exception is usually used for picking the issue title, but the convention is to send
+ * inner exceptions first, e.g. [inner, outer] This doesn't work very well on Android, as some
+ * hooks like Application.onCreate is wrapped by Android framework with a RuntimeException. Thus,
+ * if the last exception is a RuntimeInit$MethodAndArgsCaller, reverse the order to get a better
+ * issue title. This is a quick fix, for more details see: #64074 #59679 #64088
+ *
+ * @param event the event to process
+ */
+ private static void fixExceptionOrder(final @NotNull SentryEvent event) {
+ boolean reverseExceptions = false;
+
+ final @Nullable List exceptions = event.getExceptions();
+ if (exceptions != null && exceptions.size() > 1) {
+ final @NotNull SentryException lastException = exceptions.get(exceptions.size() - 1);
+ if ("java.lang".equals(lastException.getModule())) {
+ final @Nullable SentryStackTrace stacktrace = lastException.getStacktrace();
+ if (stacktrace != null) {
+ final @Nullable List frames = stacktrace.getFrames();
+ if (frames != null) {
+ for (final @NotNull SentryStackFrame frame : frames) {
+ if ("com.android.internal.os.RuntimeInit$MethodAndArgsCaller"
+ .equals(frame.getModule())) {
+ reverseExceptions = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (reverseExceptions) {
+ Collections.reverse(exceptions);
+ }
+ }
+
private void setCommons(
final @NotNull SentryBaseEvent event,
final boolean errorEvent,
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt
index 70b20e2ab2..80954f67a5 100644
--- a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt
@@ -16,6 +16,9 @@ import io.sentry.TypeCheckHint.SENTRY_DART_SDK_NAME
import io.sentry.android.core.internal.util.CpuInfoUtils
import io.sentry.protocol.OperatingSystem
import io.sentry.protocol.SdkVersion
+import io.sentry.protocol.SentryException
+import io.sentry.protocol.SentryStackFrame
+import io.sentry.protocol.SentryStackTrace
import io.sentry.protocol.SentryThread
import io.sentry.protocol.SentryTransaction
import io.sentry.protocol.User
@@ -573,4 +576,87 @@ class DefaultAndroidEventProcessorTest {
assertNull(thread.isMain)
}
}
+
+ @Test
+ fun `the exception list is reversed in case there's an RuntimeException`() {
+ val sut = fixture.getSut(context)
+ val event = SentryEvent().apply {
+ exceptions = listOf(
+ SentryException().apply {
+ type = "IllegalStateException"
+ module = "com.example"
+ stacktrace = SentryStackTrace(
+ listOf(
+ SentryStackFrame().apply {
+ function = "onCreate"
+ module = "com.example.Application"
+ filename = "Application.java"
+ }
+ )
+ )
+ },
+ SentryException().apply {
+ type = "RuntimeException"
+ value = "Unable to create application com.example.Application: java.lang.IllegalStateException"
+ module = "java.lang"
+ stacktrace = SentryStackTrace(
+ listOf(
+ SentryStackFrame().apply {
+ function = "run"
+ module = "com.android.internal.os.RuntimeInit\$MethodAndArgsCaller"
+ filename = "RuntimeInit.java"
+ }
+ )
+ )
+ }
+ )
+ }
+ val processedEvent = sut.process(event, Hint())
+ assertNotNull(processedEvent) {
+ assertEquals(2, it.exceptions!!.size)
+ assertEquals("RuntimeException", it.exceptions!![0].type)
+ assertEquals("IllegalStateException", it.exceptions!![1].type)
+ }
+ }
+
+ @Test
+ fun `the exception list is kept as-is in case there's no RuntimeException`() {
+ val sut = fixture.getSut(context)
+ val event = SentryEvent().apply {
+ exceptions = listOf(
+ SentryException().apply {
+ type = "IllegalStateException"
+ module = "com.example"
+ stacktrace = SentryStackTrace(
+ listOf(
+ SentryStackFrame().apply {
+ function = "onCreate"
+ module = "com.example.Application"
+ filename = "Application.java"
+ }
+ )
+ )
+ },
+ SentryException().apply {
+ type = "IllegalArgumentException"
+ module = "com.example"
+ stacktrace = SentryStackTrace(
+ listOf(
+ SentryStackFrame().apply {
+ function = "onCreate"
+ module = "com.example.Application"
+ filename = "Application.java"
+ }
+ )
+ )
+ }
+ )
+ }
+ val processedEvent = sut.process(event, Hint())
+ assertNotNull(processedEvent) {
+ assertEquals(2, it.exceptions!!.size)
+ assertEquals("IllegalStateException", it.exceptions!![0].type)
+ assertEquals("IllegalArgumentException", it.exceptions!![1].type)
+ }
+ }
}