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) + } + } }