|
| 1 | +package io.sentry.compose.navigation3 |
| 2 | + |
| 3 | +import androidx.compose.runtime.snapshots.Snapshot |
| 4 | +import org.jetbrains.annotations.ApiStatus |
| 5 | + |
| 6 | +/** |
| 7 | + * Extracts a human-readable route name from a back stack entry. |
| 8 | + * |
| 9 | + * **Privacy / PII** |
| 10 | + * |
| 11 | + * Values returned from [extract] are ***not*** scrubbed by the Sentry SDK before being sent to |
| 12 | + * Sentry. Only return names that are known to be safe or have been pre-scrubbed. |
| 13 | + * |
| 14 | + * **Choosing stable route names** |
| 15 | + * |
| 16 | + * Implementations should return stable, low-cardinality names that don't depend on object identity, |
| 17 | + * argument values, or runtime class-name preservation. E.g., `Home`, `DetailScreen`, etc. |
| 18 | + * |
| 19 | + * In particular, avoid `::class.simpleName` in release builds, as R8 obfuscates class names and may |
| 20 | + * map them to different symbols across builds. |
| 21 | + * |
| 22 | + * **Falls back to "/unknown"** |
| 23 | + * |
| 24 | + * If [extract] throws or returns a blank route name, Sentry records the destination as "/unknown". |
| 25 | + * Doing so signals that name extraction needs to be fixed while avoiding misleading gaps in |
| 26 | + * navigation data. |
| 27 | + * |
| 28 | + * For instance, if a user navigates from `/home -> /detail -> /settings`, but the name extractor |
| 29 | + * for `/detail` throws, the back stack record will be `/home -> /unknown -> /settings` rather than |
| 30 | + * `/home -> /settings`. |
| 31 | + * |
| 32 | + * **Using kotlinx.serialization** |
| 33 | + * |
| 34 | + * If your back stack contains `@Serializable` route types, you may want to consider mapping each |
| 35 | + * route type to a stable serializer name. For instance: |
| 36 | + * ```kotlin |
| 37 | + * val nameExtractor = RouteNameExtractor<Any> { route -> |
| 38 | + * when (route) { |
| 39 | + * is HomeRoute -> HomeRoute.serializer().descriptor.serialName |
| 40 | + * is ProfileRoute -> ProfileRoute.serializer().descriptor.serialName |
| 41 | + * is SettingsRoute -> SettingsRoute.serializer().descriptor.serialName |
| 42 | + * } |
| 43 | + * } |
| 44 | + * ``` |
| 45 | + * |
| 46 | + * Doing so prevents route names from being obfuscated while leaving per-route arguments to |
| 47 | + * [RouteArgumentsExtractor]. |
| 48 | + */ |
| 49 | +@ApiStatus.Experimental |
| 50 | +internal fun interface RouteNameExtractor<T : Any> { |
| 51 | + fun extract(backStackEntry: T): String |
| 52 | +} |
| 53 | + |
| 54 | +/** |
| 55 | + * Extracts diagnostic route arguments from a back stack entry as map of argument name -> argument |
| 56 | + * values. |
| 57 | + * |
| 58 | + * **Privacy / PII** |
| 59 | + * |
| 60 | + * Values returned from [extract] are ***not*** scrubbed by the Sentry SDK before being sent to |
| 61 | + * Sentry. Only return arguments that are known to be safe or have been pre-scrubbed. |
| 62 | + * |
| 63 | + * **Choosing performant route arguments** |
| 64 | + * |
| 65 | + * Return only a small subset of route data useful for diagnostics. Data should be stable enough to |
| 66 | + * inspect in Sentry. |
| 67 | + * |
| 68 | + * For performance reasons, implementations should avoid large structures. Cyclic or deeply nested |
| 69 | + * containers will be skipped. (See `RouteTranslator` for more details.) |
| 70 | + * |
| 71 | + * **Accepted value types** |
| 72 | + * |
| 73 | + * Values may be any of the following scalar types: |
| 74 | + * |
| 75 | + * - [String] |
| 76 | + * - [CharSequence] |
| 77 | + * - [Char] |
| 78 | + * - [Boolean] |
| 79 | + * - any [Number] |
| 80 | + * - enums (via [Enum.name]) |
| 81 | + * - `null` |
| 82 | + * |
| 83 | + * Or any of the following container types: |
| 84 | + * |
| 85 | + * - [Array]s |
| 86 | + * - primitive arrays |
| 87 | + * - [Map]s |
| 88 | + * - [Collection]s |
| 89 | + * |
| 90 | + * Container values may be nested, and they must bottom out in supported scalar types. |
| 91 | + * |
| 92 | + * **Falls back to `toString()` or nothing** |
| 93 | + * |
| 94 | + * All non-supported types are stringified via `toString()`. If [extract] throws, no arguments are |
| 95 | + * recorded for the destination. |
| 96 | + * |
| 97 | + * **Using kotlinx.serialization** |
| 98 | + * |
| 99 | + * Even if your back stack contains `@Serializable` route types, consider mapping each route type to |
| 100 | + * a small set of diagnostic arguments to avoid the cost of serializing and returning the entire |
| 101 | + * route object. For instance: |
| 102 | + * ```kotlin |
| 103 | + * val argumentsExtractor = RouteArgumentsExtractor<Any> { route -> |
| 104 | + * when (route) { |
| 105 | + * is HomeRoute -> emptyMap() |
| 106 | + * is ProfileRoute -> mapOf("userId" to route.userId, "tab" to route.tab) |
| 107 | + * is SettingsRoute -> mapOf("section" to route.section) |
| 108 | + * } |
| 109 | + * } |
| 110 | + * ``` |
| 111 | + */ |
| 112 | +@ApiStatus.Experimental |
| 113 | +internal fun interface RouteArgumentsExtractor<T : Any> { |
| 114 | + fun extract(backStackEntry: T): Map<String, Any?> |
| 115 | +} |
| 116 | + |
| 117 | +/** |
| 118 | + * Holds host app-defined extractors, which convert a back stack entry of type [T] into a route name |
| 119 | + * and a map of zero or more route arguments. Extracted values are eventually grouped into [Route]s |
| 120 | + * for display. |
| 121 | + * |
| 122 | + * Extractor invocations are hidden from Compose snapshot observation so they don't impact |
| 123 | + * invalidation of the recompose scope that reads them. |
| 124 | + */ |
| 125 | +internal class RouteResolvers<T : Any>( |
| 126 | + val nameExtractor: RouteNameExtractor<T>, |
| 127 | + val argumentsExtractor: RouteArgumentsExtractor<T>?, |
| 128 | +) { |
| 129 | + |
| 130 | + fun getName(backStackEntry: T): String = Snapshot.withoutReadObservation { |
| 131 | + nameExtractor.extract(backStackEntry) |
| 132 | + } |
| 133 | + |
| 134 | + fun getArguments(backStackEntry: T): Map<String, Any?>? = Snapshot.withoutReadObservation { |
| 135 | + argumentsExtractor?.extract(backStackEntry) |
| 136 | + } |
| 137 | +} |
0 commit comments