Skip to content

Commit 4038ff4

Browse files
committed
feat(android-nav3): Model navigation routes
Introduce the route translation layer for Navigation 3 so later stages can reason about route names and sanitized arguments in one place. At this stage the module still exposes no meaningful public API surface.
1 parent f91ee0f commit 4038ff4

7 files changed

Lines changed: 1008 additions & 0 deletions

File tree

‎gradle/libs.versions.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver
9090
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidxCompose" }
9191
androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "androidxCompose" }
9292
androidx-compose-material3 = { module = "androidx.compose.material3:material3", version = "1.4.0" }
93+
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "androidxCompose" }
9394
androidx-compose-material-icons-core = { module = "androidx.compose.material:material-icons-core", version="1.7.8" }
9495
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version="1.7.8" }
9596
androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidxCompose" }

‎sentry-android-navigation3/api/sentry-android-navigation3.api‎

Whitespace-only changes.

‎sentry-android-navigation3/build.gradle.kts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ android {
4848

4949
kotlin { explicitApi() }
5050

51+
dependencies {
52+
implementation(projects.sentry)
53+
54+
compileOnly(libs.androidx.compose.runtime)
55+
56+
testImplementation(libs.androidx.compose.runtime)
57+
testImplementation(libs.google.truth)
58+
testImplementation(libs.kotlin.test.junit)
59+
testImplementation(libs.mockito.inline)
60+
testImplementation(libs.mockito.kotlin)
61+
}
62+
5163
tasks.withType<Detekt>().configureEach {
5264
// Target version of the generated JVM bytecode. It is used for type resolution.
5365
jvmTarget = JavaVersion.VERSION_1_8.toString()
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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

Comments
 (0)