Skip to content

Commit e6e8140

Browse files
0xadam-browncodex
andcommitted
ref(android-nav3): Extract nav context binder
Move scope and transaction navigation-context updates out of BackStackObserver into a dedicated private helper. Keep the behavior unchanged while separating the route-to-context binding concern from the observer's change-detection and transaction lifecycle logic. This reduces the observer's surface area and makes the remaining TooManyFunctions pressure easier to address with smaller follow-up extractions. Co-Authored-By: OpenAI GPT-5.4 <noreply@openai.com>
1 parent b7c29ad commit e6e8140

1 file changed

Lines changed: 67 additions & 52 deletions

File tree

‎sentry-android-navigation3/src/main/kotlin/io/sentry/compose/navigation3/BackStackObserver.kt‎

Lines changed: 67 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import io.sentry.SpanStatus
1313
import io.sentry.TransactionContext
1414
import io.sentry.TransactionOptions
1515
import io.sentry.TypeCheckHint
16-
import io.sentry.compose.navigation3.BackStackObserver.Companion.NAVIGATION_CONTEXT_KEY
1716
import io.sentry.compose.navigation3.PreparedChange.BackStackHasNewTop
1817
import io.sentry.compose.navigation3.PreparedChange.BackStackHasSameTop
1918
import io.sentry.compose.navigation3.PreparedChange.BackStackIsEmpty
@@ -23,6 +22,11 @@ import io.sentry.protocol.TransactionNameSource
2322
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
2423
import java.lang.ref.WeakReference
2524

