feat(extend-app-start): [3/4] Eagerly create the extended app start transaction#5608
feat(extend-app-start): [3/4] Eagerly create the extended app start transaction#5608buenaflor wants to merge 10 commits into
Conversation
|
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 201a6fd | 326.00 ms | 371.92 ms | 45.92 ms |
| 388ffc5 | 315.18 ms | 362.43 ms | 47.25 ms |
| db5be2f | 311.84 ms | 365.94 ms | 54.10 ms |
| b76203f | 336.02 ms | 403.42 ms | 67.40 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 201a6fd | 0 B | 0 B | 0 B |
| 388ffc5 | 0 B | 0 B | 0 B |
| db5be2f | 0 B | 0 B | 0 B |
| b76203f | 0 B | 0 B | 0 B |
Previous results on branch: feat/app-start-extension-materialize
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 4cf89ba | 340.51 ms | 416.92 ms | 76.41 ms |
| 4451634 | 321.21 ms | 376.21 ms | 55.00 ms |
| 20bbe9a | 305.48 ms | 387.25 ms | 81.77 ms |
| 69caaee | 313.73 ms | 373.92 ms | 60.19 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 4cf89ba | 0 B | 0 B | 0 B |
| 4451634 | 0 B | 0 B | 0 B |
| 20bbe9a | 0 B | 0 B | 0 B |
| 69caaee | 0 B | 0 B | 0 B |
54be119 to
d2b96ad
Compare
0f8ee4d to
5dfb0e1
Compare
5dfb0e1 to
0bd259a
Compare
5dd39af to
3bc1643
Compare
0bd259a to
0b6ada0
Compare
3bc1643 to
45f6c0b
Compare
c3f3627 to
cbaef2f
Compare
…tion (standalone-only) Registers an extend-listener on AppStartExtension that eagerly creates the standalone app.start transaction + extended child span in Application.onCreate, held open via waitForChildren until Sentry.finishAppStart() or the deadline. The first activity continues the eager trace into ui.load (attaching the screen so it stays a foreground app.start) instead of creating a second app.start; headless finishes the eager txn. The app start vital is max(natural, extended) so an early finish never shortens it, and is suppressed on deadline. Standalone-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2d5188e to
01b1dc4
Compare
cbaef2f to
d996425
Compare
…on extendAppStart Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… coverage Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nd trim comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xtension-materialize
…app start path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xtension-materialize
…xtension-materialize
| // time to continue this trace. | ||
| metrics.setAppStartEndTime(endTime); | ||
|
|
||
| transaction.finish(SpanStatus.OK, endTime); |
There was a problem hiding this comment.
Headless path duplicates finished extension
Medium Severity
onHeadlessAppStart only skips creating a new standalone app.start when AppStartExtension.isActive() is true. If Sentry.finishAppStart() completes the eager extended transaction before the main-looper headless idle runs, isActive() is false and a second standalone app.start is created and finished, duplicating headless app-start tracing for one launch.
Reviewed by Cursor Bugbot for commit 66cd87d. Configure here.
| // creating a second one. | ||
| if (metrics.getAppStartExtension().isActive()) { | ||
| metrics.getAppStartExtension().finishTransaction(endTime); | ||
| return; |
There was a problem hiding this comment.
Extended headless omits end time
Low Severity
When headless startup reuses an active extended eager transaction, onHeadlessAppStart calls finishTransaction and returns without calling setAppStartEndTime. The non-extended headless path persists that end time so a later ui.load can use the one-minute continuation window; extended headless always treats the gap as unknown and may continue the trace incorrectly.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 66cd87d. Configure here.
| if (metrics.getAppStartExtension().isActive()) { | ||
| metrics.getAppStartExtension().finishTransaction(endTime); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Bug: An extended headless app start fails to set appStartEndTime, causing all subsequent activities to be incorrectly attached to the initial app start trace.
Severity: MEDIUM
Suggested Fix
After the call to metrics.getAppStartExtension().finishTransaction(endTime) in the extended headless start code path, add a call to metrics.setAppStartEndTime(endTime) to properly close the app start time window.
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-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java#L1029-L1032
Potential issue: In the case of an extended headless app start, the code finishes the
transaction but fails to call `metrics.setAppStartEndTime(endTime)`. Consequently,
`AppStartMetrics.getInstance().getAppStartEndTime()` returns `null`. This causes the
`isWithinAppStartContinuationWindow` check to always return `true`, effectively creating
an infinite time window for trace continuation. As a result, all subsequent activities,
even those occurring much later, are incorrectly attached to the initial app start
trace, leading to inaccurate performance metrics.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Pull request overview
Implements end-to-end wiring for the Android “extend app start” feature by registering an extend listener (standalone-only), eagerly creating/holding open a standalone app.start transaction + extended child span, and updating app start vital calculation logic to account for the extension (including deadline suppression).
Changes:
ActivityLifecycleIntegrationregisters the extend listener when standalone app start tracing is enabled, eagerly creates the standaloneapp.starttransaction +app.start.extended_app_startspan on extension, and continues the trace intoui.loadwhile attaching the first activity screen to the eager transaction.PerformanceAndroidEventProcessorcomputes the app start measurement asmax(natural first-frame duration, extended end)when extended, and suppresses the measurement entirely on deadline while still attaching app start spans.AppStartExtensionaddsisExtended()(processor gate) andsetData(...)(attach screen name to the eager transaction once known), with expanded unit test coverage.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java | Registers extend listener (standalone-only), eagerly creates standalone app.start + extended span, continues trace into ui.load, and finishes the eager txn at first-frame/headless end while waiting for children. |
| sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java | Adjusts app start measurement logic for extended flows (never shorten; suppress on deadline) while still finalizing/attaching app start spans. |
| sentry-android-core/src/main/java/io/sentry/android/core/AppStartExtension.java | Adds setData for eager txn mutation and isExtended for processor gating. |
| sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt | Adds tests covering eager standalone app.start creation, trace continuation into ui.load, screen attachment, wait-for-children behavior, standalone-off no-op, and survival across activity destroy. |
| sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt | Adds tests for extended end driving cold measurement, never reporting shorter than natural duration, and deadline suppression behavior. |
| sentry-android-core/src/test/java/io/sentry/android/core/AppStartExtensionTest.kt | Adds test ensuring isExtended remains true after the transaction finishes. |
| sentry-android-core/api/sentry-android-core.api | API dump updated for new AppStartExtension methods (isExtended, setData). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
….extended Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 571416e. Configure here.
| AppStartMetrics.getInstance() | ||
| .getAppStartExtension() | ||
| .setData(APP_START_SCREEN_DATA, activityName); | ||
| } |
There was a problem hiding this comment.
Extended headless vitals dropped after screen
Medium Severity
Attaching app.vitals.start.screen to the eager extended app.start transaction makes the processor classify it as non-headless, while shouldSendStartMeasurements still requires appLaunchedInForeground. Headless extended starts that later open an activity can skip cold/warm app-start measurements entirely.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 571416e. Configure here.


PR Stack (Extend App Start)
📜 Description
Registers the extend-listener and wires the extension through to the trace and the vital, making the feature work end-to-end. The extension is eager and standalone-only — only the standalone
app.starttransaction is independent of an activity and can be created inApplication.onCreate.ActivityLifecycleIntegrationenableStandaloneAppStartTracingis on (this is what makes the API standalone-only). OnSentry.extendAppStart()the listener eagerly creates the standaloneapp.starttransaction (waitForChildren+ deadline) and itsapp.start.extendedchild, and returns them as anExtendedAppStart.PerformanceAndroidEventProcessormax(natural first-frame duration, extended end), so an early finish never shortens it. On deadline the measurement is suppressed entirely (no inflated ~30s value); spans still attach. Non-extended starts are unchanged.AppStartExtensionisExtended()(the processor's gate) andsetData(...)(to attach the screen once the first activity is known).Notes
appStartTransactionfield, so per-activity cleanup can't cancel it.ui.load(viaisActive()) instead of starting a secondapp.start, and attaches the screen so the foregroundapp.startkeeps itsapp.vitals.start.screen.waitForChildrenholds it open untilSentry.finishAppStart(). Headless finishes it at the headless stop time. A sharedcreateStandaloneAppStartTransaction(...)helper backs both paths.ui.load) extension path are removed — eager creation makes them unnecessary.💡 Motivation and Context
Part of the app start extension API stack (#5553). This is where the extension actually affects the trace and the vital, scoped to standalone app start tracing because only the standalone
app.starttransaction can be created eagerly inonCreate.💚 How did you test it?
Unit tests (TDD):
ActivityLifecycleIntegrationTest— eagerapp.start+ extended child on extend; first activity continues the trace with no secondapp.startand the screen attached; standalone & headless stay open untilfinishAppStart(); standalone-off is a no-op; the eager txn survives activity destroy.PerformanceAndroidEventProcessorTest— extended end drives the cold measurement; an early finish never reports shorter than the first-frame duration; a deadline finish suppresses the measurement.apiCheck, spotless, and the:sentry-android-coreunit suite pass.📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
[4/4]— add the publicSentry.extendAppStart()/Sentry.finishAppStart()/Sentry.getExtendedAppStartSpan()facade.#skip-changelog