Skip to content

fix(compose): allow overriding the IScopes used by SentryTraced via LocalSentryScopes#5838

Open
Tabishahmad wants to merge 3 commits into
getsentry:mainfrom
Tabishahmad:fix/sentry-traced-custom-scopes
Open

fix(compose): allow overriding the IScopes used by SentryTraced via LocalSentryScopes#5838
Tabishahmad wants to merge 3 commits into
getsentry:mainfrom
Tabishahmad:fix/sentry-traced-custom-scopes

Conversation

@Tabishahmad

Copy link
Copy Markdown

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 LocalSentryScopes CompositionLocal to sentry-compose, defaulting to Sentry.getCurrentScopes() and overridable via CompositionLocalProvider(LocalSentryScopes provides myScopes) { ... }.

SentryTraced now resolves the current IScopes from LocalSentryScopes instead of always reading the global default. The root "Initial Composition" / "Initial Render" spans are cached per IScopes in a shared holder so that sibling SentryTraced calls 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/Hub instance to use SentryTraced against 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 SentryTracedTest covering:

  • spans are created on the IScopes provided via LocalSentryScopes
  • sibling SentryTraced calls under the same scopes share one root span
  • SentryTraced calls under different scopes don't interfere with each other

Existing ComposeIntegrationTests still pass.

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.

🔮 Next steps

Update public API docs for sentry-compose once this lands, to document LocalSentryScopes.

…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
Comment thread CHANGELOG.md Outdated
val parentCompositionSpan =
rootSpans.compositionSpans.getOrPut(scopes) { createCompositionParentSpan(scopes) }
val parentRenderingSpan =
rootSpans.renderingSpans.getOrPut(scopes) { createRenderingParentSpan(scopes) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d1e90a3. Configure here.

Comment on lines +69 to +78
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() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread CHANGELOG.md

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ 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) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5a43f30. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow using a custom Hub with SentryTraced for Jetpack Compose

2 participants