25+
private const val BACKSTACK_KEY = "backstack"
26+
private const val NAVIGATION_CONTEXT_KEY = "navigation"
27+
private const val NAVIGATION_OP: String = "navigation"
28+
private const val TRANSACTION_ORIGIN = "auto.navigation.nav3"
29+
2630
/**
2731
* Observes the back stack managed by a single [SentryNavEffect] and records Sentry state as the
2832
* back stack is updated.
@@ -72,12 +76,6 @@ internal class BackStackObserver<T : Any>(
7276
}
7377

7478
internal companion object {
75-
76-
private const val BACKSTACK_KEY = "backstack"
77-
private const val NAVIGATION_CONTEXT_KEY = "navigation"
78-
private const val NAVIGATION_OP: String = "navigation"
79-
private const val TRANSACTION_ORIGIN = "auto.navigation.nav3"
80-
8179
init {
8280
SentryIntegrationPackageStorage.getInstance()
8381
.addPackage("maven:io.sentry:sentry-android-navigation3", BuildConfig.VERSION_NAME)
@@ -109,7 +107,7 @@ internal class BackStackObserver<T : Any>(
109107
// replaces one observer with another, there may be a brief gap where events lack nav
110108
// context. Apps should keep the observer at the nav root so cleanup only runs when the
111109
// navigation session is ending, not during normal destination changes.
112-
scope.removeNavigationContext()
110+
NavigationContextBinder.updateScope(scope, emptyList())
113111
}
114112
}
115113
}
@@ -184,7 +182,7 @@ internal class BackStackObserver<T : Any>(
184182
) {
185183
val currentTopRoute = currentBackStack.topRoute
186184

187-
scope.updateNavigationContext(currentBackStack.capturedRoutes)
185+
NavigationContextBinder.updateScope(scope, currentBackStack.capturedRoutes)
188186

189187
if (scopes.options.isEnableScreenTracking) {
190188
screenTracker.track(scope, currentTopRoute.name)
@@ -207,19 +205,27 @@ internal class BackStackObserver<T : Any>(
207205
currentTopRoute.name,
208206
currentTopRoute.arguments,
209207
)
210-
?.updateNavigationContext(scope, currentBackStack)
208+
?.let { transaction ->
209+
NavigationContextBinder.updateTransaction(
210+
transaction = transaction,
211+
scope = scope,
212+
backStack = currentBackStack,
213+
isScreenTrackingEnabled = scopes.options.isEnableScreenTracking,
214+
captureBackStack = options.captureBackStack,
215+
)
216+
}
211217
} else {
212218
// Rotate the propagation context.
213219
scope.withPropagationContext { scope.setPropagationContext(PropagationContext()) }
214220
}
215221
}
216222

217223
private fun handleSameTop(scope: IScope, backStack: BackStackData<T>) {
218-
scope.updateNavigationContext(backStack.capturedRoutes)
224+
NavigationContextBinder.updateScope(scope, backStack.capturedRoutes)
219225
}
220226

221227
private fun handleEmptyBackStack(scope: IScope) {
222-
scope.updateNavigationContext(emptyList())
228+
NavigationContextBinder.updateScope(scope, emptyList())
223229
navTransactions.stop(scope)
224230
screenTracker.clear(scope)
225231
previousTopEntry = null
@@ -231,46 +237,6 @@ internal class BackStackObserver<T : Any>(
231237
previousTopRoute = topRoute
232238
}
233239

234-
private fun IScope.updateNavigationContext(capturedRoutes: List<Route>) {
235-
if (capturedRoutes.isEmpty()) {
236-
this.removeNavigationContext()
237-
} else {
238-
this.setContexts(NAVIGATION_CONTEXT_KEY, capturedRoutes.toNavigationContext())
239-
}
240-
}
241-
242-
private fun IScope.removeNavigationContext() {
243-
// We purposefully don't call IScope.removeContexts(), as it doesn't notify IScopeObserver and
244-
// therefore doesn't write its updates to disk ¯\_ (ツ)_/¯.
245-
this.setContexts(NAVIGATION_CONTEXT_KEY, null as Any?)
246-
}
247-
248-
/**
249-
* Updates the receiver's context with the provided navigation info.
250-
*
251-
* Needed because transactions inherit base scope context on a per-key basis unless transactions
252-
* have their own values for those keys. In our case, we need to keep fresh back stack and route
253-
* values in the base context for purposes of crash reporting. But those values will often advance
254-
* past what's relevant to a given transaction. This method prevents misassociation by binding
255-
* proper values to the transaction context instead.
256-
*/
257-
private fun ITransaction.updateNavigationContext(scope: IScope, backStack: BackStackData<T>) {
258-
if (scopes.options.isEnableScreenTracking) {
259-
val appContext = contexts.app ?: io.sentry.protocol.Contexts(scope.contexts).app ?: App()
260-
261-
appContext.viewNames = listOf(backStack.topRoute.name)
262-
contexts.setApp(appContext)
263-
}
264-
265-
if (options.captureBackStack && backStack.capturedRoutes.isNotEmpty()) {
266-
setContext(NAVIGATION_CONTEXT_KEY, backStack.capturedRoutes.toNavigationContext())
267-
}
268-
}
269-
270-
/** Builds the `{"backstack": [...]}` map bound under [NAVIGATION_CONTEXT_KEY]. */
271-
private fun List<Route>.toNavigationContext(): Map<String, Any?> =
272-
mapOf(BACKSTACK_KEY to serialize())
273-
274240
private fun IScopes.addNav3Breadcrumb(
275241
from: Route?,
276242
toEntry: T,
@@ -338,6 +304,55 @@ private data class BackStackData<T>(
338304
val capturedRoutes: List<Route>,
339305
)
340306

307+
private object NavigationContextBinder {
308+
309+
fun updateScope(scope: IScope, capturedRoutes: List<Route>) {
310+
if (capturedRoutes.isEmpty()) {
311+
removeScopeContext(scope)
312+
} else {
313+
scope.setContexts(NAVIGATION_CONTEXT_KEY, capturedRoutes.toNavigationContext())
314+
}
315+
}
316+
317+
/**
318+
* Updates the transaction with the provided navigation info.
319+
*
320+
* Needed because transactions inherit base scope context on a per-key basis unless transactions
321+
* have their own values for those keys. In our case, we need to keep fresh back stack and route
322+
* values in the base context for purposes of crash reporting. But those values will often advance
323+
* past what's relevant to a given transaction. This method prevents misassociation by binding
324+
* proper values to the transaction context instead.
325+
*/
326+
fun <T : Any> updateTransaction(
327+
transaction: ITransaction,
328+
scope: IScope,
329+
backStack: BackStackData<T>,
330+
isScreenTrackingEnabled: Boolean,
331+
captureBackStack: Boolean,
332+
) {
333+
if (isScreenTrackingEnabled) {
334+
val appContext = transaction.contexts.app ?: io.sentry.protocol.Contexts(scope.contexts).app ?: App()
335+
336+
appContext.viewNames = listOf(backStack.topRoute.name)
337+
transaction.contexts.setApp(appContext)
338+
}
339+
340+
if (captureBackStack && backStack.capturedRoutes.isNotEmpty()) {
341+
transaction.setContext(NAVIGATION_CONTEXT_KEY, backStack.capturedRoutes.toNavigationContext())
342+
}
343+
}
344+
345+
private fun removeScopeContext(scope: IScope) {
346+
// We purposefully don't call IScope.removeContexts(), as it doesn't notify IScopeObserver and
347+
// therefore doesn't write its updates to disk ¯\_ (ツ)_/¯.
348+
scope.setContexts(NAVIGATION_CONTEXT_KEY, null as Any?)
349+
}
350+
351+
/** Builds the `{"backstack": [...]}` map bound under [NAVIGATION_CONTEXT_KEY]. */
352+
private fun List<Route>.toNavigationContext(): Map<String, Any?> =
353+
mapOf(BACKSTACK_KEY to serialize())
354+
}
355+
341356
/** Tracks a provided name as the current visible screen. */
342357
private class ScreenTracker {
343358

0 commit comments

Comments
 (0)