fix(tracing): Recover app start data when first navigation transaction is discarded#5833
Merged
fix(tracing): Recover app start data when first navigation transaction is discarded#5833
Conversation
…n is discarded When the first navigation idle transaction is discarded at spanEnd (e.g., by ignoreEmptyRouteChangeTransactions because the navigation container was registered too late), firstStartedActiveRootSpanId remained permanently locked to the dead span. Since no transaction event was ever created for it, processEvent never ran, and all subsequent transactions failed the span ID check — silently losing app start data for the entire session. Add a spanEnd listener that resets the lock when the first root span is unsampled at end time. Also set appStartEndData timestamp before awaiting fetchNativeFrames to close a secondary timing race, and improve failure recovery in attachAppStartToTransactionEvent. Fixes #5831 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Contributor
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
🤖 This preview updates automatically when you update the PR. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Unsubscribe spanEnd listener after it fires to prevent listener leak - Add _updateAppStartEndFrames helper instead of direct module-level mutation - Make all processEvent failure paths set appStartDataFlushed = true (these conditions won't change within the same app start, retrying is wasteful) - Add comment about _sampled internal coupling in test - Fix unnecessary type assertions (lint) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ead of spanEnd The spanEnd listener approach had a listener ordering bug: the app start listener registered at spanStart time runs BEFORE the navigation discard listeners (ignoreEmptyRouteChangeTransactions, ignoreEmptyBackNavigation) that set _sampled = false. So our check always saw the span as sampled. Instead, store a reference to the locked root span and check its sampled status lazily at the next spanStart. By then, the old span has fully completed and _sampled has been set to false by the discard listeners. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Contributor
Author
|
@cursor review |
Contributor
Author
|
@sentry review |
Collaborator
|
Unrelated to the PR: The question lies on the following topics:
|
lucas-zimerman
approved these changes
Mar 18, 2026
Collaborator
lucas-zimerman
left a comment
There was a problem hiding this comment.
The fix looks good!
Contributor
Android (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 4a17c8f+dirty | 406.62 ms | 400.58 ms | -6.04 ms |
| df1f7df+dirty | 442.64 ms | 427.16 ms | -15.48 ms |
| a483f9f+dirty | 396.82 ms | 453.28 ms | 56.46 ms |
| 60cd796+dirty | 445.84 ms | 492.45 ms | 46.61 ms |
| 5c16cdc+dirty | 423.48 ms | 452.35 ms | 28.88 ms |
| 80e4616+dirty | 411.58 ms | 462.12 ms | 50.54 ms |
| 55b77fc+dirty | 411.87 ms | 417.16 ms | 5.29 ms |
| bca62c0+dirty | 414.36 ms | 451.06 ms | 36.70 ms |
| 0b64753+dirty | 448.67 ms | 474.61 ms | 25.94 ms |
| 4e6d7d7+dirty | 480.73 ms | 515.73 ms | 35.00 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 4a17c8f+dirty | 43.75 MiB | 47.99 MiB | 4.24 MiB |
| df1f7df+dirty | 43.75 MiB | 48.08 MiB | 4.33 MiB |
| a483f9f+dirty | 43.75 MiB | 48.41 MiB | 4.66 MiB |
| 60cd796+dirty | 43.75 MiB | 48.07 MiB | 4.32 MiB |
| 5c16cdc+dirty | 17.75 MiB | 19.68 MiB | 1.94 MiB |
| 80e4616+dirty | 43.75 MiB | 48.55 MiB | 4.80 MiB |
| 55b77fc+dirty | 43.75 MiB | 47.99 MiB | 4.24 MiB |
| bca62c0+dirty | 43.75 MiB | 48.41 MiB | 4.66 MiB |
| 0b64753+dirty | 17.75 MiB | 19.70 MiB | 1.95 MiB |
| 4e6d7d7+dirty | 43.75 MiB | 48.40 MiB | 4.64 MiB |
Contributor
iOS (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ea3e26e+dirty | 1229.13 ms | 1228.46 ms | -0.67 ms |
| 80e4616+dirty | 1221.32 ms | 1225.64 ms | 4.32 ms |
| 818a608+dirty | 1205.76 ms | 1208.00 ms | 2.24 ms |
| 77061ed+dirty | 1233.16 ms | 1234.88 ms | 1.71 ms |
| bef3709+dirty | 1222.07 ms | 1220.24 ms | -1.83 ms |
| a206511+dirty | 1185.00 ms | 1186.35 ms | 1.35 ms |
| 74979ac+dirty | 1210.49 ms | 1213.31 ms | 2.82 ms |
| a2bb688+dirty | 1223.53 ms | 1232.90 ms | 9.37 ms |
| 8a868fe+dirty | 1221.50 ms | 1230.78 ms | 9.28 ms |
| d590428+dirty | 1211.77 ms | 1220.51 ms | 8.75 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ea3e26e+dirty | 3.41 MiB | 4.58 MiB | 1.17 MiB |
| 80e4616+dirty | 3.38 MiB | 4.60 MiB | 1.22 MiB |
| 818a608+dirty | 2.63 MiB | 3.91 MiB | 1.28 MiB |
| 77061ed+dirty | 2.63 MiB | 3.98 MiB | 1.34 MiB |
| bef3709+dirty | 3.38 MiB | 4.78 MiB | 1.40 MiB |
| a206511+dirty | 3.41 MiB | 4.67 MiB | 1.25 MiB |
| 74979ac+dirty | 3.38 MiB | 4.60 MiB | 1.22 MiB |
| a2bb688+dirty | 2.63 MiB | 3.99 MiB | 1.36 MiB |
| 8a868fe+dirty | 3.38 MiB | 4.60 MiB | 1.22 MiB |
| d590428+dirty | 3.38 MiB | 4.78 MiB | 1.39 MiB |
Contributor
Android (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 70250df+dirty | 418.08 ms | 480.84 ms | 62.76 ms |
| 8d89cc9+dirty | 357.69 ms | 415.79 ms | 58.10 ms |
| 1853710+dirty | 360.67 ms | 396.28 ms | 35.61 ms |
| 55b77fc+dirty | 410.46 ms | 414.11 ms | 3.65 ms |
| 69602ce+dirty | 375.37 ms | 405.28 ms | 29.91 ms |
| c1573b3+dirty | 355.65 ms | 448.82 ms | 93.17 ms |
| 90afdd3+dirty | 367.79 ms | 404.84 ms | 37.05 ms |
| 955f2eb+dirty | 388.13 ms | 433.56 ms | 45.44 ms |
| 80e4616+dirty | 427.31 ms | 461.15 ms | 33.84 ms |
| 276d348+dirty | 356.30 ms | 405.27 ms | 48.97 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 70250df+dirty | 43.94 MiB | 48.91 MiB | 4.97 MiB |
| 8d89cc9+dirty | 7.15 MiB | 8.41 MiB | 1.26 MiB |
| 1853710+dirty | 7.15 MiB | 8.41 MiB | 1.26 MiB |
| 55b77fc+dirty | 43.94 MiB | 48.82 MiB | 4.88 MiB |
| 69602ce+dirty | 7.15 MiB | 8.41 MiB | 1.26 MiB |
| c1573b3+dirty | 7.15 MiB | 8.42 MiB | 1.27 MiB |
| 90afdd3+dirty | 7.15 MiB | 8.43 MiB | 1.28 MiB |
| 955f2eb+dirty | 7.15 MiB | 8.42 MiB | 1.27 MiB |
| 80e4616+dirty | 43.94 MiB | 49.38 MiB | 5.44 MiB |
| 276d348+dirty | 7.15 MiB | 8.42 MiB | 1.26 MiB |
Contributor
iOS (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ea3e26e+dirty | 1216.61 ms | 1214.15 ms | -2.47 ms |
| 80e4616+dirty | 1206.90 ms | 1205.94 ms | -0.96 ms |
| 818a608+dirty | 1218.84 ms | 1223.18 ms | 4.34 ms |
| 77061ed+dirty | 1210.77 ms | 1218.45 ms | 7.68 ms |
| bef3709+dirty | 1217.79 ms | 1225.33 ms | 7.54 ms |
| a206511+dirty | 1225.02 ms | 1223.74 ms | -1.28 ms |
| 74979ac+dirty | 1212.33 ms | 1212.54 ms | 0.21 ms |
| a2bb688+dirty | 1244.82 ms | 1238.60 ms | -6.22 ms |
| 8a868fe+dirty | 1206.85 ms | 1215.04 ms | 8.19 ms |
| d590428+dirty | 1221.23 ms | 1225.27 ms | 4.03 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ea3e26e+dirty | 3.41 MiB | 4.58 MiB | 1.17 MiB |
| 80e4616+dirty | 3.38 MiB | 4.60 MiB | 1.22 MiB |
| 818a608+dirty | 3.19 MiB | 4.48 MiB | 1.29 MiB |
| 77061ed+dirty | 3.19 MiB | 4.54 MiB | 1.36 MiB |
| bef3709+dirty | 3.38 MiB | 4.78 MiB | 1.40 MiB |
| a206511+dirty | 3.41 MiB | 4.67 MiB | 1.25 MiB |
| 74979ac+dirty | 3.38 MiB | 4.60 MiB | 1.22 MiB |
| a2bb688+dirty | 3.19 MiB | 4.56 MiB | 1.37 MiB |
| 8a868fe+dirty | 3.38 MiB | 4.60 MiB | 1.22 MiB |
| d590428+dirty | 3.38 MiB | 4.78 MiB | 1.39 MiB |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📢 Type of change
📜 Description
When the first navigation idle transaction is discarded at
spanEnd(e.g., byignoreEmptyRouteChangeTransactionsbecause the navigation container was registered after the idle transaction finished),firstStartedActiveRootSpanIdremained permanently locked to the dead span. Since no transaction event was ever created for it,processEventnever ran, and all subsequent transactions failed the span ID check — silently losing app start data for the entire session.Changes
Lazy sampled check at next
spanStart— Store a reference to the locked root span. When the next root span starts andfirstStartedActiveRootSpanIdis already set, check if the locked span was unsampled (e.g., dropped byignoreEmptyRouteChangeTransactionsorignoreEmptyBackNavigation). If so, reset the lock and allow the new span to carry app start data. This check happens atspanStartrather thanspanEndto avoid a listener ordering issue — the discard listeners that set_sampled = falserun after the app start listener in registration order, so checking atspanEndwould always see the span as still sampled.Early
appStartEndDatatimestamp — Set the end timestamp in_captureAppStartbefore awaitingfetchNativeFrames, closing a secondary timing race whereprocessEventcould run before the timestamp was available. Added_updateAppStartEndFrameshelper to update frames after the async fetch without triggering the overwrite warning.Permanent failure handling in
attachAppStartToTransactionEvent— All failure paths after the span ID check now setappStartDataFlushed = trueto prevent wasteful retries, since these conditions (native returns null,has_fetched, missing timestamps, negative/excessive duration) won't change within the same app start. Added debug log to the previously silentappStartDataFlushedearly return.💡 Motivation and Context
Fixes #5831
User logs from the support ticket confirmed the exact root cause. The sequence:
firstStartedActiveRootSpanIdlockedregisterNavigationContainer()is called ~7 seconds later (after the idle transaction finished)ignoreEmptyRouteChangeTransactionsdiscards the empty transaction (_sampled = falseatspanEnd)processEventnever runsfirstStartedActiveRootSpanIdpermanently locked to dead span → all subsequent transactions fail span ID checkThe "intermittent" nature is because the timing of
registerNavigationContainervaries between launches — sometimes it's fast enough (app start works), sometimes it's late (app start lost).💚 How did you test it?
"Attaches app start to next transaction when first root span was dropped"— reproduces the exact reported scenario (first span locked, unsampled after ending, second span detects this at spanStart and succeeds)"Sets appStartDataFlushed when native returns null to prevent wasteful retries"— verifiesfetchNativeAppStartis only called once"Sets appStartDataFlushed when has_fetched is true to prevent wasteful retries""Sets appStartDataFlushed when app start end timestamp is before app start timestamp"📝 Checklist
sendDefaultPIIis enabled🔮 Next steps