Skip to content

Commit 509777e

Browse files
0xadam-brownclaude
andcommitted
ref(android): Give each SentryNavEffect a lease registration and a transaction handle
NavLeaseCoordinator now observes each candidate's lifecycle itself, via register(), and publishes and unpublishes its nav state through LeasedNavState. NavScopes folds the old NavScope into an inner class and publishes its StagedNavData and nav transaction through a single publisher. Each NavScopes instance now keeps its nav transaction in a StagedNavTransaction handle, which INavScope.startNavTransaction and finishNavTransaction drive, rather than BackStackObserver binding and unbinding transactions itself and the coordinator retaining them. StagedNavTransaction alone decides whether the scope's transaction slot is free. No behavior change: nav transactions still start only with the nav lease and a free slot, and an owner that regains the lease still gets its unfinished transaction back. The integration tests are unchanged. This prepares for deferring transactions requested without the lease. Refs JAVA-785 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 07a8d1b commit 509777e

17 files changed

Lines changed: 2623 additions & 1471 deletions

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ internal data class NormalizedSentryBackStackEntry(
450450
internal fun List<NormalizedSentryBackStackEntry>.serialize(): List<Map<String, Any?>> =
451451
map(NormalizedSentryBackStackEntry::serialize)
452452

453-
/** Why host-provided arguments were unavailable in emitted navigation data. */
453+
/** Why host-provided arguments were unavailable in emitted nav state. */
454454
internal enum class ArgumentDropReason(
455455
val serializedValue: String,
456456
val exhaustsUpdateBudget: Boolean = false,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public class SentryBackStackEntry(
7878
*
7979
* If [map] throws, returns `null`, or returns a blank [name][SentryBackStackEntry.name], Sentry
8080
* records the destination as "/unknown". Doing so signals that name extraction needs to be fixed
81-
* while avoiding misleading gaps in navigation data.
81+
* while avoiding misleading gaps in nav state.
8282
*
8383
* For instance, if a user navigates from `/home -> /detail -> /settings`, but the mapper for
8484
* `/detail` throws, the back stack record will be `/home -> /unknown -> /settings` rather than

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

Lines changed: 27 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@ package io.sentry.compose.navigation3
22

33
import io.sentry.Breadcrumb
44
import io.sentry.Hint
5-
import io.sentry.ITransaction
65
import io.sentry.SentryIntegrationPackageStorage
7-
import io.sentry.SentryLevel.DEBUG
86
import io.sentry.SentryLevel.INFO
97
import io.sentry.SentryOptions
10-
import io.sentry.SpanStatus
118
import io.sentry.TransactionContext
129
import io.sentry.TransactionOptions
1310
import io.sentry.compose.navigation3.BackStackConverter.RetentionPolicy
@@ -16,6 +13,7 @@ import io.sentry.compose.navigation3.PreparedChange.BackStackHasSameTop
1613
import io.sentry.compose.navigation3.PreparedChange.BackStackIsEmpty
1714
import io.sentry.compose.navigation3.lifecycle.INavScope
1815
import io.sentry.compose.navigation3.lifecycle.INavScopes
16+
import io.sentry.compose.navigation3.lifecycle.INavTransaction
1917
import io.sentry.compose.navigation3.lifecycle.NAVIGATION_CONTEXT_KEY
2018
import io.sentry.protocol.App
2119
import io.sentry.protocol.TransactionNameSource
@@ -59,7 +57,7 @@ internal class BackStackObserver<T : Any>(
5957

6058
private val backStackConverter = BackStackConverter(entryMapper, scopes.options.logger)
6159

62-
private val navTransaction = NavTransaction(scopes)
60+
private val navTransactions = NavTransactions(scopes)
6361
private val navBreadcrumbs = NavBreadcrumbs(scopes)
6462
private val navScreen = NavScreen()
6563
private val navContext = NavContext(scopes, options)
@@ -83,7 +81,7 @@ internal class BackStackObserver<T : Any>(
8381
}
8482

8583
/**
86-
* Updates Sentry nav data based on the provided [backStack].
84+
* Updates Sentry nav state based on the provided [backStack].
8785
*
8886
* **Data generated**
8987
*
@@ -113,7 +111,7 @@ internal class BackStackObserver<T : Any>(
113111
}
114112

115113
/**
116-
* Clears the nav data this observer recorded.
114+
* Clears the nav state this observer recorded.
117115
*
118116
* Call it when observation ends so that no stale screen, nav context, or nav transaction outlives
119117
* it.
@@ -194,10 +192,10 @@ internal class BackStackObserver<T : Any>(
194192
navBreadcrumbs.emit(fromEntry = previousTop, toEntry = currentBackStack.topEntryNormalized)
195193
}
196194

197-
navTransaction.stop(scope)
195+
navTransactions.stop(scope)
198196

199197
if (areNavigationTransactionsEnabled) {
200-
navTransaction
198+
navTransactions
201199
.start(
202200
scope,
203201
currentTop.name,
@@ -231,7 +229,7 @@ internal class BackStackObserver<T : Any>(
231229
}
232230

233231
private fun tearDown(scope: INavScope) {
234-
navTransaction.stop(scope)
232+
navTransactions.stop(scope)
235233
navContext.clear(scope)
236234
navScreen.clear(scope)
237235
clearPreviousTop()
@@ -277,32 +275,22 @@ private data class BackStackData<T>(
277275
)
278276

279277
/** A helper class for managing nav transactions. */
280-
private class NavTransaction(private val scopes: INavScopes) {
278+
private class NavTransactions(private val scopes: INavScopes) {
281279

282280
private companion object {
283281
private const val TRANSACTION_ORIGIN = "auto.navigation.nav3"
284282
}
285283

286-
private var activeNavTransaction: ITransaction? = null
287-
288-
/** Starts an idle navigation transaction, or no-ops if another transaction is already active. */
284+
/**
285+
* Starts an idle navigation transaction, or no-ops if another transaction is already active.
286+
*
287+
* [Stop][stop] the current navigation transaction first. See [INavScope.startNavTransaction].
288+
*/
289289
fun start(
290290
scope: INavScope,
291291
name: String,
292292
arguments: Map<String, Any?>,
293-
): ITransaction? {
294-
scope.clearFinishedTransaction()
295-
296-
if (scope.transaction != null) {
297-
scopes.options.logger.log(
298-
DEBUG,
299-
"Nav3 transaction for route %s won't be created because another transaction is active.",
300-
name,
301-
)
302-
303-
return null
304-
}
305-
293+
): INavTransaction? {
306294
val transactionOptions =
307295
TransactionOptions().also {
308296
it.isWaitForChildren = true
@@ -314,36 +302,25 @@ private class NavTransaction(private val scopes: INavScopes) {
314302
}
315303

316304
val transaction =
317-
scopes.startTransaction(
305+
scope.startNavTransaction(
318306
TransactionContext(name, TransactionNameSource.ROUTE, NAVIGATION_OP),
319307
transactionOptions,
320-
)
308+
) ?: return null
321309

322-
// Not an error: INavScopes.startTransaction may decline to start a transaction by design.
323-
if (transaction.isNoOp) {
324-
return null
310+
if (arguments.isNotEmpty()) {
311+
transaction.setData("arguments", arguments)
325312
}
326313

327-
activeNavTransaction = transaction
328-
329-
transaction.apply {
330-
if (arguments.isNotEmpty()) {
331-
setData("arguments", arguments)
332-
}
333-
}
334-
335-
scope.bindTransaction(transaction)
336-
337314
return transaction
338315
}
339316

340-
/** Finishes and unsets the active navigation transaction, if one exists. */
317+
/**
318+
* Finishes and unsets the active navigation transaction, if one exists.
319+
*
320+
* See [INavScope.finishNavTransaction].
321+
*/
341322
fun stop(scope: INavScope) {
342-
val transaction = activeNavTransaction ?: return
343-
val status = transaction.status ?: SpanStatus.OK
344-
transaction.finish(status)
345-
scope.unbindTransaction(transaction)
346-
activeNavTransaction = null
323+
scope.finishNavTransaction()
347324
}
348325
}
349326

@@ -377,15 +354,15 @@ private class NavContext(private val scopes: INavScopes, private val options: Se
377354
* proper values to the transaction context instead.
378355
*/
379356
fun <T : Any> updateTransaction(
380-
transaction: ITransaction,
357+
transaction: INavTransaction,
381358
scope: INavScope,
382359
backStack: BackStackData<T>,
383360
) {
384361
if (scopes.options.isEnableScreenTracking) {
385-
val appContext = transaction.contexts.app ?: scope.app ?: App()
362+
val appContext = scope.app ?: App()
386363

387364
appContext.viewNames = listOf(backStack.topEntryNormalized.name)
388-
transaction.contexts.setApp(appContext)
365+
transaction.setApp(appContext)
389366
}
390367

391368
if (options.captureBackStack && backStack.capturedEntriesNormalized.isNotEmpty()) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import io.sentry.compose.navigation3.lifecycle.NavScopes
2222
* **Thread safety**
2323
*
2424
* This class is ***not*** thread-safe. Invocations should be confined to the same thread as the
25-
* [LifecycleEventObserver] callbacks implemented by [NavScopes].
25+
* [LifecycleEventObserver] callbacks through which the [NavLeaseCoordinator] observes the attached
26+
* lifecycle.
2627
*/
2728
internal class CoordinatedBackStackObserver<T : Any>(
2829
scopes: IScopes,

0 commit comments

Comments
 (0)