Skip to content

Commit c789f9b

Browse files
authored
Merge pull request #1345 from square/ray/more-exception-detangling
Exception reporter disentangling for Android UI code
2 parents a173a9b + 0495048 commit c789f9b

File tree

7 files changed

+58
-11
lines changed

7 files changed

+58
-11
lines changed

workflow-runtime/api/workflow-runtime.api

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,12 @@ public final class com/squareup/workflow1/WorkflowInterceptor$WorkflowSession$De
116116
public static fun isRootWorkflow (Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Z
117117
}
118118

119+
public final class com/squareup/workflow1/internal/ThrowablesKt {
120+
public static final fun requireNotNullWithKey (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
121+
public static synthetic fun requireNotNullWithKey$default (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Ljava/lang/Object;
122+
}
123+
124+
public final class com/squareup/workflow1/internal/Throwables_jvmKt {
125+
public static final fun withKey (Ljava/lang/Throwable;Ljava/lang/Object;)Ljava/lang/Throwable;
126+
}
127+

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/Throwables.kt

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,36 @@ package com.squareup.workflow1.internal
33
import kotlin.contracts.ExperimentalContracts
44
import kotlin.contracts.contract
55

6+
/**
7+
* Like Kotlin's [requireNotNull], but uses [stackTraceKey] to create a fake top element
8+
* on the stack trace, ensuring that a crash reporter's default grouping will create unique
9+
* groups for unique keys.
10+
*
11+
* @see [withKey]
12+
*
13+
* @throws IllegalArgumentException if the [value] is false.
14+
*/
15+
@OptIn(ExperimentalContracts::class)
16+
inline fun <T : Any> requireNotNullWithKey(
17+
value: T?,
18+
stackTraceKey: Any,
19+
lazyMessage: () -> Any = { "Required value was null." }
20+
): T {
21+
contract {
22+
returns() implies (value != null)
23+
}
24+
if (value == null) {
25+
val message = lazyMessage()
26+
val exception: Throwable = IllegalArgumentException(message.toString())
27+
throw exception.withKey(stackTraceKey)
28+
} else {
29+
return value
30+
}
31+
}
32+
633
/**
734
* Like Kotlin's [require], but uses [stackTraceKey] to create a fake top element
8-
* on the stack trace, ensuring that crash reporter's default grouping will create unique
35+
* on the stack trace, ensuring that a crash reporter's default grouping will create unique
936
* groups for unique keys.
1037
*
1138
* So far [stackTraceKey] is only effective on JVM, it has no effect in other languages.
@@ -36,7 +63,7 @@ internal inline fun requireWithKey(
3663

3764
/**
3865
* Like Kotlin's [check], but uses [stackTraceKey] to create a fake top element
39-
* on the stack trace, ensuring that crash reporter's default grouping will create unique
66+
* on the stack trace, ensuring that a crash reporter's default grouping will create unique
4067
* groups for unique keys.
4168
*
4269
* So far [stackTraceKey] is only effective on JVM, it has no effect in other languages.
@@ -67,12 +94,12 @@ internal inline fun checkWithKey(
6794

6895
/**
6996
* Uses [stackTraceKey] to create a fake top element on the stack trace, ensuring
70-
* that crash reporter's default grouping will create unique groups for unique keys.
97+
* that a crash reporter's default grouping will create unique groups for unique keys.
7198
*
7299
* So far only effective on JVM, this is a pass through in other languages.
73100
*
74101
* @param stackTraceKey an object whose [toString] method will serve as a grouping key
75102
* for crash reporters. It is important that keys are stable across processes,
76103
* avoid system hashes.
77104
*/
78-
internal expect fun <T : Throwable> T.withKey(stackTraceKey: Any): T
105+
expect fun <T : Throwable> T.withKey(stackTraceKey: Any): T

workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/Throwables.jvm.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.squareup.workflow1.internal
22

3-
internal actual fun <T : Throwable> T.withKey(stackTraceKey: Any): T = apply {
3+
actual fun <T : Throwable> T.withKey(stackTraceKey: Any): T = apply {
44
val realTop = stackTrace[0]
55
val fakeTop = StackTraceElement(
66
// Real class name to ensure that we are still "in project".

workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactoryFinder.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.squareup.workflow1.ui.compose
22

33
import androidx.compose.runtime.CompositionLocalProvider
44
import androidx.compose.runtime.remember
5+
import com.squareup.workflow1.internal.withKey
6+
import com.squareup.workflow1.ui.Compatible.Companion.keyFor
57
import com.squareup.workflow1.ui.EnvironmentScreen
68
import com.squareup.workflow1.ui.NamedScreen
79
import com.squareup.workflow1.ui.Screen
@@ -72,5 +74,5 @@ public fun <ScreenT : Screen> ScreenComposableFactoryFinder.requireComposableFac
7274
environment[ViewRegistry]
7375
.getEntryFor(Key(rendering::class, ScreenComposableFactory::class))
7476
}."
75-
)
77+
).withKey(keyFor(rendering))
7678
}

workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.squareup.workflow1.ui
22

3+
import com.squareup.workflow1.internal.withKey
4+
import com.squareup.workflow1.ui.Compatible.Companion.keyFor
35
import com.squareup.workflow1.ui.ScreenViewFactory.Companion.forWrapper
46
import com.squareup.workflow1.ui.ViewRegistry.Key
57
import com.squareup.workflow1.ui.navigation.BackStackScreen
@@ -98,5 +100,5 @@ public fun <ScreenT : Screen> ScreenViewFactoryFinder.requireViewFactoryForRende
98100
"ViewEnvironment.withComposeInteropSupport() " +
99101
"from module com.squareup.workflow1:workflow-ui-compose at the top " +
100102
"of your Android view hierarchy."
101-
)
103+
).withKey(keyFor(rendering))
102104
}

workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowSavedStateRegistryAggregator.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import androidx.lifecycle.findViewTreeLifecycleOwner
1111
import androidx.savedstate.SavedStateRegistryOwner
1212
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
1313
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
14+
import com.squareup.workflow1.internal.requireNotNullWithKey
15+
import com.squareup.workflow1.internal.withKey
1416

1517
/**
1618
* Manages a group of [SavedStateRegistryOwner]s that are all saved to
@@ -106,6 +108,7 @@ public class WorkflowSavedStateRegistryAggregator {
106108
} catch (e: IllegalStateException) {
107109
// Exception thrown by SavedStateRegistryOwner is pretty useless.
108110
throw IllegalStateException("Error consuming $parentKey from $parentRegistryOwner", e)
111+
.withKey(parentKey.orEmpty())
109112
}
110113
restoreFromBundle(restoredState)
111114
}
@@ -163,7 +166,7 @@ public class WorkflowSavedStateRegistryAggregator {
163166
"Compatible.compatibilityKey -- note the name fields on BodyAndOverlaysScreen " +
164167
"and BackStackScreen.",
165168
e
166-
)
169+
).withKey(key)
167170
}
168171

169172
// Even if the parent lifecycle is in a state further than CREATED, new observers are sent all
@@ -223,20 +226,21 @@ public class WorkflowSavedStateRegistryAggregator {
223226
key: String,
224227
force: Boolean = false
225228
) {
226-
val lifecycleOwner = requireNotNull(view.findViewTreeLifecycleOwner()) {
229+
val lifecycleOwner = requireNotNullWithKey(view.findViewTreeLifecycleOwner(), key) {
227230
"Expected $view($key) to have a ViewTreeLifecycleOwner. " +
228231
"Use WorkflowLifecycleOwner to fix that."
229232
}
230233
val registryOwner = KeyedSavedStateRegistryOwner(key, lifecycleOwner)
231234
children.put(key, registryOwner)?.let {
232235
throw IllegalArgumentException("$key is already in use, it cannot be used to register $view")
236+
.withKey(key)
233237
}
234238
view.findViewTreeSavedStateRegistryOwner()
235239
?.takeIf { !force || it is KeyedSavedStateRegistryOwner }
236240
?.let {
237241
throw IllegalArgumentException(
238242
"Using $key to register $view, but it already has SavedStateRegistryOwner: $it"
239-
)
243+
).withKey(key)
240244
}
241245
view.setViewTreeSavedStateRegistryOwner(registryOwner)
242246
restoreIfOwnerReady(registryOwner)
@@ -253,6 +257,7 @@ public class WorkflowSavedStateRegistryAggregator {
253257
public fun saveAndPruneChildRegistryOwner(key: String) {
254258
children.remove(key)?.let { saveIfOwnerReady(it) }
255259
?: throw IllegalArgumentException("No such child: $key, on parent $parentKey")
260+
.withKey(key)
256261
}
257262

258263
private fun saveIfOwnerReady(child: KeyedSavedStateRegistryOwner) {

workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/navigation/OverlayDialogFactoryFinder.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.squareup.workflow1.ui.navigation
22

3+
import com.squareup.workflow1.internal.withKey
4+
import com.squareup.workflow1.ui.Compatible.Companion.keyFor
35
import com.squareup.workflow1.ui.Screen
46
import com.squareup.workflow1.ui.ViewEnvironment
57
import com.squareup.workflow1.ui.ViewEnvironmentKey
@@ -31,7 +33,7 @@ public interface OverlayDialogFactoryFinder {
3133
?: throw IllegalArgumentException(
3234
"An OverlayDialogFactory should have been registered to display $rendering, " +
3335
"or that class should implement AndroidOverlay. Instead found $entry."
34-
)
36+
).withKey(keyFor(rendering))
3537
}
3638

3739
public companion object : ViewEnvironmentKey<OverlayDialogFactoryFinder>() {

0 commit comments

Comments
 (0)