@@ -2,20 +2,21 @@ package io.sentry.compose.navigation3
22
33import io.sentry.Breadcrumb
44import io.sentry.Hint
5- import io.sentry.IScope
6- import io.sentry.IScopes
75import io.sentry.ITransaction
8- import io.sentry.PropagationContext
96import io.sentry.SentryIntegrationPackageStorage
107import io.sentry.SentryLevel.DEBUG
118import io.sentry.SentryLevel.INFO
9+ import io.sentry.SentryOptions
1210import io.sentry.SpanStatus
1311import io.sentry.TransactionContext
1412import io.sentry.TransactionOptions
1513import io.sentry.compose.navigation3.BackStackConverter.RetentionPolicy
1614import io.sentry.compose.navigation3.PreparedChange.BackStackHasNewTop
1715import io.sentry.compose.navigation3.PreparedChange.BackStackHasSameTop
1816import io.sentry.compose.navigation3.PreparedChange.BackStackIsEmpty
17+ import io.sentry.compose.navigation3.lifecycle.INavScope
18+ import io.sentry.compose.navigation3.lifecycle.INavScopes
19+ import io.sentry.compose.navigation3.lifecycle.NAVIGATION_CONTEXT_KEY
1920import io.sentry.protocol.App
2021import io.sentry.protocol.TransactionNameSource
2122import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
@@ -47,10 +48,11 @@ private const val NAVIGATION_OP: String = "navigation"
4748 * **Thread safety**
4849 *
4950 * This class is ***not*** thread-safe. Clients should serialize calls to [onBackStackChanged] and
50- * [cleanup ] (e.g., via invocation from an `*Effect` or another form of thread confinement).
51+ * [dispose ] (e.g., via invocation from an `*Effect` or another form of thread confinement).
5152 */
53+ @Suppress(" TooManyFunctions" )
5254internal class BackStackObserver <T : Any >(
53- private val scopes : IScopes ,
55+ private val scopes : INavScopes ,
5456 private val options : SentryNavOptions ,
5557 entryMapper : ForwardingBackStackEntryMapper <T >,
5658) {
@@ -110,23 +112,14 @@ internal class BackStackObserver<T : Any>(
110112 scopes.configureScope { scope -> applyChange(scope, change) }
111113 }
112114
113- internal fun cleanup () {
114- previousTop = null
115- previousTopNormalized = null
116-
117- scopes.configureScope { scope ->
118- navTransaction.stop(scope)
119- navScreen.clear(scope)
120-
121- if (options.captureBackStack) {
122- // This observer owns the nav context while it's in the composition, and cleanup removes
123- // it to avoid leaking stale back stack data after observation stops. If the host app
124- // replaces one observer with another, there may be a brief gap where events lack nav
125- // context. Apps should keep the observer at the nav root so cleanup only runs when the
126- // navigation session is ending, not during normal destination changes.
127- navContext.clear(scope)
128- }
129- }
115+ /* *
116+ * Clears the nav data this observer recorded.
117+ *
118+ * Call it when observation ends so that no stale screen, nav context, or nav transaction outlives
119+ * it.
120+ */
121+ internal fun dispose () {
122+ scopes.configureScope { scope -> tearDown(scope) }
130123 }
131124
132125 private fun prepareChange (backStack : List <T >): PreparedChange <T > {
@@ -140,7 +133,7 @@ internal class BackStackObserver<T : Any>(
140133 }
141134 }
142135
143- private fun applyChange (scope : IScope , change : PreparedChange <T >) {
136+ private fun applyChange (scope : INavScope , change : PreparedChange <T >) {
144137 when (change) {
145138 is BackStackIsEmpty -> handleEmptyBackStack(scope)
146139 is BackStackHasNewTop -> handleNewTop(scope, change.previousTop, change.backStack)
@@ -185,7 +178,7 @@ internal class BackStackObserver<T : Any>(
185178 }
186179
187180 private fun handleNewTop (
188- scope : IScope ,
181+ scope : INavScope ,
189182 previousTop : NormalizedSentryBackStackEntry ? ,
190183 currentBackStack : BackStackData <T >,
191184 ) {
@@ -212,23 +205,19 @@ internal class BackStackObserver<T : Any>(
212205 )
213206 ?.let { transaction -> navContext.updateTransaction(transaction, scope, currentBackStack) }
214207 } else {
215- // Rotate the propagation context.
216- scope.withPropagationContext { scope.setPropagationContext(PropagationContext ()) }
208+ scope.rotatePropagationContext()
217209 }
218210
219211 storeAsPreviousTop(currentBackStack.topEntry, currentBackStack.topEntryNormalized)
220212 }
221213
222- private fun handleSameTop (scope : IScope , backStack : BackStackData <T >) {
214+ private fun handleSameTop (scope : INavScope , backStack : BackStackData <T >) {
223215 navContext.update(scope, backStack.capturedEntriesNormalized)
224216 storeAsPreviousTop(backStack.topEntry, backStack.topEntryNormalized)
225217 }
226218
227- private fun handleEmptyBackStack (scope : IScope ) {
228- navTransaction.stop(scope)
229- navContext.clear(scope)
230- navScreen.clear(scope)
231- clearPreviousTop()
219+ private fun handleEmptyBackStack (scope : INavScope ) {
220+ tearDown(scope)
232221 }
233222
234223 private fun storeAsPreviousTop (topEntry : T , topEntryNormalized : NormalizedSentryBackStackEntry ) {
@@ -240,12 +229,19 @@ internal class BackStackObserver<T : Any>(
240229 previousTop = null
241230 previousTopNormalized = null
242231 }
232+
233+ private fun tearDown (scope : INavScope ) {
234+ navTransaction.stop(scope)
235+ navContext.clear(scope)
236+ navScreen.clear(scope)
237+ clearPreviousTop()
238+ }
243239}
244240
245241/* *
246242 * A model for applying one back stack update.
247243 *
248- * Lets us separate change preparation from its application so that the [IScopes .configureScope]
244+ * Lets us separate change preparation from its application so that the [INavScopes .configureScope]
249245 * callback in charge of application can use already-computed navigation state. Otherwise, any
250246 * exceptions thrown during state computation would be swallowed by `configureScope`'s over-broad
251247 * `catch` clause.
@@ -281,7 +277,7 @@ private data class BackStackData<T>(
281277)
282278
283279/* * A helper class for managing nav transactions. */
284- private class NavTransaction (private val scopes : IScopes ) {
280+ private class NavTransaction (private val scopes : INavScopes ) {
285281
286282 private companion object {
287283 private const val TRANSACTION_ORIGIN = " auto.navigation.nav3"
@@ -291,11 +287,11 @@ private class NavTransaction(private val scopes: IScopes) {
291287
292288 /* * Starts an idle navigation transaction, or no-ops if another transaction is already active. */
293289 fun start (
294- scope : IScope ,
290+ scope : INavScope ,
295291 name : String ,
296292 arguments : Map <String , Any ?>,
297293 ): ITransaction ? {
298- clearTransactionIfFinished( scope)
294+ scope.clearFinishedTransaction( )
299295
300296 if (scope.transaction != null ) {
301297 scopes.options.logger.log(
@@ -323,6 +319,7 @@ private class NavTransaction(private val scopes: IScopes) {
323319 transactionOptions,
324320 )
325321
322+ // Not an error: INavScopes.startTransaction may decline to start a transaction by design.
326323 if (transaction.isNoOp) {
327324 return null
328325 }
@@ -335,61 +332,39 @@ private class NavTransaction(private val scopes: IScopes) {
335332 }
336333 }
337334
338- scope.withTransaction { tx ->
339- if (tx == null ) {
340- scope.transaction = transaction
341- }
342- }
335+ scope.bindTransaction(transaction)
343336
344337 return transaction
345338 }
346339
347340 /* * Finishes and unsets the active navigation transaction, if one exists. */
348- fun stop (scope : IScope ) {
341+ fun stop (scope : INavScope ) {
349342 val transaction = activeNavTransaction ? : return
350343 val status = transaction.status ? : SpanStatus .OK
351344 transaction.finish(status)
352-
353- scope.withTransaction { tx ->
354- if (tx == transaction) {
355- scope.clearTransaction()
356- }
357- }
358-
345+ scope.unbindTransaction(transaction)
359346 activeNavTransaction = null
360347 }
361-
362- /* * Clears a stale finished transaction that's still bound to the default scope. */
363- private fun clearTransactionIfFinished (scope : IScope ) {
364- scope.withTransaction { tx ->
365- if (tx?.isFinished == true ) {
366- scope.clearTransaction()
367- }
368- }
369- }
370348}
371349
372350/* * A helper class for updating [nav context][NAVIGATION_CONTEXT_KEY]. */
373- private class NavContext (private val scopes : IScopes , private val options : SentryNavOptions ) {
351+ private class NavContext (private val scopes : INavScopes , private val options : SentryNavOptions ) {
374352
375353 private companion object {
376354 private const val BACKSTACK_KEY = " Back Stack"
377- private const val NAVIGATION_CONTEXT_KEY = " Navigation"
378355 }
379356
380- fun update (scope : IScope , backStackEntries : List <NormalizedSentryBackStackEntry >) {
357+ fun update (scope : INavScope , backStackEntries : List <NormalizedSentryBackStackEntry >) {
381358 if (backStackEntries.isEmpty()) {
382359 clear(scope)
383360 return
384361 }
385362
386- scope.setContexts( NAVIGATION_CONTEXT_KEY , backStackEntries.toBackStackMap())
363+ scope.setNavigationContext( backStackEntries.toBackStackMap())
387364 }
388365
389- fun clear (scope : IScope ) {
390- // We purposefully don't call IScope.removeContexts(), as it doesn't notify IScopeObserver and
391- // therefore doesn't write its updates to disk ¯\_ (ツ)_/¯.
392- scope.setContexts(NAVIGATION_CONTEXT_KEY , null as Any? )
366+ fun clear (scope : INavScope ) {
367+ scope.setNavigationContext(null )
393368 }
394369
395370 /* *
@@ -403,12 +378,11 @@ private class NavContext(private val scopes: IScopes, private val options: Sentr
403378 */
404379 fun <T : Any > updateTransaction (
405380 transaction : ITransaction ,
406- scope : IScope ,
381+ scope : INavScope ,
407382 backStack : BackStackData <T >,
408383 ) {
409384 if (scopes.options.isEnableScreenTracking) {
410- val appContext =
411- transaction.contexts.app ? : io.sentry.protocol.Contexts (scope.contexts).app ? : App ()
385+ val appContext = transaction.contexts.app ? : scope.app ? : App ()
412386
413387 appContext.viewNames = listOf (backStack.topEntryNormalized.name)
414388 transaction.contexts.setApp(appContext)
@@ -432,12 +406,12 @@ private class NavScreen {
432406
433407 private var lastScreenName: String? = null
434408
435- fun update (scope : IScope , currentEntry : NormalizedSentryBackStackEntry ) {
409+ fun update (scope : INavScope , currentEntry : NormalizedSentryBackStackEntry ) {
436410 scope.screen = currentEntry.name
437411 lastScreenName = currentEntry.name
438412 }
439413
440- fun clear (scope : IScope ) {
414+ fun clear (scope : INavScope ) {
441415 val screenName = lastScreenName ? : return
442416 if (scope.screen == screenName) {
443417 scope.screen = null
@@ -447,7 +421,7 @@ private class NavScreen {
447421}
448422
449423/* * A helper class for generating nav breadcrumbs. */
450- private class NavBreadcrumbs (private val scopes : IScopes ) {
424+ private class NavBreadcrumbs (private val scopes : INavScopes ) {
451425
452426 fun emit (fromEntry : NormalizedSentryBackStackEntry ? , toEntry : NormalizedSentryBackStackEntry ) {
453427 val breadcrumb =
0 commit comments