FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(compose): Introduce LocalSentrySpan (#6112) · getsentry/sentry-java@9fd9105 · GitHub

Commit 9fd9105

Browse files
feat(compose): Introduce LocalSentrySpan (#6112)
Commit: 1. Defines a LocalSentrySpan that lets Sentry and host apps deliver ISpan instances throughout composable subtrees. The delivered span can be used to parent any spans the receiving subtree produces, thereby freeing child composables from having to care about the ISpan hierarchies constructed by their ancestors. 2. Introduces an internal UnsetSentrySpan + bootstrapping system that lets existing Sentry composable infrastructure provide its own ISpan in situations where the environment doesn't set a LocalSentrySpan. 3. Updates SentryTraced to use both LocalSentrySpan and the bootstrapping system. --------- Co-authored-by: Tabish Ahmad <tabishahmad1@gmail.com>
1 parent 8f0dc10 commit 9fd9105

5 files changed

Lines changed: 163 additions & 39 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Add `LocalSentrySpan` to `sentry-compose` so apps can provide a parent `ISpan` to a composable subtree and have nested `SentryTraced` spans attach to it ([#6112]https://github.com/getsentry/sentry-java/pull/6112)
8+
59
### Fixes
610

711
- Disable URL caching when reading `META-INF/MANIFEST.MF` files during version detection so that the SDK no longer keeps jar file handles open for the life of the process ([#6124](https://github.com/getsentry/sentry-java/pull/6124)

‎sentry-compose/api/android/sentry-compose.api‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ public final class io/sentry/compose/BuildConfig {
66
public fun <init> ()V
77
}
88

9+
public final class io/sentry/compose/LocalSentrySpanKt {
10+
public static final fun getLocalSentrySpan ()Landroidx/compose/runtime/ProvidableCompositionLocal;
11+
}
12+
913
public final class io/sentry/compose/SentryComposeHelperKt {
1014
public static final fun boundsInWindow (Landroidx/compose/ui/layout/LayoutCoordinates;Landroidx/compose/ui/layout/LayoutCoordinates;)Landroidx/compose/ui/geometry/Rect;
1115
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.sentry.compose
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.CompositionLocalProvider
5+
import androidx.compose.runtime.ProvidableCompositionLocal
6+
import androidx.compose.runtime.compositionLocalOf
7+
import androidx.compose.ui.ExperimentalComposeUiApi
8+
import io.sentry.ISpan
9+
import io.sentry.NoOpSpan
10+
import io.sentry.Sentry
11+
12+
/**
13+
* A [ProvidableCompositionLocal] for delivering [ISpan]s to composable subtrees. The delivered span
14+
* should be used to parent any spans the receiving subtree produces.
15+
*
16+
* Lets child composables remain agnostic about the [ISpan] hierarchies constructed by their
17+
* ancestors.
18+
*/
19+
@ExperimentalComposeUiApi
20+
public val LocalSentrySpan: ProvidableCompositionLocal<ISpan> = compositionLocalOf {
21+
UnsetSentrySpan
22+
}
23+
24+
/**
25+
* A wrapper for any composable subtree that should be passed the provided [span] via
26+
* [LocalSentrySpan].
27+
*
28+
* The span is [normalized][normalize].
29+
*/
30+
@OptIn(ExperimentalComposeUiApi::class)
31+
@Composable
32+
internal fun ProvideSentrySpan(span: ISpan?, content: @Composable () -> Unit) {
33+
CompositionLocalProvider(LocalSentrySpan provides span.normalize()) {
34+
content()
35+
}
36+
}
37+
38+
/**
39+
* Returns the receiver as-is unless it's an [UnsetSentrySpan], in which case it returns the current
40+
* transaction.
41+
*
42+
* `*Bootstrap` methods are for internal use only. They're designed for scenarios where
43+
* [LocalSentrySpan] hasn't been set, but we want to provide a reasonable alternative or fall back
44+
* to preexisting behavior.
45+
*/
46+
internal fun ISpan.orBootstrapCurrentTransaction(): ISpan =
47+
if (this.isUnset()) {
48+
Sentry.getCurrentScopes().transaction.normalize()
49+
} else {
50+
this
51+
}
52+
53+
/**
54+
* A sentinel span indicating that [LocalSentrySpan] hasn't been set. For internal use only.
55+
*
56+
* Note: This must be a distinct type from [NoOpSpan] so that [isUnset] can determine whether
57+
* `LocalSentrySpan` was set with a `NoOpSpan` or was never set at all.
58+
*
59+
* Our own implementations often need to make that distinction so they can fall back to a reasonable
60+
* parent span if their environment doesn't provide one. We use a sentinel to avoid complicating our
61+
* public API for a distinction irrelevant to host apps.
62+
*/
63+
private object UnsetSentrySpan : ISpan by NoOpSpan.getInstance()
64+
65+
private fun ISpan.isUnset(): Boolean = this === UnsetSentrySpan
66+
67+
private fun ISpan?.normalize(): ISpan =
68+
when {
69+
this === UnsetSentrySpan || this is NoOpSpan -> this
70+
this == null || this.isFinished -> NoOpSpan.getInstance()
71+
else -> this
72+
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import androidx.compose.ui.Modifier
1010
import androidx.compose.ui.draw.drawWithContent
1111
import io.sentry.ISpan
1212
import io.sentry.Instrumenter
13-
import io.sentry.NoOpSpan
1413
import io.sentry.Sentry
1514
import io.sentry.SentryDate
1615
import io.sentry.SpanOptions
@@ -71,7 +70,7 @@ public fun SentryTraced(
7170
) {
7271
val baseModifier = if (enableUserInteractionTracing) modifier.sentryTag(tag) else modifier
7372
val scopes = Sentry.getCurrentScopes()
74-
val ownerSpan = scopes.transaction ?: NoOpSpan.getInstance()
73+
val ownerSpan = LocalSentrySpan.current.orBootstrapCurrentTransaction()
7574

7675
val alreadyComposed = remember(ownerSpan) { MutableRef(false) }
7776
val alreadyRendered = remember(ownerSpan) { MutableRef(false) }

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

Lines changed: 82 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
2828
import com.google.common.truth.Truth.assertThat
2929
import io.sentry.ISpan
3030
import io.sentry.ITransaction
31+
import io.sentry.NoOpSpan
3132
import io.sentry.Sentry
3233
import io.sentry.SentryOptions
3334
import io.sentry.TransactionOptions
@@ -75,7 +76,9 @@ class SentryTracedTest {
7576
fun `records a composition span for the initial composition`() {
7677
val tx = initSentryAndStartTransaction("tx")
7778

78-
rule.setContent { SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp)) } }
79+
rule.setContent {
80+
ProvideSentrySpan(tx) { SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp)) } }
81+
}
7982
tx.waitForSpanCount(OP_COMPOSE, 1)
8083

8184
assertThat(tx.countSpans(OP_PARENT_COMPOSITION)).isEqualTo(1)
@@ -88,7 +91,7 @@ class SentryTracedTest {
8891
}
8992

9093
@Test
91-
fun `falls back to the current transaction when no owner span is provided`() {
94+
fun `falls back to the current transaction when no owner span is provided via LocalSentrySpan`() {
9295
val tx = initSentryAndStartTransaction("tx")
9396

9497
rule.setContent { SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp)) } }
@@ -112,12 +115,32 @@ class SentryTracedTest {
112115
}
113116

114117
@Test
115-
fun `renders content without spans when the current transaction is finished`() {
118+
fun `renders content without spans when provided owner span is a no-op`() {
119+
val tx = initSentryAndStartTransaction("tx")
120+
121+
rule.setContent {
122+
ProvideSentrySpan(NoOpSpan.getInstance()) {
123+
SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp).testTag("content")) }
124+
}
125+
}
126+
rule.waitForIdle()
127+
128+
rule.onNodeWithTag("content").assertExists()
129+
assertThat(tx.countSpans(OP_PARENT_COMPOSITION)).isEqualTo(0)
130+
assertThat(tx.countSpans(OP_COMPOSE)).isEqualTo(0)
131+
assertThat(tx.countSpans(OP_PARENT_RENDER)).isEqualTo(0)
132+
assertThat(tx.countSpans(OP_RENDER)).isEqualTo(0)
133+
}
134+
135+
@Test
136+
fun `renders content without spans when provided owner span is finished`() {
116137
val tx = initSentryAndStartTransaction("tx")
117138
rule.runOnUiThread { tx.finish() }
118139

119140
rule.setContent {
120-
SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp).testTag("content")) }
141+
ProvideSentrySpan(tx) {
142+
SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp).testTag("content")) }
143+
}
121144
}
122145
rule.waitForIdle()
123146

