Skip to content

fix(compose): Stop SentryTraced from reusing stale parent spans - #6057

Open
0xadam-brown wants to merge 3 commits into
mainfrom
fix/sentry-traced-stale-parent
Open

fix(compose): Stop SentryTraced from reusing stale parent spans#6057
0xadam-brown wants to merge 3 commits into
mainfrom
fix/sentry-traced-stale-parent

Conversation

@0xadam-brown

@0xadam-brown 0xadam-brown commented Sep 5, 2026

Copy link
Copy Markdown
Member

📜 Description

PR fixes the stale parent problem that causes spans to be dropped from all SentryTraced instances for the entire app process after the initial active transaction finishes.

Prior to this PR, SentryTraced used process-wide composition locals to bind the transaction active whenever the first SentryTraced for an app process entered the composition, and to reuse it for all SentryTraced instances thereafter. That meant all SentryTraced spans for the entire app would be dropped for the lifetime of the app process once the initial transaction finished.

This PR sets things right by having each SentryTraced composable request the current active transaction and update the generation of spans accordingly.

Because we're no longer relying on a single transaction + parent span pair, SentryTraced now needs to manage the creation of possibly multiple parent span pairs, as the owning transaction updates. That logic lives in the new ParentSpans class.

💡 Motivation and Context

(See above.)

Measuring performance

Performance of SentryTraced is essentially unchanged, if not slightly better with the introduction of this PR. Expand below to see the details.

Details

I had my clanker run 8 time trials, comparing main against this PR.

Key Timing Results

Suite Metric Main PR Notes
Standard 8-run Stock flow ui.compose child count 21 28 PR still attaches the extra ComposeActivity child set in the stock multi-screen flow
Reduced-noise 8-run Stock flow ui.compose child count 21 28 Same semantic difference remains under reduced-noise conditions
Standard 8-run ComposeActivity buttons_page ui.compose avg ms 41.406 34.949 PR lower
Standard 8-run ComposeActivity button_nav_github ui.compose avg ms 20.250 14.905 PR lower
Standard 8-run ComposeActivity button_dialog#2 ui.compose avg ms 11.465 11.358 Essentially flat
Reduced-noise 8-run ComposeActivity buttons_page ui.compose avg ms 29.628 25.104 PR lower
Reduced-noise 8-run ComposeActivity button_nav_github ui.compose avg ms 14.878 12.046 PR lower
Reduced-noise 8-run ComposeActivity button_dialog#2 ui.compose avg ms 7.694 7.369 Essentially flat
Reduced-noise 8-run ComposeActivity buttons_page ui.render avg ms 4.617 4.644 Flat
Reduced-noise 8-run ComposeActivity navhost ui.render avg ms 4.753 4.872 Flat

Executive Summary

Metric Main PR Takeaway
Stock flow ui.compose child count 21 28 PR still records the extra ComposeActivity child spans in the stock flow
Reduced-noise buttons_page ui.compose avg ms 29.628 25.104 No slowdown here; PR is slightly lower
Reduced-noise button_nav_github ui.compose avg ms 14.878 12.046 No slowdown here; PR is slightly lower

💚 How did you test it?

New unit tests + I had my clanker run manual tests for both correctness and performance against the Compose and Nav2 sample apps.

📝 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

Fix the stale parent problem that causes spans from all SentryTraced instances for the entire app process to be dropped once the initial active transaction finishes.

Prior to this commit, SentryTraced used process-wide composition locals to bind the transaction active whenever the first SentryTraced for an app process entered the composition, and to reuse it for all SentryTraced instances thereafter. That meant all SentryTraced spans for the entire app would be dropped for the lifetime of the app process once the initial transaction finished. Oof.

This commit sets things right by having each SentryTraced composable request the current active transaction and update the generation of spans accordingly.

Because we're no longer relying on a single transaction + parent span pair, SentryTraced now needs to manage the creation of possibly multiple parent span pairs, as the owning transaction updates. That logic lives in the new ParentSpans class.
@0xadam-brown
0xadam-brown force-pushed the fix/sentry-traced-stale-parent branch from 302c95b to e508334 Compare September 5, 2026 08:38
getByName("androidUnitTest") {
dependencies {
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.foundation.layout)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fyi, new deps are test-only.

foundation and foundation-layout are needed to keep the Robolectric Compose test runtime from picking up navigation-compose's obsolete transitive foundation artifacts, which led to a BoxKt.maybeCachedBoxMeasurePolicy(...) linkage failure (given the Box used by SentryTraced).

@sentry

sentry Bot commented Sep 5, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.55.0 (1) release

⚙️ sentry-android Build Distribution Settings

Comment thread sentry-compose/src/androidUnitTest/kotlin/io/sentry/compose/SentryTracedTest.kt Outdated

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

private val localSentryCompositionParentSpan = compositionLocalOf {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

These composition locals were the cause of the stale parent bug.

The lambda here is executed when localSentryCompositionParentSpan.current is first read (ie, by the first SentryTraced in the app process to be invoked). CompositionLocal then caches that default value and returns it every time .current is called. Once the original transaction returned by getRootSpan() expired, all remaining spans generated by SentryTraced through the lifetime of the app process were dropped.

@0xadam-brown
0xadam-brown marked this pull request as ready for review September 5, 2026 08:46

@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.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e508334. Configure here.

0xadam-brown and others added 2 commits September 5, 2026 13:41
Pass the captured start timestamp through the timestamp-aware span overload when creating shared SentryTraced parent spans. This keeps parent spans from starting after their child composition or render spans.

Add regression coverage for parent span start ordering.

Co-Authored-By: OpenAI GPT-5.5 <noreply@openai.com>
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.

1 participant