Recover Jetpack Compose stacktraces
Problem Statement
Since compose is a declarative UI framework, there's a big usage of lambdas, which get transformed into anonymous classes, therefore stackframes that contain compose code do not really look helpful (besides the file name and line numbers, it's hard to figure out what's going on). For example:
FATAL EXCEPTION: main
Process: com.google.samples.apps.nowinandroid.debug, PID: 2799
java.lang.RuntimeException: TEST
at com.google.samples.apps.nowinandroid.feature.topic.TopicScreenKt$TopicRoute$1.invoke(TopicScreen.kt:71)
at com.google.samples.apps.nowinandroid.feature.topic.TopicScreenKt$TopicRoute$1.invoke(TopicScreen.kt:66)
at com.google.samples.apps.nowinandroid.core.ui.component.ChipKt$NiaFilterChip$1$1.invoke(Chip.kt:48)
at com.google.samples.apps.nowinandroid.core.ui.component.ChipKt$NiaFilterChip$1$1.invoke(Chip.kt:48)
at androidx.compose.foundation.ClickableKt$clickable$4$gesture$1$2.invoke-k-4lQ0M(Clickable.kt:153)
at androidx.compose.foundation.ClickableKt$clickable$4$gesture$1$2.invoke(Clickable.kt:142)
at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1$1.invokeSuspend(TapGestureDetector.kt:220)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:178)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:328)
at androidx.compose.ui.input.pointer.SuspendingPointerInputFilter$PointerEventHandlerCoroutine.offerPointerEvent(SuspendingPointerInputFilter.kt:560)
at androidx.compose.ui.input.pointer.SuspendingPointerInputFilter.dispatchPointerEvent(SuspendingPointerInputFilter.kt:452)
at androidx.compose.ui.input.pointer.SuspendingPointerInputFilter.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:465)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:310)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:179)
at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:98)
at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:80)
at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1261)
at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1211)
at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1150)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3118)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2799)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3118)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2799)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3118)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2799)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3118)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2799)
at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:488)
at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1871)
It'd be nice if we can recover the stackframes and make them pretty by including the actual lambda argument names (in this example it'd be NiaFilterChip$onClick and TopicRoute$onFollowClick instead of NiaFilterChip$1$1 and TopicRoute$1 respectively).
Solution Brainstorm
Probably would require some logic on the bytecode level (either through SAGP or a jvm-agent) to collect the information about lambdas and add them to the proguard-mapping file so they get correctly interpreted on the product side (or maybe we'd require some new logic on the product side as well).
https://github.com/Guardsquare/kotlin-metadata-printer might be useful to read kotlin's metadata.
@romtsn do you know if there's a way to trace back to the original stack trace frames? e.g. https://github.com/getsentry/sentry-java/issues/1524#issuecomment-1135506317 I often see libs doing this (rewriting the frames), e.g. for coroutines and rxjava, the problem is kinda the same, I guess.
Yeah the problem is the usage of lambdas/anonymous classes for all of them I guess. I feel like this can be done through bytecode manipulation (e.g. getting the argument name for lambda), but not sure if there's a silver bullet for all.
We should check if there's a library for doing this. If we can find a workaround for the meantime we should add it to the troubleshooting page.
There's a flag in the coroutines library that makes it print more verbose stack traces. This could help.
We've decided it's not worth the effort, and it's not clear what should be the outcome. At the moment, linenos are still correct and show you exactly where the error has happened. When #633 is in place, this should help even more and show surrounding code.