|
| 1 | +package io.sentry.compose.navigation3 |
| 2 | + |
| 3 | +import androidx.compose.runtime.Composable |
| 4 | +import androidx.compose.runtime.DisposableEffect |
| 5 | +import androidx.compose.runtime.remember |
| 6 | +import androidx.compose.runtime.rememberUpdatedState |
| 7 | +import io.sentry.IScopes |
| 8 | +import io.sentry.ScopesAdapter |
| 9 | +import io.sentry.SentryOptions |
| 10 | + |
| 11 | +/** |
| 12 | + * An effect for generating Sentry data from your Nav3 backstack. Configure it via [options] and |
| 13 | + * call it before you invoke your `NavDisplay`. |
| 14 | + * |
| 15 | + * ```kotlin |
| 16 | + * @Composable |
| 17 | + * fun AppNavigation() { |
| 18 | + * val navBackStack = rememberNavBackStack(Home) |
| 19 | + * |
| 20 | + * // Place SentryNavEffect in the same composable as your NavDisplay and call |
| 21 | + * // the effect first. Doing so ensures the effect's lifecycle matches your |
| 22 | + * // NavDisplay, and that any Sentry data produced by your nav destinations |
| 23 | + * // get attributed to the appropriate nav transaction. |
| 24 | + * SentryNavEffect( |
| 25 | + * backStack = navBackStack, |
| 26 | + * options = SentryNavOptions(maxCapturedBackStackEntries = 10), |
| 27 | + * nameExtractor = { route -> route.extractName() }, |
| 28 | + * argumentsExtractor = { route -> route.extractArgument() }, |
| 29 | + * ) |
| 30 | + * |
| 31 | + * // Configure your NavDisplay like usual. |
| 32 | + * NavDisplay( |
| 33 | + * backStack = navBackStack, |
| 34 | + * ... |
| 35 | + * ) |
| 36 | + * } |
| 37 | + * ``` |
| 38 | + * |
| 39 | + * **Data generated** |
| 40 | + * |
| 41 | + * By default, the following data is produced for each nav destination: |
| 42 | + * |
| 43 | + * - a breadcrumb |
| 44 | + * - a screen name |
| 45 | + * - a record of the current back stack (last 10 frames) |
| 46 | + * |
| 47 | + * A new transaction is started at each nav destination, assuming another non-nav transaction isn't |
| 48 | + * already active. |
| 49 | + * |
| 50 | + * You can configure the above defaults via [SentryNavOptions]. (Screen names can be disabled via |
| 51 | + * [SentryOptions.setEnableScreenTracking].) |
| 52 | + * |
| 53 | + * **Limitations** |
| 54 | + * |
| 55 | + * `SentryNavEffect` generates all Sentry data based solely on the top entry of your back stack. In |
| 56 | + * particular, it has no awareness of |
| 57 | + * [`Scene`](https://developer.android.com/guide/navigation/navigation-3/scenes)s. Transaction |
| 58 | + * routes, breadcrumbs, and screen names are all derived from the top entry of the back stack and |
| 59 | + * are updated as it changes. |
| 60 | + * |
| 61 | + * `SentryNavEffect` also doesn't make any special accommodations for |
| 62 | + * [predictive back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture) |
| 63 | + * gestures. That means, for instance, that spans produced by predictively rendered composables can |
| 64 | + * show up under the current destination's transaction. |
| 65 | + * |
| 66 | + * **Privacy / PII** |
| 67 | + * |
| 68 | + * Values returned from [nameExtractor] and [argumentsExtractor] are ***not*** scrubbed by the |
| 69 | + * Sentry SDK before being sent to Sentry. Only return route names and arguments that are known to |
| 70 | + * be safe or have been pre-scrubbed. |
| 71 | + * |
| 72 | + * @param backStack The navigation backstack to observe. |
| 73 | + * @param scopes A scopes instance used to track generated Sentry data. |
| 74 | + * @param options The kinds of navigation info this effect should record. |
| 75 | + * @param nameExtractor Optional lambda to extract a human-readable route name from the top entry of |
| 76 | + * the [backStack]. If not provided, defaults to the simple name of the entry's class. |
| 77 | + * @param argumentsExtractor Optional lambda to extract a map of argument name -> argument values |
| 78 | + * from the top entry of the [backStack]. If not provided, no arguments are attached. The |
| 79 | + * following scalar values are supported: [String], [CharSequence], [Char], [Boolean], any |
| 80 | + * [Number], enums (via [Enum.name]), and `null`. Supported container values are: [Array]s, |
| 81 | + * primitive arrays, [Map]s, and [Collection]s of supported values, including nested containers. |
| 82 | + * All other types are stringified via `toString()`. Cyclic or deeply nested containers are |
| 83 | + * skipped. Return only the arguments needed for diagnostics and avoid large structures. |
| 84 | + */ |
| 85 | +@Composable |
| 86 | +@Suppress("FunctionNaming") |
| 87 | +internal fun <T : Any> SentryNavEffect( |
| 88 | + backStack: List<T>, |
| 89 | + scopes: IScopes = ScopesAdapter.getInstance(), |
| 90 | + options: SentryNavOptions = SentryNavOptions(), |
| 91 | + nameExtractor: ((T) -> String)? = null, |
| 92 | + argumentsExtractor: ((T) -> Map<String, Any?>)? = null, |
| 93 | +) { |
| 94 | + val routeResolvers = rememberUpdatedState(RouteResolvers(nameExtractor, argumentsExtractor)) |
| 95 | + |
| 96 | + val observer = |
| 97 | + remember(scopes, options) { |
| 98 | + BackStackObserver( |
| 99 | + scopes = scopes, |
| 100 | + options = options, |
| 101 | + resolvers = { routeResolvers.value }, |
| 102 | + ) |
| 103 | + } |
| 104 | + |
| 105 | + val capturedBackStack = backStack.toList() |
| 106 | + |
| 107 | + DisposableEffect(observer, BackStackKey(capturedBackStack)) { |
| 108 | + observer.onBackStackChanged(backStack = capturedBackStack) |
| 109 | + onDispose {} |
| 110 | + } |
| 111 | + |
| 112 | + DisposableEffect(observer) { |
| 113 | + onDispose { observer.cleanup() } |
| 114 | + } |
| 115 | +} |
0 commit comments