Skip to content

feat(extend-app-start): [3/4] Eagerly create the extended app start transaction#5608

Merged
buenaflor merged 69 commits into
feat/extend-app-startfrom
feat/app-start-extension-materialize
Jul 6, 2026
Merged

feat(extend-app-start): [3/4] Eagerly create the extended app start transaction#5608
buenaflor merged 69 commits into
feat/extend-app-startfrom
feat/app-start-extension-materialize

Conversation

@buenaflor

@buenaflor buenaflor commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

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.start transaction is independent of an activity and can be created in Application.onCreate.

Class Change
ActivityLifecycleIntegration Registers the extend-listener, but only when enableStandaloneAppStartTracing is on (this is what makes the API standalone-only). On Sentry.extendAppStart() the listener eagerly creates the standalone app.start transaction (waitForChildren + deadline) and its app.start.extended child, and returns them as an ExtendedAppStart.
PerformanceAndroidEventProcessor App start vital is now max(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

  • The transaction is owned by the extension, not the integration's appStartTransaction field, so per-activity cleanup can't cancel it.
  • The first activity continues the eager trace into ui.load (via isActive()) instead of starting a second app.start, and attaches the screen so the foreground app.start keeps its app.vitals.start.screen.
  • The first frame finishes the transaction; waitForChildren holds it open until Sentry.finishExtendedAppStart(). Headless finishes it at the headless stop time. A shared createStandaloneAppStartTransaction(...) helper backs both paths.
  • The legacy deferred-span path and the non-standalone (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.start transaction can be created eagerly in onCreate.

💚 How did you test it?

Unit tests (TDD):

  • ActivityLifecycleIntegrationTest — eager app.start + extended child on extend; first activity continues the trace with no second app.start and the screen attached; standalone & headless stay open until finishExtendedAppStart(); 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-core unit suite pass.

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

🔮 Next steps

[4/4] — add the public Sentry.extendAppStart() / Sentry.finishExtendedAppStart() / Sentry.getExtendedAppStartSpan() facade.

⚠️ Merge this PR using a merge commit (not squash). Only the collection branch is squash-merged into main.

#skip-changelog

…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>
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against c553a7a

@sentry

sentry Bot commented Jun 23, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-android Build Distribution Settings

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 322.02 ms 380.71 ms 58.69 ms
Size 0 B 0 B 0 B

Baseline results on branch: feat/app-start-extension-android

Startup times

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>
@buenaflor buenaflor force-pushed the feat/app-start-extension-android branch from 54be119 to d2b96ad Compare June 24, 2026 10:47
@buenaflor buenaflor force-pushed the feat/app-start-extension-materialize branch from 0f8ee4d to 5dfb0e1 Compare June 24, 2026 23:28
@buenaflor buenaflor force-pushed the feat/app-start-extension-materialize branch from 5dfb0e1 to 0bd259a Compare June 25, 2026 09:59
@buenaflor buenaflor changed the title feat(extend-app-start): [3/4] Materialize extended app start span and extend the vital feat(extend-app-start): [3/4] Eagerly create the extended app start transaction Jun 25, 2026
@buenaflor buenaflor force-pushed the feat/app-start-extension-android branch from 5dd39af to 3bc1643 Compare June 25, 2026 11:07
@buenaflor buenaflor force-pushed the feat/app-start-extension-materialize branch from 0bd259a to 0b6ada0 Compare June 25, 2026 11:07
@buenaflor buenaflor force-pushed the feat/app-start-extension-android branch from 3bc1643 to 45f6c0b Compare June 25, 2026 11:37
@buenaflor buenaflor force-pushed the feat/app-start-extension-materialize branch 5 times, most recently from c3f3627 to cbaef2f Compare June 25, 2026 12:23
buenaflor and others added 9 commits June 25, 2026 14:27
…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>
buenaflor and others added 4 commits June 29, 2026 11:32
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>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
buenaflor and others added 8 commits June 29, 2026 14:04
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>
…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>
…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>

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

Reviewed by Cursor Bugbot for commit 9196ec9. Configure here.

@romtsn romtsn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

@buenaflor

Copy link
Copy Markdown
Contributor Author

it's just too many if-elses in here

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.

come up with nice a refactor plan maybe and create an issue out of that?

👍

buenaflor and others added 2 commits July 3, 2026 12:10
…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>
Base automatically changed from feat/app-start-extension-android to feat/extend-app-start July 6, 2026 13:47
…/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
@buenaflor buenaflor merged commit d4fca2d into feat/extend-app-start Jul 6, 2026
26 of 29 checks passed
@buenaflor buenaflor deleted the feat/app-start-extension-materialize branch July 6, 2026 14:48
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.

4 participants