Skip to content

feat(extend-app-start): [4/4] Add public Sentry app start extension API and update example app#5628

Merged
buenaflor merged 94 commits into
feat/extend-app-startfrom
feat/app-start-extension-api
Jul 7, 2026
Merged

feat(extend-app-start): [4/4] Add public Sentry app start extension API and update example app#5628
buenaflor merged 94 commits into
feat/extend-app-startfrom
feat/app-start-extension-api

Conversation

@buenaflor

@buenaflor buenaflor commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

PR Stack (Extend App Start)


📜 Description

Adds the public static facade for the app start extension API — the only PR in the stack that adds public API surface. Each method is a thin bridge to getCurrentScopes().getOptions().getAppStartExtender() (the IAppStartExtender from [1/4]), mirroring Sentry.reportFullyDisplayed().

Method Purpose
Sentry.extendAppStart() Begin extending the app start (call from Application.onCreate, right after SentryAndroid.init).
Sentry.getExtendedAppStartSpan() The extended ISpan to attach child spans to — or a NoOpSpan when no extension is active.
Sentry.finishExtendedAppStart() Finish the extension, letting the app start transaction complete.

No-op when the SDK is disabled, on platforms without an app start extender, or when standalone app start tracing is off.

Also adds a usage example to the Android sample (MyApplication): extendAppStart(), child spans under getExtendedAppStartSpan(), then finishExtendedAppStart().

💡 Motivation and Context

Fourth and final PR of the app start extension API stack (#5553), which lets apps extend the app start measurement to cover extra launch-time work — e.g. loading remote config before the first screen (mirrors sentry-cocoa's extendAppLaunch() / finishExtendedAppLaunch()).

💚 How did you test it?

SentryTest verifies the three methods delegate to options.getAppStartExtender(), and that the API returns a no-op span / no-ops when the SDK is disabled. apiCheck, spotless, and the :sentry unit suite pass; the sample module compiles.

📝 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

Final PR of the stack. Once the stack is merged up, the collection branch (#5604) squash-merges into main.

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

#skip-changelog

buenaflor and others added 2 commits June 23, 2026 14:13
…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>
…ix test name typo

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 25, 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 9caa920

@sentry

sentry Bot commented Jun 25, 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 25, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 319.39 ms 383.19 ms 63.80 ms
Size 0 B 0 B 0 B

Previous results on branch: feat/app-start-extension-api

Startup times

Revision Plain With Sentry Diff
c5023f1 314.61 ms 362.49 ms 47.88 ms
4589e4d 294.21 ms 364.89 ms 70.68 ms
3ce7cf9 348.98 ms 428.10 ms 79.12 ms
c08cfa0 317.36 ms 373.48 ms 56.12 ms
097718d 323.29 ms 363.42 ms 40.13 ms
259d761 328.98 ms 388.63 ms 59.65 ms
d35892b 313.39 ms 367.63 ms 54.25 ms
13d4096 307.35 ms 362.96 ms 55.61 ms

App size

Revision Plain With Sentry Diff
c5023f1 0 B 0 B 0 B
4589e4d 0 B 0 B 0 B
3ce7cf9 0 B 0 B 0 B
c08cfa0 0 B 0 B 0 B
097718d 0 B 0 B 0 B
259d761 0 B 0 B 0 B
d35892b 0 B 0 B 0 B
13d4096 0 B 0 B 0 B

@buenaflor buenaflor force-pushed the feat/app-start-extension-materialize branch from 5dfb0e1 to 0bd259a Compare June 25, 2026 09:59
@buenaflor buenaflor force-pushed the feat/app-start-extension-api branch from bd89a68 to 90684a2 Compare June 25, 2026 10:00
@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-api branch from 90684a2 to 51ca64a Compare June 25, 2026 11:07
@buenaflor buenaflor force-pushed the feat/app-start-extension-materialize branch from 0b6ada0 to 9e6f0a0 Compare June 25, 2026 11:38
@buenaflor buenaflor force-pushed the feat/app-start-extension-api branch from 51ca64a to 4c3438c Compare June 25, 2026 11:38
@buenaflor buenaflor force-pushed the feat/app-start-extension-materialize branch from 9e6f0a0 to 976c29a Compare June 25, 2026 12:10
@buenaflor buenaflor force-pushed the feat/app-start-extension-api branch from 4c3438c to 93464ef Compare June 25, 2026 12:10
@buenaflor buenaflor force-pushed the feat/app-start-extension-materialize branch from 976c29a to ac7a577 Compare June 25, 2026 12:17
@buenaflor buenaflor force-pushed the feat/app-start-extension-api branch from 93464ef to a4baf22 Compare June 25, 2026 12:17
@buenaflor buenaflor force-pushed the feat/app-start-extension-materialize branch from ac7a577 to c3f3627 Compare June 25, 2026 12:18
@buenaflor buenaflor force-pushed the feat/app-start-extension-api branch from a4baf22 to c559b65 Compare June 25, 2026 12:18
@buenaflor buenaflor force-pushed the feat/app-start-extension-materialize branch from c3f3627 to cbaef2f Compare June 25, 2026 12:23
@buenaflor buenaflor force-pushed the feat/app-start-extension-api branch from c559b65 to 50357a5 Compare June 25, 2026 12:23
buenaflor and others added 7 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>
buenaflor and others added 9 commits June 29, 2026 11:09
…ed 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>
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>
@buenaflor buenaflor changed the title feat(extend-app-start): [4/4] Add public Sentry app start extension API feat(extend-app-start): [4/4] Add public Sentry app start extension API and update example app Jun 29, 2026
buenaflor and others added 13 commits June 29, 2026 13:42
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>
…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>
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>
Base automatically changed from feat/app-start-extension-materialize to feat/extend-app-start July 6, 2026 14:48
…/app-start-extension-api

# Conflicts:
#	sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java
#	sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt
#	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 33ccdf9 into feat/extend-app-start Jul 7, 2026
62 of 64 checks passed
@buenaflor buenaflor deleted the feat/app-start-extension-api branch July 7, 2026 08:51
buenaflor added a commit that referenced this pull request Jul 7, 2026
* collection: extend app start

* changelog

* feat(extend-app-start): [1/4] Add IAppStartExtender bridge (#5605)

* 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>

* 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>

* feat(extend-app-start): [2/4] Extract AppStartExtension component (#5606)

* 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>

* feat(extend-app-start): [3/4] Eagerly create the extended app start transaction (#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>

* feat(extend-app-start): [4/4] Add public Sentry app start extension API 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>

* changelog

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
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