@@ -13,7 +13,6 @@ import io.sentry.SpanStatus
1313import io.sentry.TransactionContext
1414import io.sentry.TransactionOptions
1515import io.sentry.TypeCheckHint
16- import io.sentry.compose.navigation3.BackStackObserver.Companion.NAVIGATION_CONTEXT_KEY
1716import io.sentry.compose.navigation3.PreparedChange.BackStackHasNewTop
1817import io.sentry.compose.navigation3.PreparedChange.BackStackHasSameTop
1918import io.sentry.compose.navigation3.PreparedChange.BackStackIsEmpty
@@ -23,6 +22,11 @@ import io.sentry.protocol.TransactionNameSource
2322import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
2423import 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. */
342357private class ScreenTracker {
343358
0 commit comments