fix(compose): allow overriding the IScopes used by SentryTraced via LocalSentryScopes#5838
fix(compose): allow overriding the IScopes used by SentryTraced via LocalSentryScopes#5838Tabishahmad wants to merge 3 commits into
Conversation
…ocalSentryScopes SDKs that report their own telemetry through a separate IScopes/Hub instance (distinct from the host app's Sentry setup) previously had no way to make SentryTraced trace against that instance, since it always read from the global Sentry.getCurrentScopes(). LocalSentryScopes can now be overridden via CompositionLocalProvider to scope tracing to a specific IScopes, while sibling SentryTraced calls under the same scopes still share one root composition/render span. Fixes getsentry#2668
| val parentCompositionSpan = | ||
| rootSpans.compositionSpans.getOrPut(scopes) { createCompositionParentSpan(scopes) } | ||
| val parentRenderingSpan = | ||
| rootSpans.renderingSpans.getOrPut(scopes) { createRenderingParentSpan(scopes) } |
There was a problem hiding this comment.
Null root spans cached forever
Medium Severity
getOrPut caches an ImmutableHolder even when getRootSpan returns null, so the first SentryTraced call under an IScopes with no active transaction permanently disables compose root spans for that scopes instance. Later transactions on the same scopes never create ui.compose.composition / ui.compose.rendering parents, which is likely for LocalSentryScopes providers placed at the composition root before tracing starts.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d1e90a3. Configure here.
| private class RootSpans { | ||
| val compositionSpans = HashMap<IScopes, ImmutableHolder<ISpan?>>() | ||
| val renderingSpans = HashMap<IScopes, ImmutableHolder<ISpan?>>() | ||
| } | ||
|
|
||
| // Cached once per Composition and shared by every SentryTraced call within it, mirroring the | ||
| // old eagerly-computed `compositionLocalOf { ... }` default (which Compose resolves once and | ||
| // reuses for every `.current` read that has no ancestor Provider, sibling or not). Keyed per | ||
| // IScopes so distinct custom scopes each get their own root span instead of colliding. | ||
| private val LocalRootSpans = staticCompositionLocalOf { RootSpans() } |
There was a problem hiding this comment.
Bug: The RootSpans singleton holds strong references to IScopes instances in global HashMaps. Since there's no cleanup mechanism, this causes a memory leak as compositions are destroyed.
Severity: HIGH
Suggested Fix
Use a DisposableEffect or remember { onDispose { ... } } within the SentryTraced composable. When the effect is disposed, remove the current IScopes instance from the RootSpans's compositionSpans and renderingSpans maps. This will break the strong reference and allow the IScopes and ISpan objects to be garbage collected when their composition is no longer active.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location:
sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeTracing.kt#L69-L78
Potential issue: The `RootSpans` instance, created via `staticCompositionLocalOf`, acts
as a global singleton. Its `compositionSpans` and `renderingSpans` HashMaps store strong
references to every unique `IScopes` instance used with `SentryTraced`. When a
composition is disposed, the `IScopes` instance is not removed from these global maps.
This prevents the `IScopes` instance and its associated `ISpan` from being garbage
collected, resulting in a memory leak that grows throughout the application's lifetime.
This is especially problematic when new `IScopes` instances are created for different
screens or sessions.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5a43f30. Configure here.
| val parentCompositionSpan = | ||
| rootSpans.compositionSpans.getOrPut(scopes) { createCompositionParentSpan(scopes) } | ||
| val parentRenderingSpan = | ||
| rootSpans.renderingSpans.getOrPut(scopes) { createRenderingParentSpan(scopes) } |
There was a problem hiding this comment.
Root span cache never evicts
Medium Severity
LocalRootSpans defaults to a process-wide RootSpans whose HashMaps strongly retain every IScopes and its parent spans. Entries are never removed, so short-lived custom scopes (the new override use case) pin finished transactions and span trees for the app lifetime.
Reviewed by Cursor Bugbot for commit 5a43f30. Configure here.


SDKs that report their own telemetry through a separate IScopes/Hub instance (distinct from the host app's Sentry setup) previously had no way to make SentryTraced trace against that instance, since it always read from the global Sentry.getCurrentScopes(). LocalSentryScopes can now be overridden via CompositionLocalProvider to scope tracing to a specific IScopes, while sibling SentryTraced calls under the same scopes still share one root composition/render span.
📜 Description
Adds a public
LocalSentryScopesCompositionLocal tosentry-compose, defaulting toSentry.getCurrentScopes()and overridable viaCompositionLocalProvider(LocalSentryScopes provides myScopes) { ... }.SentryTracednow resolves the currentIScopesfromLocalSentryScopesinstead of always reading the global default. The root "Initial Composition" / "Initial Render" spans are cached perIScopesin a shared holder so that siblingSentryTracedcalls under the same scopes still share one root span (matching current behavior), while calls under different scopes each get their own independent root span.💡 Motivation and Context
Enables SDKs/libraries that report their own telemetry through a separate
IScopes/Hubinstance to useSentryTracedagainst that instance, independently of the host app's own Sentry setup. Direction confirmed with @markushi and @romtsn on the issue.💚 How did you test it?
Added
SentryTracedTestcovering:IScopesprovided viaLocalSentryScopesSentryTracedcalls under the same scopes share one root spanSentryTracedcalls under different scopes don't interfere with each otherExisting
ComposeIntegrationTestsstill pass.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
Update public API docs for
sentry-composeonce this lands, to documentLocalSentryScopes.