@@ -136,7 +159,9 @@ class SentryTracedTest {
136159
}
137160

138161
rule.setContent {
139-
SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp).testTag("content")) }
162+
ProvideSentrySpan(tx) {
163+
SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp).testTag("content")) }
164+
}
140165
}
141166
rule.waitForIdle()
142167
drawContent()
@@ -153,9 +178,11 @@ class SentryTracedTest {
153178
val tx = initSentryAndStartTransaction("tx")
154179

155180
rule.setContent {
156-
Column {
157-
SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp)) }
158-
SentryTraced(tag = "add_to_cart_button") { Box(Modifier.size(1.dp)) }
181+
ProvideSentrySpan(tx) {
182+
Column {
183+
SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp)) }
184+
SentryTraced(tag = "add_to_cart_button") { Box(Modifier.size(1.dp)) }
185+
}
159186
}
160187
}
161188

@@ -170,9 +197,11 @@ class SentryTracedTest {
170197
val tx = initSentryAndStartTransaction("tx")
171198

172199
rule.setContent {
173-
Column {
174-
SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp)) }
175-
SentryTraced(tag = "add_to_cart_button") { Box(Modifier.size(1.dp)) }
200+
ProvideSentrySpan(tx) {
201+
Column {
202+
SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp)) }
203+
SentryTraced(tag = "add_to_cart_button") { Box(Modifier.size(1.dp)) }
204+
}
176205
}
177206
}
178207
tx.waitForSpanCount(OP_COMPOSE, 2)
@@ -200,9 +229,11 @@ class SentryTracedTest {
200229
val tx = initSentryAndStartTransaction("tx")
201230

202231
rule.setContent {
203-
val currentStep = step
204-
SentryTraced(tag = "product_info") {
205-
Box(Modifier.size((currentStep + 1).dp).testTag("content-$currentStep"))
232+
ProvideSentrySpan(tx) {
233+
val currentStep = step
234+
SentryTraced(tag = "product_info") {
235+
Box(Modifier.size((currentStep + 1).dp).testTag("content-$currentStep"))
236+
}
206237
}
207238
}
208239
tx.waitForSpanCount(OP_COMPOSE, 1)
@@ -230,15 +261,17 @@ class SentryTracedTest {
230261
val tx = initSentryAndStartTransaction("tx")
231262

232263
rule.setContent {
233-
val currentStep = step
234-
SentryTraced(tag = "product_info") {
235-
val state = remember {
236-
rememberedInstanceCount++
237-
Any()
264+
ProvideSentrySpan(tx) {
265+
val currentStep = step
266+
SentryTraced(tag = "product_info") {
267+
val state = remember {
268+
rememberedInstanceCount++
269+
Any()
270+
}
271+
DisposableEffect(Unit) { onDispose { disposeCount++ } }
272+
rememberedState = state
273+
Box(Modifier.size((currentStep + 1).dp).testTag("content-$currentStep"))
238274
}
239-
DisposableEffect(Unit) { onDispose { disposeCount++ } }
240-
rememberedState = state
241-
Box(Modifier.size((currentStep + 1).dp).testTag("content-$currentStep"))
242275
}
243276
}
244277
tx.waitForSpanCount(OP_COMPOSE, 1)
@@ -260,18 +293,24 @@ class SentryTracedTest {
260293
@Test
261294
fun `starts recording once an owner span becomes available`() {
262295
var step by mutableStateOf(0)
296+
var sentrySpan by mutableStateOf<ISpan>(NoOpSpan.getInstance())
263297

264298
rule.runOnUiThread { Sentry.close() }
265299
rule.setContent {
266-
val currentStep = step
267-
key(currentStep) {
268-
SentryTraced(tag = "late-transaction-$currentStep") { Box(Modifier.size(1.dp)) }
300+
ProvideSentrySpan(sentrySpan) {
301+
val currentStep = step
302+
key(currentStep) {
303+
SentryTraced(tag = "late-transaction-$currentStep") { Box(Modifier.size(1.dp)) }
304+
}
269305
}
270306
}
271307
rule.waitForIdle()
272308

273309
val tx = initSentryAndStartTransaction("tx")
274-
rule.runOnIdle { step = 1 }
310+
rule.runOnIdle {
311+
sentrySpan = tx
312+
step = 1
313+
}
275314
rule.waitForIdle()
276315

277316
tx.waitForSpanCount(OP_COMPOSE, 1)
@@ -284,11 +323,14 @@ class SentryTracedTest {
284323
fun `records spans under replacement owner after previous owner finishes`() {
285324
var step by mutableStateOf(0)
286325
val firstTx = initSentryAndStartTransaction("first-tx")
326+
var sentrySpan by mutableStateOf<ISpan>(firstTx)
287327

288328
rule.setContent {
289-
val currentStep = step
290-
key(currentStep) {
291-
SentryTraced(tag = "transaction-$currentStep") { Box(Modifier.size(1.dp)) }
329+
ProvideSentrySpan(sentrySpan) {
330+
val currentStep = step
331+
key(currentStep) {
332+
SentryTraced(tag = "transaction-$currentStep") { Box(Modifier.size(1.dp)) }
333+
}
292334
}
293335
}
294336
firstTx.waitForSpanCount(OP_COMPOSE, 1)
@@ -298,7 +340,10 @@ class SentryTracedTest {
298340
firstTx.finish()
299341
secondTx = startBoundTransaction("second-tx")
300342
}
301-
rule.runOnIdle { step = 1 }
343+
rule.runOnIdle {
344+
sentrySpan = secondTx
345+
step = 1
346+
}
302347
rule.waitForIdle()
303348

304349
secondTx.waitForSpanCount(OP_COMPOSE, 1)
@@ -310,13 +355,12 @@ class SentryTracedTest {
310355

311356
@Test
312357
fun `records a new span group when the owner span changes for the same composable node`() {
313-
var step by mutableStateOf(0)
314358
val firstTx = initSentryAndStartTransaction("first-tx")
359+
var sentrySpan by mutableStateOf<ISpan>(firstTx)
315360

316361
rule.setContent {
317-
val currentStep = step
318-
SentryTraced(tag = "transaction", modifier = Modifier.testTag("content-$currentStep")) {
319-
Box(Modifier.size((currentStep + 1).dp))
362+
ProvideSentrySpan(sentrySpan) {
363+
SentryTraced(tag = "transaction") { Box(Modifier.size(1.dp)) }
320364
}
321365
}
322366
firstTx.waitForSpanCount(OP_COMPOSE, 1)
@@ -328,14 +372,13 @@ class SentryTracedTest {
328372
firstTx.finish()
329373
secondTx = startBoundTransaction("second-tx")
330374
}
331-
rule.runOnIdle { step = 1 }
375+
rule.runOnIdle { sentrySpan = secondTx }
332376
rule.waitForIdle()
333377

334378
secondTx.waitForSpanCount(OP_COMPOSE, 1)
335379
drawContent()
336380
secondTx.waitForSpanCount(OP_RENDER, 1)
337381

338-
rule.onNodeWithTag("content-1").assertExists()
339382
assertThat(firstTx.countSpans(OP_PARENT_COMPOSITION)).isEqualTo(1)
340383
assertThat(firstTx.countSpans(OP_PARENT_RENDER)).isEqualTo(1)
341384
assertThat(secondTx.countSpans(OP_PARENT_COMPOSITION)).isEqualTo(1)
@@ -349,7 +392,9 @@ class SentryTracedTest {
349392
val tx = initSentryAndStartTransaction("tx")
350393

351394
assertFailsWith<IllegalStateException> {
352-
rule.setContent { SentryTraced(tag = "throws") { error("boom") } }
395+
rule.setContent {
396+
ProvideSentrySpan(tx) { SentryTraced(tag = "throws") { error("boom") } }
397+
}
353398
}
354399

355400
assertThat(tx.countSpans(OP_PARENT_COMPOSITION)).isEqualTo(0)

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL