| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 8f0dc10 commit 9fd9105
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,10 @@ | |||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased | |
| 4 | 4 | ||
| 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 | + | ||
| 5 | 9 | ### Fixes | |
| 6 | 10 | ||
| 7 | 11 | - 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) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,6 +6,10 @@ public final class io/sentry/compose/BuildConfig { | |||
| 6 | 6 | public fun <init> ()V | |
| 7 | 7 | } | |
| 8 | 8 | ||
| 9 | + public final class io/sentry/compose/LocalSentrySpanKt { | ||
| 10 | + public static final fun getLocalSentrySpan ()Landroidx/compose/runtime/ProvidableCompositionLocal; | ||
| 11 | + } | ||
| 12 | + | ||
| 9 | 13 | public final class io/sentry/compose/SentryComposeHelperKt { | |
| 10 | 14 | public static final fun boundsInWindow (Landroidx/compose/ui/layout/LayoutCoordinates;Landroidx/compose/ui/layout/LayoutCoordinates;)Landroidx/compose/ui/geometry/Rect; | |
| 11 | 15 | } | |
| Original file line number | Diff line number | Diff 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 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,7 +10,6 @@ import androidx.compose.ui.Modifier | |||
| 10 | 10 | import androidx.compose.ui.draw.drawWithContent | |
| 11 | 11 | import io.sentry.ISpan | |
| 12 | 12 | import io.sentry.Instrumenter | |
| 13 | - import io.sentry.NoOpSpan | ||
| 14 | 13 | import io.sentry.Sentry | |
| 15 | 14 | import io.sentry.SentryDate | |
| 16 | 15 | import io.sentry.SpanOptions | |
@@ -71,7 +70,7 @@ public fun SentryTraced( | |||
| 71 | 70 | ) { | |
| 72 | 71 | val baseModifier = if (enableUserInteractionTracing) modifier.sentryTag(tag) else modifier | |
| 73 | 72 | val scopes = Sentry.getCurrentScopes() | |
| 74 | - val ownerSpan = scopes.transaction ?: NoOpSpan.getInstance() | ||
| 73 | + val ownerSpan = LocalSentrySpan.current.orBootstrapCurrentTransaction() | ||
| 75 | 74 | ||
| 76 | 75 | val alreadyComposed = remember(ownerSpan) { MutableRef(false) } | |
| 77 | 76 | val alreadyRendered = remember(ownerSpan) { MutableRef(false) } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -28,6 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 | |||
| 28 | 28 | import com.google.common.truth.Truth.assertThat | |
| 29 | 29 | import io.sentry.ISpan | |
| 30 | 30 | import io.sentry.ITransaction | |
| 31 | + import io.sentry.NoOpSpan | ||
| 31 | 32 | import io.sentry.Sentry | |
| 32 | 33 | import io.sentry.SentryOptions | |
| 33 | 34 | import io.sentry.TransactionOptions | |
@@ -75,7 +76,9 @@ class SentryTracedTest { | |||
| 75 | 76 | fun `records a composition span for the initial composition`() { | |
| 76 | 77 | val tx = initSentryAndStartTransaction("tx") | |
| 77 | 78 | ||
| 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 | + } | ||
| 79 | 82 | tx.waitForSpanCount(OP_COMPOSE, 1) | |
| 80 | 83 | ||
| 81 | 84 | assertThat(tx.countSpans(OP_PARENT_COMPOSITION)).isEqualTo(1) | |
@@ -88,7 +91,7 @@ class SentryTracedTest { | |||
| 88 | 91 | } | |
| 89 | 92 | ||
| 90 | 93 | @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`() { | ||
| 92 | 95 | val tx = initSentryAndStartTransaction("tx") | |
| 93 | 96 | ||
| 94 | 97 | rule.setContent { SentryTraced(tag = "product_info") { Box(Modifier.size(1.dp)) } } | |
@@ -112,12 +115,32 @@ class SentryTracedTest { | |||
| 112 | 115 | } | |
| 113 | 116 | ||
| 114 | 117 | @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`() { | ||
| 116 | 137 | val tx = initSentryAndStartTransaction("tx") | |
| 117 | 138 | rule.runOnUiThread { tx.finish() } | |
| 118 | 139 | ||
| 119 | 140 | 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 | + } | ||
| 121 | 144 | } | |
| 122 | 145 | rule.waitForIdle() | |
| 123 | 146 | ||
@@ -136,7 +159,9 @@ class SentryTracedTest { | |||
| 136 | 159 | } | |
| 137 | 160 | ||
| 138 | 161 | 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 | + } | ||
| 140 | 165 | } | |
| 141 | 166 | rule.waitForIdle() | |
| 142 | 167 | drawContent() | |
@@ -153,9 +178,11 @@ class SentryTracedTest { | |||
| 153 | 178 | val tx = initSentryAndStartTransaction("tx") | |
| 154 | 179 | ||
| 155 | 180 | 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 | + } | ||
| 159 | 186 | } | |
| 160 | 187 | } | |
| 161 | 188 | ||
@@ -170,9 +197,11 @@ class SentryTracedTest { | |||
| 170 | 197 | val tx = initSentryAndStartTransaction("tx") | |
| 171 | 198 | ||
| 172 | 199 | 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 | + } | ||
| 176 | 205 | } | |
| 177 | 206 | } | |
| 178 | 207 | tx.waitForSpanCount(OP_COMPOSE, 2) | |
@@ -200,9 +229,11 @@ class SentryTracedTest { | |||
| 200 | 229 | val tx = initSentryAndStartTransaction("tx") | |
| 201 | 230 | ||
| 202 | 231 | 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 | + } | ||
| 206 | 237 | } | |
| 207 | 238 | } | |
| 208 | 239 | tx.waitForSpanCount(OP_COMPOSE, 1) | |
@@ -230,15 +261,17 @@ class SentryTracedTest { | |||
| 230 | 261 | val tx = initSentryAndStartTransaction("tx") | |
| 231 | 262 | ||
| 232 | 263 | 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")) | ||
| 238 | 274 | } | |
| 239 | - DisposableEffect(Unit) { onDispose { disposeCount++ } } | ||
| 240 | - rememberedState = state | ||
| 241 | - Box(Modifier.size((currentStep + 1).dp).testTag("content-$currentStep")) | ||
| 242 | 275 | } | |
| 243 | 276 | } | |
| 244 | 277 | tx.waitForSpanCount(OP_COMPOSE, 1) | |
@@ -260,18 +293,24 @@ class SentryTracedTest { | |||
| 260 | 293 | @Test | |
| 261 | 294 | fun `starts recording once an owner span becomes available`() { | |
| 262 | 295 | var step by mutableStateOf(0) | |
| 296 | + var sentrySpan by mutableStateOf<ISpan>(NoOpSpan.getInstance()) | ||
| 263 | 297 | ||
| 264 | 298 | rule.runOnUiThread { Sentry.close() } | |
| 265 | 299 | 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 | + } | ||
| 269 | 305 | } | |
| 270 | 306 | } | |
| 271 | 307 | rule.waitForIdle() | |
| 272 | 308 | ||
| 273 | 309 | val tx = initSentryAndStartTransaction("tx") | |
| 274 | - rule.runOnIdle { step = 1 } | ||
| 310 | + rule.runOnIdle { | ||
| 311 | + sentrySpan = tx | ||
| 312 | + step = 1 | ||
| 313 | + } | ||
| 275 | 314 | rule.waitForIdle() | |
| 276 | 315 | ||
| 277 | 316 | tx.waitForSpanCount(OP_COMPOSE, 1) | |
@@ -284,11 +323,14 @@ class SentryTracedTest { | |||
| 284 | 323 | fun `records spans under replacement owner after previous owner finishes`() { | |
| 285 | 324 | var step by mutableStateOf(0) | |
| 286 | 325 | val firstTx = initSentryAndStartTransaction("first-tx") | |
| 326 | + var sentrySpan by mutableStateOf<ISpan>(firstTx) | ||
| 287 | 327 | ||
| 288 | 328 | 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 | + } | ||
| 292 | 334 | } | |
| 293 | 335 | } | |
| 294 | 336 | firstTx.waitForSpanCount(OP_COMPOSE, 1) | |
@@ -298,7 +340,10 @@ class SentryTracedTest { | |||
| 298 | 340 | firstTx.finish() | |
| 299 | 341 | secondTx = startBoundTransaction("second-tx") | |
| 300 | 342 | } | |
| 301 | - rule.runOnIdle { step = 1 } | ||
| 343 | + rule.runOnIdle { | ||
| 344 | + sentrySpan = secondTx | ||
| 345 | + step = 1 | ||
| 346 | + } | ||
| 302 | 347 | rule.waitForIdle() | |
| 303 | 348 | ||
| 304 | 349 | secondTx.waitForSpanCount(OP_COMPOSE, 1) | |
@@ -310,13 +355,12 @@ class SentryTracedTest { | |||
| 310 | 355 | ||
| 311 | 356 | @Test | |
| 312 | 357 | fun `records a new span group when the owner span changes for the same composable node`() { | |
| 313 | - var step by mutableStateOf(0) | ||
| 314 | 358 | val firstTx = initSentryAndStartTransaction("first-tx") | |
| 359 | + var sentrySpan by mutableStateOf<ISpan>(firstTx) | ||
| 315 | 360 | ||
| 316 | 361 | 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)) } | ||
| 320 | 364 | } | |
| 321 | 365 | } | |
| 322 | 366 | firstTx.waitForSpanCount(OP_COMPOSE, 1) | |
@@ -328,14 +372,13 @@ class SentryTracedTest { | |||
| 328 | 372 | firstTx.finish() | |
| 329 | 373 | secondTx = startBoundTransaction("second-tx") | |
| 330 | 374 | } | |
| 331 | - rule.runOnIdle { step = 1 } | ||
| 375 | + rule.runOnIdle { sentrySpan = secondTx } | ||
| 332 | 376 | rule.waitForIdle() | |
| 333 | 377 | ||
| 334 | 378 | secondTx.waitForSpanCount(OP_COMPOSE, 1) | |
| 335 | 379 | drawContent() | |
| 336 | 380 | secondTx.waitForSpanCount(OP_RENDER, 1) | |
| 337 | 381 | ||
| 338 | - rule.onNodeWithTag("content-1").assertExists() | ||
| 339 | 382 | assertThat(firstTx.countSpans(OP_PARENT_COMPOSITION)).isEqualTo(1) | |
| 340 | 383 | assertThat(firstTx.countSpans(OP_PARENT_RENDER)).isEqualTo(1) | |
| 341 | 384 | assertThat(secondTx.countSpans(OP_PARENT_COMPOSITION)).isEqualTo(1) | |
@@ -349,7 +392,9 @@ class SentryTracedTest { | |||
| 349 | 392 | val tx = initSentryAndStartTransaction("tx") | |
| 350 | 393 | ||
| 351 | 394 | assertFailsWith<IllegalStateException> { | |
| 352 | - rule.setContent { SentryTraced(tag = "throws") { error("boom") } } | ||
| 395 | + rule.setContent { | ||
| 396 | + ProvideSentrySpan(tx) { SentryTraced(tag = "throws") { error("boom") } } | ||
| 397 | + } | ||
| 353 | 398 | } | |
| 354 | 399 | ||
| 355 | 400 | assertThat(tx.countSpans(OP_PARENT_COMPOSITION)).isEqualTo(0) | |
| Back | FazBrowse Home | New Git URL |
0 commit comments