@@ -83,15 +83,22 @@ import io.sentry.protocol.Contexts
8383 * A possible solution would be to implement a parent span delivery system similar to
8484 * [LocalSentrySpan], but that's a task for another day.
8585 *
86- * **Only leaseholders generate nav transactions and nav state **
86+ * **Only leaseholders publish nav state and bind nav transactions **
8787 *
8888 * This class works by only allowing `NavScopes` instances holding a lease to publish their
89- * navigation state and transactions to the underlying `IScope`.
89+ * navigation state and bind their transactions to the underlying `IScope`.
9090 *
9191 * `NavScopes` instances without a lease write to a [stagedNavData] holder, which lets them preserve
9292 * their latest updates without overwriting another instance's published data. When an instance
9393 * acquires the lease, it publishes its staged data to the underlying `IScope`.
9494 *
95+ * Nav transactions are started whether or not an instance holds the lease, so each one begins when
96+ * its destination is reached. Without the lease, a transaction is retained rather than bound, and
97+ * bound once the instance acquires the lease, if it's still unfinished and the scope is free. An
98+ * unbound transaction collects no child spans, so if its instance stays without the lease longer
99+ * than the idle timeout, the transaction finishes empty and is dropped. Propagation context
100+ * rotations requested without the lease are likewise deferred until the lease is acquired.
101+ *
95102 * Breadcrumbs are the exception: they only append to the scope, so every instance records them
96103 * immediately, preserving their real order whether or not it holds the lease.
97104 *
@@ -285,17 +292,31 @@ internal class NavScopes(
285292 }
286293 }
287294
295+ /* * Whether this instance currently holds the nav lease. */
296+ fun hasLease (): Boolean = coordinator.hasLease(owner)
297+
298+ /* *
299+ * Starts a nav transaction whether or not this instance holds the nav lease, so the transaction
300+ * begins when its destination is actually reached.
301+ *
302+ * Only the leaseholder binds it to the scope (see [NavScope.bindTransaction]). Without the lease,
303+ * the [coordinator] retains it and binds it once this instance acquires the lease, provided it's
304+ * still unfinished and the scope is free.
305+ *
306+ * Returns a no-op transaction while this instance isn't [attached][attach] (in particular, after
307+ * it's [disposed][dispose]), so it can't start transactions that nothing would finish.
308+ */
288309 override fun startTransaction (
289310 transactionContext : TransactionContext ,
290311 transactionOptions : TransactionOptions ,
291312 ): ITransaction {
292- if (! coordinator.hasLease (owner)) {
313+ if (! coordinator.isRegistered (owner)) {
293314 return NoOpTransaction .getInstance()
294315 }
295316
296317 return delegate.startTransaction(transactionContext, transactionOptions).also { transaction ->
297318 if (! transaction.isNoOp) {
298- coordinator.bindTransaction (owner, transaction)
319+ coordinator.retainTransaction (owner, transaction)
299320 }
300321 }
301322 }
@@ -354,8 +375,10 @@ internal class NavScopes(
354375 * breadcrumbs) only append to the scope, so they can't overwrite another instance's data and are
355376 * recorded immediately, irrespective of lease ownership.
356377 *
357- * Propagation context isn't staged either: like nav transactions, it's only changed by the
358- * leaseholder, and changes made without the lease are discarded.
378+ * Propagation context isn't staged either, since an earlier propagation context mustn't overwrite
379+ * the scope's current one. Instead, a rotation requested without the lease is
380+ * [deferred][deferPropagationContextRotation] and performed once when the lease is acquired, so the
381+ * destination reached in the meantime still gets a fresh trace.
359382 */
360383internal class StagedNavData {
361384
@@ -364,6 +387,9 @@ internal class StagedNavData {
364387 private var hasNavigationContextUpdate = false
365388 private var hasScreenUpdate = false
366389
390+ /* * Whether a propagation context rotation was requested while this instance lacked the lease. */
391+ private var hasDeferredPropagationContextRotation = false
392+
367393 // These may differ from the staged values after background updates. We keep the exact published
368394 // references so cleanup preserves host replacements, including structurally equal replacements.
369395 private var publishedNavigationContext: Any? = null
@@ -396,6 +422,11 @@ internal class StagedNavData {
396422 }
397423 }
398424
425+ /* * Requests a propagation context rotation for when this instance next [publishes][publishTo]. */
426+ fun deferPropagationContextRotation () {
427+ hasDeferredPropagationContextRotation = true
428+ }
429+
399430 fun publishTo (realScope : IScope ) {
400431 // A new leaseholder publishes a complete navigation state. Clearing fields it hasn't staged
401432 // prevents the previous leaseholder's values from lingering after a handoff.
@@ -404,6 +435,10 @@ internal class StagedNavData {
404435 if (hasNavigationContextUpdate) contexts[NAVIGATION_CONTEXT_KEY ] else null ,
405436 realScope,
406437 )
438+ if (hasDeferredPropagationContextRotation) {
439+ realScope.withPropagationContext { realScope.propagationContext = PropagationContext () }
440+ hasDeferredPropagationContextRotation = false
441+ }
407442 }
408443
409444 /* * Called only for the last publishing session; releasing its lease alone does not clear data. */
@@ -437,9 +472,11 @@ internal class StagedNavData {
437472 * An [IScope] that writes to [delegate] only when the current [owner] holds the nav lease.
438473 *
439474 * Without the lease, nav state (screen and nav context) is redirected to [stagedNavData] and
440- * published if [owner] later acquires the lease, while transaction binding and propagation context
441- * rotation are discarded. Reads of the scope's app context and transaction, and unbinding [owner]'s
442- * own transaction, always go to [delegate].
475+ * published if [owner] later acquires the lease. Transaction binding is skipped, as the
476+ * [coordinator] binds [owner]'s retained transaction when it acquires the lease, and propagation
477+ * context rotation is deferred until then. Reading the scope's app context and unbinding [owner]'s
478+ * own transaction always go to [delegate]; reading the scope's transaction does too, except that
479+ * another owner's nav transaction is hidden without the lease (see [transaction]).
443480 */
444481internal class NavScope (
445482 private val delegate : IScope ,
@@ -462,8 +499,17 @@ internal class NavScope(
462499 override val app: App ?
463500 get() = Contexts (delegate.contexts).app
464501
502+ /* *
503+ * The transaction bound to the scope. Without the lease, another owner's nav transaction isn't
504+ * reported: it doesn't stop [owner] from starting its own, which the [coordinator] binds in its
505+ * place when [owner] acquires the lease. Non-nav transactions (e.g., the host's) are always
506+ * reported.
507+ */
465508 override val transaction: ITransaction ?
466- get() = delegate.transaction
509+ get() =
510+ delegate.transaction?.takeUnless { current ->
511+ ! hasLease && coordinator.isNavTransaction(current)
512+ }
467513
468514 override fun bindTransaction (transaction : ITransaction ) {
469515 if (! hasLease) {
@@ -498,6 +544,8 @@ internal class NavScope(
498544 override fun rotatePropagationContext () {
499545 if (hasLease) {
500546 delegate.withPropagationContext { delegate.propagationContext = PropagationContext () }
547+ } else {
548+ stagedNavData.deferPropagationContextRotation()
501549 }
502550 }
503551}
@@ -537,7 +585,10 @@ internal interface INavScope {
537585 /* * A copy of the scope's app context, if any. */
538586 val app: App ?
539587
540- /* * The transaction bound to the scope, if any. */
588+ /* *
589+ * The transaction bound to the scope, if any, as far as starting a nav transaction is concerned:
590+ * see [NavScope.transaction].
591+ */
541592 val transaction: ITransaction ?
542593
543594 /* * Binds [transaction] to the scope if no transaction is bound to it. */
0 commit comments