Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

### Fixes

- Prevent `SentryTraced` from producing dangling spans if recomposition is abandoned or drawing fails ([#6049](https://github.com/getsentry/sentry-java/pull/6049))
- 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))
- 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))
- `globalHubMode` is enabled by default on Android, where tags, extras, contexts and level set inside the callback were silently dropped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import io.sentry.ISpan
import io.sentry.Sentry
import io.sentry.SentryDate
import io.sentry.SpanOptions
import io.sentry.compose.SentryModifier.sentryTag

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

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

@Immutable private class ImmutableHolder<T>(var item: T)

private fun getRootSpan(): ISpan? {
var rootSpan: ISpan? = null
Sentry.configureScope { rootSpan = it.transaction }
return rootSpan
}

private val localSentryCompositionParentSpan = compositionLocalOf {
ImmutableHolder(
getRootSpan()
Expand Down Expand Up @@ -62,6 +56,15 @@ private val localSentryRenderingParentSpan = compositionLocalOf {
)
}

@Immutable internal class ImmutableHolder<T>(var item: T)
Comment thread
0xadam-brown marked this conversation as resolved.

/**
* Creates spans for tracking the time required to compose the wrapped [content], and a span for its
* initial draw.
*
* Spans are approximate and include work performed by any composables [content] invokes. Abandoned
* recompositions are ignored.
*/
@ExperimentalComposeUiApi
@Composable
public fun SentryTraced(
Expand All @@ -70,32 +73,74 @@ public fun SentryTraced(
enableUserInteractionTracing: Boolean = true,
content: @Composable BoxScope.() -> Unit,
) {
val parentCompositionSpan = localSentryCompositionParentSpan.current
val parentRenderingSpan = localSentryRenderingParentSpan.current
val compositionSpan =
parentCompositionSpan.item?.startChild(OP_COMPOSE, tag)?.apply {
spanContext.origin = OP_TRACE_ORIGIN
}
val firstRendered = remember { ImmutableHolder(false) }

val alreadyRendered = remember { ImmutableHolder(false) }
val baseModifier = if (enableUserInteractionTracing) modifier.sentryTag(tag) else modifier

val parentCompositionSpan = localSentryCompositionParentSpan.current.item
val parentRenderingSpan = localSentryRenderingParentSpan.current.item
val dateProvider = Sentry.getCurrentScopes().options.dateProvider

// Only record spans if we have a parent for them.
val compositionStart = parentCompositionSpan?.let { dateProvider.now() }

Box(
modifier =
baseModifier.drawWithContent {
val renderSpan =
if (!firstRendered.item) {
parentRenderingSpan.item?.startChild(OP_RENDER, tag)
} else {
null
}
drawContent()
firstRendered.item = true
renderSpan?.finish()
if (alreadyRendered.item || parentRenderingSpan == null) {
drawContent()
} else {
val renderStart = dateProvider.now()
drawContent()
val renderEnd = dateProvider.now()

alreadyRendered.item = true
recordRenderSpan(parentRenderingSpan, tag, renderStart, renderEnd)
}
},
propagateMinConstraints = true,
) {
content()
}
compositionSpan?.finish()

if (compositionStart != null) {
val compositionEnd = dateProvider.now()

SideEffect {
recordCompositionSpan(
parentSpan = parentCompositionSpan,
tag = tag,
startTimestamp = compositionStart,
endTimestamp = compositionEnd,
)
}
}
}

private fun getRootSpan(): ISpan? {
var rootSpan: ISpan? = null
Sentry.configureScope { rootSpan = it.transaction }
return rootSpan
}

private fun recordCompositionSpan(
parentSpan: ISpan?,
tag: String,
startTimestamp: SentryDate,
endTimestamp: SentryDate,
) {
parentSpan?.startChild(OP_COMPOSE, tag, startTimestamp)?.apply {
spanContext.origin = OP_TRACE_ORIGIN
finish(null, endTimestamp)
}
}

private fun recordRenderSpan(
parentSpan: ISpan?,
tag: String,
startTimestamp: SentryDate,
endTimestamp: SentryDate,
) {
parentSpan?.startChild(OP_RENDER, tag, startTimestamp)?.apply {
finish(null, endTimestamp)
}
}
Loading