feat(extend-app-start): App start extension API#5604
Conversation
|
📲 Install BuildsAndroid
|
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(extend-app-start): Add IAppStartExtender bridge and SentryOptions 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> * chore(extend-app-start): Annotate NoOpAppStartExtender internal and fix test name typo Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(extend-app-start): Rename finishAppStart to finishExtendedAppStart 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> * feat(extend-app-start): Return null from getExtendedAppStartSpan when 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> * test(extend-app-start): Remove NoOpAppStartExtenderTest per PR review Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(extend-app-start): Guard setAppStartExtender against null Match the NoOp-fallback convention used by other SentryOptions setters: accept a @nullable argument and coalesce to NoOpAppStartExtender so the @NotNull field and getter can never be nulled out. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) * feat(extend-app-start): Add IAppStartExtender bridge and SentryOptions 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> * chore(extend-app-start): Annotate NoOpAppStartExtender internal and fix test name typo Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(extend-app-start): Rename finishAppStart to finishExtendedAppStart 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> * feat(extend-app-start): Extract AppStartExtension component for the Android 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> * refactor(extend-app-start): Inject logger into AppStartExtension instead of static scope lookup Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(extend-app-start): Return ExtendedAppStart from the listener 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> * chore(extend-app-start): Remove redundant comments and rename reset to 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> * refactor(extend-app-start): Inline the logger lookup instead of injecting 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> * chore(extend-app-start): Drop the class and listener javadocs on AppStartExtension Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(extend-app-start): Trim finishTransaction/getExtendedEndTime comments 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> * refactor(extend-app-start): Rename AppStartExtension.finishAppStart to finishExtendedAppStart Implements the renamed IAppStartExtender.finishExtendedAppStart(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(extend-app-start): Drop the redundant foreground-check comment on extendAppStart Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop the no-value getAppStartExtension test and trim comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop the no-op finishExtendedAppStart test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Read the extended span finish date, not its finished flag getExtendedEndTime() gated on span.isFinished(), but finishing the extended span completes the waitForChildren transaction and runs the event processor re-entrantly within finishExtendedAppStart(), before the span's finished flag is set. The processor then saw an unfinished span and dropped the app start measurement whenever the extension finished after the first frame. Read getFinishDate() (set before the finish callback) instead, which also keeps the extended end controllable in tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): End the extended transaction at the extended span end When the extended span finished after the timestamp passed to finishTransaction() but before that call ran (e.g. a synchronous extension in a headless start, where finishTransaction runs later at main-thread idle), waitForChildren had nothing left to wait for and the transaction kept the earlier passed timestamp. The extended span then ended after the transaction, and the app start vital exceeded the transaction duration. Finish at max(endTimestamp, extended span finish date) so the span is contained and the duration matches the vital. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Guard extendAppStartListener with the lock The setter wrote the field without synchronization while extendAppStart() reads it under the lock, leaving no happens-before edge. Acquire the same lock in the setter, consistent with the rest of the class's mutable state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Trim verbose finishTransaction comment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Read finish date in getExtendedAppStartSpan Finishing the extended span runs the event processor re-entrantly (via the waitForChildren transaction) before the span's isFinished() flag is set, while the finish timestamp is already in place. Reading isFinished() could therefore hand out a span that is already finishing. Switch to getFinishDate() == null, matching getExtendedEndTime(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Trim duplicated reentrancy comment in getExtendedAppStartSpan Replace the repeated reentrancy explanation with a cross-reference to getExtendedEndTime(), which holds the canonical version. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Keep trimmed reentrancy comment within line length Single-line form fits the 100-char limit so spotless leaves it unwrapped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Reword getExtendedAppStartSpan reentrancy comment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(extend-app-start): Rename isAppStartWindowOpen to canExtendAppStart The predicate has a single consumer — the extend gate in AppStartExtension — so name it for that intent. Also drop the self-evident max-end comment in finishTransaction. Regenerated apiDump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop redundant getExtendedAppStartSpan finished test After getExtendedAppStartSpan switched to getFinishDate(), the "after the span finished" case exercises the same branch as the reentrancy test (finishDate set -> NoOp); the isFinished stub was inert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(extend-app-start): Add setData and isExtended to AppStartExtension 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> * fix(extend-app-start): Make shouldSendStartMeasurements volatile 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> * docs(extend-app-start): Explain why AppStartExtension holds span and transaction Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(extend-app-start): Return null from getExtendedAppStartSpan when 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> * feat(extend-app-start): Return null from AppStartExtension.getExtendedAppStartSpan when inactive Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Use setter method for appStartExtender The @nullable setter and @NotNull getter no longer form a Kotlin mutable property, so use setAppStartExtender(...) instead of the property-assignment syntax (matches setTransportFactory/setReplayController). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ransaction (#5608) * feat(extend-app-start): Add IAppStartExtender bridge and SentryOptions 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> * chore(extend-app-start): Annotate NoOpAppStartExtender internal and fix test name typo Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(extend-app-start): Rename finishAppStart to finishExtendedAppStart 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> * feat(extend-app-start): Extract AppStartExtension component for the Android 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> * refactor(extend-app-start): Inject logger into AppStartExtension instead of static scope lookup Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(extend-app-start): Return ExtendedAppStart from the listener 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> * chore(extend-app-start): Remove redundant comments and rename reset to 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> * refactor(extend-app-start): Inline the logger lookup instead of injecting 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> * chore(extend-app-start): Drop the class and listener javadocs on AppStartExtension Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(extend-app-start): Trim finishTransaction/getExtendedEndTime comments 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> * refactor(extend-app-start): Rename AppStartExtension.finishAppStart to finishExtendedAppStart Implements the renamed IAppStartExtender.finishExtendedAppStart(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(extend-app-start): Eagerly create the extended App Start transaction (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> * chore(extend-app-start): Drop the redundant foreground-check comment on extendAppStart Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Replace the no-op finish test with isExtended coverage Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop the no-value getAppStartExtension test and trim comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(extend-app-start): Drop the redundant foreground-check comment on extendAppStart Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop the no-value getAppStartExtension test and trim comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop the no-op finishExtendedAppStart test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(extend-app-start): Trim and de-duplicate comments in the eager app start path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(extend-app-start): Clarify the extensionActive comment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Read the extended span finish date, not its finished flag getExtendedEndTime() gated on span.isFinished(), but finishing the extended span completes the waitForChildren transaction and runs the event processor re-entrantly within finishExtendedAppStart(), before the span's finished flag is set. The processor then saw an unfinished span and dropped the app start measurement whenever the extension finished after the first frame. Read getFinishDate() (set before the finish callback) instead, which also keeps the extended end controllable in tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): End the extended transaction at the extended span end When the extended span finished after the timestamp passed to finishTransaction() but before that call ran (e.g. a synchronous extension in a headless start, where finishTransaction runs later at main-thread idle), waitForChildren had nothing left to wait for and the transaction kept the earlier passed timestamp. The extended span then ended after the transaction, and the app start vital exceeded the transaction duration. Finish at max(endTimestamp, extended span finish date) so the span is contained and the duration matches the vital. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ref(extend-app-start): Rename extended app start span op to app.start.extended Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Guard extendAppStartListener with the lock The setter wrote the field without synchronization while extendAppStart() reads it under the lock, leaving no happens-before edge. Acquire the same lock in the setter, consistent with the rest of the class's mutable state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Fix headless app start end time and duplicate txn Two issues in onHeadlessAppStart for extended starts: - The extended branch finished the eager transaction but never persisted appStartEndTime, leaving the continuation window unbounded so a later activity would wrongly continue the stale trace. Persist it on all paths. - The branch only checked isActive(), so if the extension already finished (finishExtendedAppStart or the deadline) before the headless idle ran, a second, empty standalone app.start was created. Guard on shouldSendStartMeasurements to avoid the duplicate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Trim low-value comments in the headless fix Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Drop redundant extend-listener comment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Trim redundant comments in onActivityCreated Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Remove explanatory comments in app start paths Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Consume stored app start trace on the extension path The first activity continuing an extended app.start did not clear the stored trace headers (only the headless-follow path did), so a later activity saw them and wrongly reused the finished app start trace. Clear them on the extension path too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Only the launch activity attaches the app start screen While the extension was open, every activity's onActivityCreated re-attached app.vitals.start.screen to the eager app.start, so a second activity opened before finishExtendedAppStart() overwrote the launch screen. Gate the attach on isAppStart so only the launch activity sets it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Format code * style(extend-app-start): Trim verbose finishTransaction comment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Trim verbose comments in app start paths Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Read finish date in getExtendedAppStartSpan Finishing the extended span runs the event processor re-entrantly (via the waitForChildren transaction) before the span's isFinished() flag is set, while the finish timestamp is already in place. Reading isFinished() could therefore hand out a span that is already finishing. Switch to getFinishDate() == null, matching getExtendedEndTime(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Trim duplicated reentrancy comment in getExtendedAppStartSpan Replace the repeated reentrancy explanation with a cross-reference to getExtendedEndTime(), which holds the canonical version. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Keep trimmed reentrancy comment within line length Single-line form fits the 100-char limit so spotless leaves it unwrapped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Drop two line-narrating test comments Remove comments that restated the adjacent test code (the eager extendAppStart call and the second-activity open). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Reword getExtendedAppStartSpan reentrancy comment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(extend-app-start): Rename isAppStartWindowOpen to canExtendAppStart The predicate has a single consumer — the extend gate in AppStartExtension — so name it for that intent. Also drop the self-evident max-end comment in finishTransaction. Regenerated apiDump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop redundant getExtendedAppStartSpan finished test After getExtendedAppStartSpan switched to getFinishDate(), the "after the span finished" case exercises the same branch as the reentrancy test (finishDate set -> NoOp); the isFinished stub was inert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop narrating comments from app start tests The test names describe the scenarios; the inline comments restated them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(extend-app-start): Add setData and isExtended to AppStartExtension 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> * refactor(extend-app-start): Name app start branching booleans by intent Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Make shouldSendStartMeasurements volatile 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> * docs(extend-app-start): Explain why AppStartExtension holds span and transaction Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(extend-app-start): Return null from getExtendedAppStartSpan when 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> * feat(extend-app-start): Return null from AppStartExtension.getExtendedAppStartSpan when inactive Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Assert null extended span now that getExtendedAppStartSpan is nullable Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(extend-app-start): Explain single-use app start sampling decision 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> * fix(extend-app-start): Bound the trace continuation window after an eager 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> * fix(extend-app-start): Skip warm-start reclassification while the extension 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> * test(extend-app-start): Remove NoOpAppStartExtenderTest per PR review Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
…PI and update example app (#5628) * feat(extend-app-start): Add IAppStartExtender bridge and SentryOptions 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> * chore(extend-app-start): Annotate NoOpAppStartExtender internal and fix test name typo Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(extend-app-start): Rename finishAppStart to finishExtendedAppStart 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> * feat(extend-app-start): Extract AppStartExtension component for the Android 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> * refactor(extend-app-start): Inject logger into AppStartExtension instead of static scope lookup Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(extend-app-start): Return ExtendedAppStart from the listener 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> * chore(extend-app-start): Remove redundant comments and rename reset to 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> * refactor(extend-app-start): Inline the logger lookup instead of injecting 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> * chore(extend-app-start): Drop the class and listener javadocs on AppStartExtension Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(extend-app-start): Trim finishTransaction/getExtendedEndTime comments 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> * refactor(extend-app-start): Rename AppStartExtension.finishAppStart to finishExtendedAppStart Implements the renamed IAppStartExtender.finishExtendedAppStart(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(extend-app-start): Eagerly create the extended App Start transaction (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> * feat(extend-app-start): Add public Sentry app start extension API Static bridge delegating to options.getAppStartExtender(): extendAppStart(), finishAppStart(), getExtendedAppStartSpan(). No-op when the SDK is disabled or the platform provides no app start extender. Completes the extend app start feature (4/4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(extend-app-start): Drop the redundant foreground-check comment on extendAppStart Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Replace the no-op finish test with isExtended coverage Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop the no-value getAppStartExtension test and trim comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(extend-app-start): Drop the redundant foreground-check comment on extendAppStart Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop the no-value getAppStartExtension test and trim comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop the no-op finishExtendedAppStart test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(extend-app-start): Trim and de-duplicate comments in the eager app start path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(extend-app-start): Clarify the extensionActive comment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(extend-app-start): Show extending the app start in the Android sample Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Read the extended span finish date, not its finished flag getExtendedEndTime() gated on span.isFinished(), but finishing the extended span completes the waitForChildren transaction and runs the event processor re-entrantly within finishExtendedAppStart(), before the span's finished flag is set. The processor then saw an unfinished span and dropped the app start measurement whenever the extension finished after the first frame. Read getFinishDate() (set before the finish callback) instead, which also keeps the extended end controllable in tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): End the extended transaction at the extended span end When the extended span finished after the timestamp passed to finishTransaction() but before that call ran (e.g. a synchronous extension in a headless start, where finishTransaction runs later at main-thread idle), waitForChildren had nothing left to wait for and the transaction kept the earlier passed timestamp. The extended span then ended after the transaction, and the app start vital exceeded the transaction duration. Finish at max(endTimestamp, extended span finish date) so the span is contained and the duration matches the vital. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ref(extend-app-start): Rename extended app start span op to app.start.extended Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Guard extendAppStartListener with the lock The setter wrote the field without synchronization while extendAppStart() reads it under the lock, leaving no happens-before edge. Acquire the same lock in the setter, consistent with the rest of the class's mutable state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Fix headless app start end time and duplicate txn Two issues in onHeadlessAppStart for extended starts: - The extended branch finished the eager transaction but never persisted appStartEndTime, leaving the continuation window unbounded so a later activity would wrongly continue the stale trace. Persist it on all paths. - The branch only checked isActive(), so if the extension already finished (finishExtendedAppStart or the deadline) before the headless idle ran, a second, empty standalone app.start was created. Guard on shouldSendStartMeasurements to avoid the duplicate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Trim low-value comments in the headless fix Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Drop redundant extend-listener comment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Trim redundant comments in onActivityCreated Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Remove explanatory comments in app start paths Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Consume stored app start trace on the extension path The first activity continuing an extended app.start did not clear the stored trace headers (only the headless-follow path did), so a later activity saw them and wrongly reused the finished app start trace. Clear them on the extension path too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Only the launch activity attaches the app start screen While the extension was open, every activity's onActivityCreated re-attached app.vitals.start.screen to the eager app.start, so a second activity opened before finishExtendedAppStart() overwrote the launch screen. Gate the attach on isAppStart so only the launch activity sets it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Format code * Format code * style(extend-app-start): Trim verbose finishTransaction comment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Trim verbose comments in app start paths Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Read finish date in getExtendedAppStartSpan Finishing the extended span runs the event processor re-entrantly (via the waitForChildren transaction) before the span's isFinished() flag is set, while the finish timestamp is already in place. Reading isFinished() could therefore hand out a span that is already finishing. Switch to getFinishDate() == null, matching getExtendedEndTime(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Trim duplicated reentrancy comment in getExtendedAppStartSpan Replace the repeated reentrancy explanation with a cross-reference to getExtendedEndTime(), which holds the canonical version. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Keep trimmed reentrancy comment within line length Single-line form fits the 100-char limit so spotless leaves it unwrapped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Drop two line-narrating test comments Remove comments that restated the adjacent test code (the eager extendAppStart call and the second-activity open). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(extend-app-start): Reword getExtendedAppStartSpan reentrancy comment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(extend-app-start): Rename isAppStartWindowOpen to canExtendAppStart The predicate has a single consumer — the extend gate in AppStartExtension — so name it for that intent. Also drop the self-evident max-end comment in finishTransaction. Regenerated apiDump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop redundant getExtendedAppStartSpan finished test After getExtendedAppStartSpan switched to getFinishDate(), the "after the span finished" case exercises the same branch as the reentrancy test (finishDate set -> NoOp); the isFinished stub was inert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Drop narrating comments from app start tests The test names describe the scenarios; the inline comments restated them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(extend-app-start): Add setData and isExtended to AppStartExtension 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> * refactor(extend-app-start): Name app start branching booleans by intent Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(extend-app-start): Make shouldSendStartMeasurements volatile 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> * docs(extend-app-start): Explain why AppStartExtension holds span and transaction Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(extend-app-start): Return null from getExtendedAppStartSpan when 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> * feat(extend-app-start): Return null from AppStartExtension.getExtendedAppStartSpan when inactive Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(extend-app-start): Assert null extended span now that getExtendedAppStartSpan is nullable Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(extend-app-start): Make Sentry.getExtendedAppStartSpan() nullable Mirrors Sentry.getSpan(): returns null when no extension is active instead of a NoOpSpan, so callers can tell there is no span running. Sample null-checks the span. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
# Conflicts: # CHANGELOG.md
…/extend-app-start
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| d364ace | 382.77 ms | 443.21 ms | 60.44 ms |
| 4c04bb8 | 333.16 ms | 408.16 ms | 75.00 ms |
| bb0ff41 | 344.70 ms | 413.82 ms | 69.12 ms |
| 91bb874 | 314.47 ms | 440.00 ms | 125.53 ms |
| bdbe1f4 | 380.66 ms | 464.44 ms | 83.78 ms |
| 8687935 | 294.00 ms | 304.02 ms | 10.02 ms |
| eb95ded | 317.51 ms | 369.08 ms | 51.57 ms |
| b936425 | 302.69 ms | 372.86 ms | 70.17 ms |
| d15471f | 342.08 ms | 415.44 ms | 73.35 ms |
| 5b66efd | 308.67 ms | 363.85 ms | 55.18 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| d364ace | 1.58 MiB | 2.11 MiB | 539.75 KiB |
| 4c04bb8 | 0 B | 0 B | 0 B |
| bb0ff41 | 0 B | 0 B | 0 B |
| 91bb874 | 1.58 MiB | 2.13 MiB | 559.07 KiB |
| bdbe1f4 | 1.58 MiB | 2.11 MiB | 538.88 KiB |
| 8687935 | 1.58 MiB | 2.19 MiB | 619.17 KiB |
| eb95ded | 0 B | 0 B | 0 B |
| b936425 | 0 B | 0 B | 0 B |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| 5b66efd | 1.58 MiB | 2.13 MiB | 559.07 KiB |
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.
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 409b60a. Configure here.
There was a problem hiding this comment.
Pull request overview
Adds a public “extend app start” API to the Sentry Java/Android SDK and wires it through Android app-start tracing so customers can keep the standalone app.start transaction open beyond the first frame and attach their own child spans.
Changes:
- Introduces
Sentry.extendAppStart(),Sentry.finishExtendedAppStart(), andSentry.getExtendedAppStartSpan()and the internal bridging contract (IAppStartExtender). - Implements the Android-side extension lifecycle (
AppStartExtension) and integrates it with standalone app start tracing + app start vital calculation/suppression behavior. - Updates tests, sample app usage, API dumps, and changelog to reflect the new feature.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| sentry/src/test/java/io/sentry/SentryTest.kt | Adds delegation/no-op tests for the new Sentry.*AppStart* facade. |
| sentry/src/test/java/io/sentry/SentryOptionsTest.kt | Verifies SentryOptions.appStartExtender default + setter behavior. |
| sentry/src/test/java/io/sentry/NoOpAppStartExtenderTest.kt | Adds unit tests for the no-op extender behavior. |
| sentry/src/main/java/io/sentry/SentryOptions.java | Adds internal appStartExtender option (defaulting to a no-op). |
| sentry/src/main/java/io/sentry/Sentry.java | Adds the public static app start extension methods. |
| sentry/src/main/java/io/sentry/NoOpAppStartExtender.java | Implements the default no-op bridge implementation. |
| sentry/src/main/java/io/sentry/IAppStartExtender.java | Adds the internal app start extension bridge interface. |
| sentry/api/sentry.api | Updates sentry module API surface dump for the new types/methods. |
| sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java | Demonstrates extending app start and attaching child spans in the Android sample. |
| sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt | Adds tests for extended-end app start measurement and deadline suppression behavior. |
| sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt | Adds tests for the “can extend” gate and extension lifecycle reset behavior. |
| sentry-android-core/src/test/java/io/sentry/android/core/AppStartExtensionTest.kt | Adds focused tests for AppStartExtension state/finish/deadline semantics. |
| sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt | Adds end-to-end tests around eager standalone app.start creation, continuation into ui.load, and lifecycle edge cases. |
| sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java | Uses extended end time (when present) to compute/suppress app start measurements while still attaching spans. |
| sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java | Adds AppStartExtension, exposes canExtendAppStart(), and resets extension state appropriately. |
| sentry-android-core/src/main/java/io/sentry/android/core/AppStartExtension.java | Implements the Android app start extension lifecycle and internal contract. |
| sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java | Wires the Android extender into SentryOptions during initialization. |
| sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java | Registers the extend listener (standalone-only) and implements eager standalone app.start + extended child span creation/continuation. |
| sentry-android-core/api/sentry-android-core.api | Updates android-core module API dump for new internal API surface. |
| CHANGELOG.md | Documents the new app start extension API and includes a usage snippet. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

PR Stack (Extend App Start)
Tested scenarios:
1 · Non-headless — extended after first frame (finish called ~5s in) → OK, app_start_cold = 6511 ms
https://sentry-sdks.sentry.io/explore/traces/trace/cb4ce5b65c42445da7d87d80fd49896a/
2 · Non-headless — finish never called → DEADLINE_EXCEEDED, measurement suppressed
https://sentry-sdks.sentry.io/explore/traces/trace/87bb5f4916f04837b5b4b6a31b021d0a/
3 · Headless — extended (finish called) → OK, app_start_cold = 6342 ms
https://sentry-sdks.sentry.io/explore/traces/trace/0563146982d44e05ac67000cc0089f6e/
4 · Headless — finish never called → DEADLINE_EXCEEDED, measurement suppressed
https://sentry-sdks.sentry.io/explore/traces/trace/6aa847bb646c4810b2991ad6b1c2687d/
Collection PR for the Extend App Start stack. Squash-merge this into
mainonce all stack PRs are merged.Implements the app start extension API —
Sentry.extendAppStart()/Sentry.finishAppStart()/Sentry.getExtendedAppStartSpan()— which lets apps extend the app start measurement to cover extra launch-time work.Closes #5553 #3581