feat(extend-app-start): [3/4] Eagerly create the extended app start transaction#5608
Conversation
…s wiring Introduces the @ApiStatus.Internal IAppStartExtender contract (extendAppStart / finishAppStart / getExtendedAppStartSpan) and a NoOp default, wired into SentryOptions. This is the naming-stable core bridge for the app start extension API; it is inert (returns NoOpSpan / no-ops) until the Android implementation and public Sentry facade land later in the stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| db5be2f | 311.84 ms | 365.94 ms | 54.10 ms |
| 388ffc5 | 315.18 ms | 362.43 ms | 47.25 ms |
| d8041d7 | 305.32 ms | 370.59 ms | 65.27 ms |
| ca0c0cfc14d958d067e640e132d8de863dfe8eb1 | 318.45 ms | 370.06 ms | 51.61 ms |
| 201a6fd | 326.00 ms | 371.92 ms | 45.92 ms |
| b76203f | 336.02 ms | 403.42 ms | 67.40 ms |
| ba0608b | 319.63 ms | 373.04 ms | 53.41 ms |
| 682e090 | 347.33 ms | 427.28 ms | 79.95 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| db5be2f | 0 B | 0 B | 0 B |
| 388ffc5 | 0 B | 0 B | 0 B |
| d8041d7 | 0 B | 0 B | 0 B |
| ca0c0cfc14d958d067e640e132d8de863dfe8eb1 | 0 B | 0 B | 0 B |
| 201a6fd | 0 B | 0 B | 0 B |
| b76203f | 0 B | 0 B | 0 B |
| ba0608b | 0 B | 0 B | 0 B |
| 682e090 | 0 B | 0 B | 0 B |
Previous results on branch: feat/app-start-extension-materialize
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 69caaee | 313.73 ms | 373.92 ms | 60.19 ms |
| e5800ed | 318.21 ms | 375.48 ms | 57.27 ms |
| ae82ca0 | 381.62 ms | 426.12 ms | 44.51 ms |
| a037262 | 318.06 ms | 364.59 ms | 46.52 ms |
| 511890d | 315.38 ms | 376.00 ms | 60.62 ms |
| 20bbe9a | 305.48 ms | 387.25 ms | 81.77 ms |
| 4451634 | 321.21 ms | 376.21 ms | 55.00 ms |
| 3a27919 | 371.17 ms | 447.31 ms | 76.14 ms |
| 4cf89ba | 340.51 ms | 416.92 ms | 76.41 ms |
| 44c06ea9f76963a0e6034503bb612c43aca71502 | 324.19 ms | 384.88 ms | 60.69 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 69caaee | 0 B | 0 B | 0 B |
| e5800ed | 0 B | 0 B | 0 B |
| ae82ca0 | 0 B | 0 B | 0 B |
| a037262 | 0 B | 0 B | 0 B |
| 511890d | 0 B | 0 B | 0 B |
| 20bbe9a | 0 B | 0 B | 0 B |
| 4451634 | 0 B | 0 B | 0 B |
| 3a27919 | 0 B | 0 B | 0 B |
| 4cf89ba | 0 B | 0 B | 0 B |
| 44c06ea9f76963a0e6034503bb612c43aca71502 | 0 B | 0 B | 0 B |
…ix test name typo Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
…pStart Mirrors sentry-cocoa's finishExtendedAppLaunch() and makes the API name explicit about finishing the *extended* app start. Renames IAppStartExtender.finishAppStart() and the NoOpAppStartExtender implementation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ndroid extender Replaces the AppStartMetrics IAppStartExtender implementation and the deferred ExtendedAppStartSpan with a focused, lock-guarded AppStartExtension that owns the eager App Start transaction and extended span. AppStartMetrics now only holds the component and exposes isAppStartWindowOpen(). Inert until 3/4 registers the listener. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ead of static scope lookup Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… instead of a callback The listener now returns the created transaction+span (or null to decline) rather than calling back into onExtended() while extendAppStart() holds the lock. This removes the re-entrant lock acquisition and the A->B->A round trip, collapsing two public methods into one linear flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o clear Drop comments that restate code or annotate omissions (region markers, getter javadoc, the reset call-site comment, the ExtendedAppStart/isActive docs). Rename AppStartExtension.reset() to clear() to match its owner AppStartMetrics.clear(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ting it The extension logs only two warnings, both on rare guard paths in extendAppStart(). Inline Sentry.getCurrentScopes().getOptions().getLogger() at those sites instead of holding a logger field set at init, dropping the field, setLogger(), and the AndroidOptionsInitializer wiring. extendAppStart() runs post-init, so the lookup always yields the configured logger. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tartExtension Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mments Drop finishTransaction's javadoc (trivial body; it described caller context and waitForChildren behavior configured elsewhere) and reduce getExtendedEndTime's javadoc to a single inline note on the only non-obvious branch (deadline suppression). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o finishExtendedAppStart Implements the renamed IAppStartExtender.finishExtendedAppStart(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xtension-materialize
The test names describe the scenarios; the inline comments restated them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move these component accessors into the component-extraction PR (they were introduced later in the wiring PR). Keeps the full AppStartExtension surface and its unit tests together here; the wiring PR only consumes them. Inert on its own. Regenerated apiDump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xtension-materialize
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It is written without a lock in onAppStartSpansSent() and read across threads in canExtendAppStart() (via extendAppStart()), with no happens-before edge. volatile guarantees visibility, matching the AtomicInteger/AtomicBoolean siblings in the same check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xtension-materialize
…transaction Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… inactive Mirrors Sentry.getSpan(): the extender reports null when there is no active extended span instead of a NoOpSpan, so callers can tell there is no span running. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dAppStartSpan when inactive Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xtension-materialize
…dAppStartSpan is nullable Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The get-then-clear of the app start sampling decision in onExtendAppStartRequested looks redundant without context. Document that the decision is pre-rolled on the previous run, forces the eager app.start transaction's sampling and profiler binding, and must be cleared so the first ui.load can't also claim it. Co-Authored-By: Claude <noreply@anthropic.com>
…ager extension finishes The eager extendAppStart path persists trace headers so a later ui.load can continue the app.start trace, but never recorded the app start end time - that was only set on the headless path. If the extended transaction finished (user finish or deadline) before any activity, the first ui.load treated the continuation window as unbounded and joined the completed app-start trace no matter how much later it started. Persist the transaction's finish date via the transaction finished callback, which covers every finish path (finishExtendedAppStart, first frame, deadline), so the existing continuation window check bounds the extend path the same way it bounds the headless one. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9196ec9. Configure here.
romtsn
left a comment
There was a problem hiding this comment.
LGTM, but we really need to refactor this thing 🙈 it's just too many if-elses in here. If you got time could you task Fable to come up with nice a refactor plan maybe and create an issue out of that?
yeah :/ so many edge cases with all the different combinations, prolly some of the complexity will be gone as well once we make standalone the only way to go.
👍 |
…ension is active A first activity arriving more than a minute after launch resets the app start span to the activity create time and flips the type to warm. While an app start extension is active this corrupts the extended vital: the eager app.start transaction stays anchored at process start while the measurement subtracts the reset span start, and a cold launch gets reported under the warm key. An active extension is the user explicitly saying the launch is still in progress, so let it pin the launch: skip the reclassification while the extension is active and apply the heuristic as before once it has finished. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/app-start-extension-materialize # Conflicts: # sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt # sentry/src/main/java/io/sentry/SentryOptions.java # sentry/src/test/java/io/sentry/SentryOptionsTest.kt

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.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.finishExtendedAppStart(). 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 untilfinishExtendedAppStart(); 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.finishExtendedAppStart()/Sentry.getExtendedAppStartSpan()facade.#skip-changelog