Skip to content

Commit a85abfe

Browse files
committed
fix(compose): Prevent dangling spans in SentryTraced
Commit repairs a few defects in the implementation of SentryTraced while aiming to maintain parity with the previous approach. In particular it: - Prevents dangling composition spans in cases where the Compose runtime abandons the composition. (We now delay creating the span until a SideEffect executes.) - Prevents dangling render spans in cases where drawContent() throws and the host app recovers. (Similarly, we delay creating the span until drawContent() completes successfully.)
1 parent 2fd0a9d commit a85abfe

2 files changed

Lines changed: 70 additions & 26 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
### Fixes
1515

16+
- Prevent `SentryTraced` from producing dangling spans if recomposition is abandoned or drawing fails ([#6049](https://github.com/getsentry/sentry-java/pull/6049))
1617
- Keep dropped tombstone and ANR events dropped, instead of reporting the same app exit again at every app start ([#6002](https://github.com/getsentry/sentry-java/pull/6002))
1718
- Apply `Sentry.withScope` and `Sentry.withIsolationScope` data to events captured inside the callback when `globalHubMode` is enabled ([#6004](https://github.com/getsentry/sentry-java/pull/6004))
1819
- `globalHubMode` is enabled by default on Android, where tags, extras, contexts and level set inside the callback were silently dropped

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

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import androidx.compose.foundation.layout.Box
44
import androidx.compose.foundation.layout.BoxScope
55
import androidx.compose.runtime.Composable
66
import androidx.compose.runtime.Immutable
7+
import androidx.compose.runtime.SideEffect
78
import androidx.compose.runtime.compositionLocalOf
89
import androidx.compose.runtime.remember
910
import androidx.compose.ui.ExperimentalComposeUiApi
1011
import androidx.compose.ui.Modifier
1112
import androidx.compose.ui.draw.drawWithContent
1213
import io.sentry.ISpan
1314
import io.sentry.Sentry
15+
import io.sentry.SentryDate
1416
import io.sentry.SpanOptions
1517
import io.sentry.compose.SentryModifier.sentryTag
1618

@@ -22,14 +24,6 @@ private const val OP_RENDER = "ui.render"
2224

2325
private const val OP_TRACE_ORIGIN = "auto.ui.jetpack_compose"
2426

25-
@Immutable private class ImmutableHolder<T>(var item: T)
26-
27-
private fun getRootSpan(): ISpan? {
28-
var rootSpan: ISpan? = null
29-
Sentry.configureScope { rootSpan = it.transaction }
30-
return rootSpan
31-
}
32-
3327
private val localSentryCompositionParentSpan = compositionLocalOf {
3428
ImmutableHolder(
3529
getRootSpan()
@@ -62,6 +56,15 @@ private val localSentryRenderingParentSpan = compositionLocalOf {
6256
)
6357
}
6458

59+
@Immutable internal class ImmutableHolder<T>(var item: T)
60+
61+
/**
62+
* Creates spans for tracking the time required to compose the wrapped [content], and a span for its
63+
* initial draw.
64+
*
65+
* Spans are approximate and include work performed by any composables [content] invokes. Abandoned
66+
* recompositions are ignored.
67+
*/
6568
@ExperimentalComposeUiApi
6669
@Composable
6770
public fun SentryTraced(
@@ -70,32 +73,72 @@ public fun SentryTraced(
7073
enableUserInteractionTracing: Boolean = true,
7174
content: @Composable BoxScope.() -> Unit,
7275
) {
73-
val parentCompositionSpan = localSentryCompositionParentSpan.current
74-
val parentRenderingSpan = localSentryRenderingParentSpan.current
75-
val compositionSpan =
76-
parentCompositionSpan.item?.startChild(OP_COMPOSE, tag)?.apply {
77-
spanContext.origin = OP_TRACE_ORIGIN
78-
}
79-
val firstRendered = remember { ImmutableHolder(false) }
80-
76+
val alreadyRendered = remember { ImmutableHolder(false) }
8177
val baseModifier = if (enableUserInteractionTracing) modifier.sentryTag(tag) else modifier
8278

79+
val parentCompositionSpan = localSentryCompositionParentSpan.current.item
80+
val parentRenderingSpan = localSentryRenderingParentSpan.current.item
81+
val dateProvider = Sentry.getCurrentScopes().options.dateProvider
82+
83+
val compositionStart = dateProvider.now()
84+
8385
Box(
8486
modifier =
8587
baseModifier.drawWithContent {
86-
val renderSpan =
87-
if (!firstRendered.item) {
88-
parentRenderingSpan.item?.startChild(OP_RENDER, tag)
89-
} else {
90-
null
91-
}
92-
drawContent()
93-
firstRendered.item = true
94-
renderSpan?.finish()
88+
if (alreadyRendered.item) {
89+
drawContent()
90+
} else {
91+
val renderStart = dateProvider.now()
92+
drawContent()
93+
val renderEnd = dateProvider.now()
94+
95+
alreadyRendered.item = true
96+
recordRenderSpan(parentRenderingSpan, tag, renderStart, renderEnd)
97+
}
9598
},
9699
propagateMinConstraints = true,
97100
) {
98101
content()
99102
}
100-
compositionSpan?.finish()
103+
val compositionEnd = dateProvider.now()
104+
105+
if (parentCompositionSpan != null) {
106+
SideEffect {
107+
recordCompositionSpan(
108+
parentSpan = parentCompositionSpan,
109+
tag = tag,
110+
startTimestamp = compositionStart,
111+
endTimestamp = compositionEnd,
112+
)
113+
}
114+
}
115+
}
116+
117+
private fun getRootSpan(): ISpan? {
118+
var rootSpan: ISpan? = null
119+
Sentry.configureScope { rootSpan = it.transaction }
120+
return rootSpan
121+
}
122+
123+
private fun recordCompositionSpan(
124+
parentSpan: ISpan?,
125+
tag: String,
126+
startTimestamp: SentryDate,
127+
endTimestamp: SentryDate,
128+
) {
129+
parentSpan?.startChild(OP_COMPOSE, tag, startTimestamp)?.apply {
130+
spanContext.origin = OP_TRACE_ORIGIN
131+
finish(null, endTimestamp)
132+
}
133+
}
134+
135+
private fun recordRenderSpan(
136+
parentSpan: ISpan?,
137+
tag: String,
138+
startTimestamp: SentryDate,
139+
endTimestamp: SentryDate,
140+
) {
141+
parentSpan?.startChild(OP_RENDER, tag, startTimestamp)?.apply {
142+
finish(null, endTimestamp)
143+
}
101144
}

0 commit comments

Comments
 (0)