Skip to content

Commit 20dafa5

Browse files
committed
fix(compose): finish evicted root spans instead of leaving them open
isIdle spans only auto-finish when the whole transaction finishes, so evicting a cached root span from RootSpans without finishing it left it open on the transaction. If the same scopes was used again later, a fresh root span was created alongside the still-open, now-untracked original, showing up as a duplicate root span once the transaction finished. release now finishes the evicted composition/rendering spans before dropping them from the cache.
1 parent 863894f commit 20dafa5

2 files changed

Lines changed: 29 additions & 2 deletions

File tree

sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeTracing.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,12 @@ private class RootSpans {
9191
val remaining = (refCounts[scopes] ?: 1) - 1
9292
if (remaining <= 0) {
9393
refCounts.remove(scopes)
94-
compositionSpans.remove(scopes)
95-
renderingSpans.remove(scopes)
94+
// These are idle spans: they only auto-finish when the whole transaction finishes, so
95+
// evicting them from the cache without finishing them here would leave them open on the
96+
// transaction. If the same scopes is used again later, a fresh pair would be created
97+
// alongside the still-open, now-untracked originals, showing up as duplicate root spans.
98+
compositionSpans.remove(scopes)?.item?.finish()
99+
renderingSpans.remove(scopes)?.item?.finish()
96100
} else {
97101
refCounts[scopes] = remaining
98102
}

sentry-compose/src/androidUnitTest/kotlin/io/sentry/compose/SentryTracedTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import io.sentry.SentryOptions
2020
import io.sentry.TransactionOptions
2121
import io.sentry.test.createTestScopes
2222
import kotlin.test.assertEquals
23+
import kotlin.test.assertTrue
2324
import org.junit.After
2425
import org.junit.Rule
2526
import org.junit.Test
@@ -147,4 +148,26 @@ class SentryTracedTest {
147148

148149
assertEquals(1, tx.spans.count { it.operation == "ui.compose.composition" })
149150
}
151+
152+
@Test
153+
fun `evicting a root span cache entry finishes the underlying span`() {
154+
val scopes = newTracingScopes()
155+
val tx = scopes.startBoundTransaction("custom-scopes-tx")
156+
var mounted by mutableStateOf(true)
157+
158+
rule.setContent {
159+
if (mounted) {
160+
CompositionLocalProvider(LocalSentryScopes provides scopes) {
161+
SentryTraced(tag = "first") { Box {} }
162+
}
163+
}
164+
}
165+
rule.waitForIdle()
166+
167+
mounted = false
168+
rule.waitForIdle()
169+
170+
val compositionRootSpan = tx.spans.first { it.operation == "ui.compose.composition" }
171+
assertTrue(compositionRootSpan.isFinished)
172+
}
150173
}

0 commit comments

Comments
 (0)