Skip to content

feat(android): Add standalone app start tracing#5342

Open
buenaflor wants to merge 40 commits into
mainfrom
feat/standalone-app-start-tracing
Open

feat(android): Add standalone app start tracing#5342
buenaflor wants to merge 40 commits into
mainfrom
feat/standalone-app-start-tracing

Conversation

@buenaflor
Copy link
Copy Markdown
Contributor

@buenaflor buenaflor commented Apr 28, 2026

📜 Description

Adds standalone Android app-start transaction support behind the opt-in enableStandaloneAppStartTracing option and the io.sentry.standalone-app-start-tracing.enable manifest key.

This PR also covers the non-activity startup path so cold starts triggered by broadcasts or foreground services can emit an App Start Cold/Warm transaction without waiting for an activity. The legacy flag-off behavior is preserved: activity app-start data remains nested under the ui.load transaction.

Additionally this PR adds a new attribute:

  • app.vitals.start.screen

💡 Motivation and Context

Resolves: #5046

Standalone app-start traces make app-start performance visible independently from activity load transactions, and they let Android report app starts where no activity is launched. The implementation keeps the default behavior unchanged and adds tests for the new option, manifest metadata, transaction context trace reuse, app-start metrics, event processing, and activity lifecycle behavior.

App-start creation paths

This PR preserves the existing flag-off behavior and adds a separate standalone app-start branch when enableStandaloneAppStartTracing is enabled.

Scenario How app start is created Resulting trace shape
Activity launch, flag ON ActivityLifecycleIntegration starts a standalone App Start transaction and the normal activity ui.load transaction on the same trace. Two sibling transactions: app.start for app-start phases, and ui.load for TTID/TTFD. The ui.load transaction does not contain an app.start.cold/warm child.
Activity launch, flag OFF Legacy activity lifecycle tracing creates app start as a child span of the activity transaction. One ui.load transaction with nested app.start.cold/warm, plus app-start phase spans under that child.
Non-activity process start, flag ON, Gradle plugin instrumentation present The Gradle plugin records Application.onCreate start/end through AppStartMetrics.onApplicationCreate/PostCreate. When no activity starts, ActivityLifecycleIntegration emits from that completed app-start span. One standalone App Start transaction and no ui.load; includes process.load and application.load phase spans.
Non-activity process start, flag ON, no Gradle plugin on API 35+ AppStartMetrics reads ApplicationStartInfo from ActivityManager to determine start type and use the platform APPLICATION_ONCREATE timestamp as the app-start end time. One standalone App Start transaction and no ui.load; includes process.load, but not application.load because the SDK does not have plugin instrumentation for that phase.
Non-activity process start, flag ON, no Gradle plugin on API <35 ApplicationStartInfo is unavailable, so AppStartMetrics defaults the no-activity process start to cold and uses the class-loaded timestamp fallback for the app-start end time. One standalone App Start transaction and no ui.load; includes process.load only and remains classified as cold.
Non-activity process start followed by activity The non-activity App Start transaction stores its trace ID for a later activity. The later activity ui.load reuses the stored trace ID and does not create a second standalone app-start transaction.
Non-activity process start, flag OFF The non-activity listener is not installed. Broadcasts/services emit no app-start transaction.

💚 How did you test it?

  • ./gradlew spotlessApply apiDump
  • Targeted unit coverage for the option, manifest metadata, app-start metric resolution, event processing, lifecycle integration, and transaction context trace reuse.
  • Manual end-to-end harness runs from scripts/test-standalone-app-start.sh, summarized below.

End-to-end Sentry verification

# What it tests Locally emitted shape Trace ID link
1a Launcher activity cold start, standalone flag ON. Standalone App Start emits alongside sibling MainActivity ui.load; shared trace ID; ui.load has no app.start.* child. app.start (app_start_cold=1156ms) + activity.load x2 + process.load + application.load; sibling ui.load (time_to_initial_display=1156ms) 52154e91...
1c Launcher activity cold start, standalone flag OFF. Legacy shape preserved: single ui.load with app.start.cold nested inside. ui.load (time_to_initial_display=1536ms) -> nested app.start.cold (app_start_cold=1537ms) -> process.load + activity.load x2 d3f0e049...
2a Broadcast cold, tier-1 end-time resolution with Gradle-plugin timing simulation. app.start (app_start_cold=1408ms) + process.load + application.load; no ui.load 81ae80f6...
2b Broadcast cold, tier-2 end-time resolution via Android 15 ApplicationStartInfo. app.start (app_start_cold=525ms) + process.load only; no ui.load 0dcd120e...
2c Broadcast cold, tier-3 fallback on API 33 using CLASS_LOADED_UPTIME_MS. app.start (app_start_cold=614ms) + process.load only; correctly classified Cold ac8a9189...
2d Foreground service cold start through the non-activity path. app.start (app_start_cold=4605ms) + process.load + application.load; no ui.load 905262ce...
2e Broadcast then launcher: trace reuse without duplicate standalone transaction. Same trace: broadcast app.start (app_start_cold=1751ms) + later sibling ui.load (time_to_initial_display=4563ms); no duplicate standalone 5da4a88f...
2f Broadcast + standalone flag OFF. No transaction emitted --

📝 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

buenaflor and others added 10 commits April 2, 2026 14:43
Introduce experimental `enableStandaloneAppStartTracing` option that creates
a separate app start transaction instead of attaching app start as a child
span of the first activity transaction. This is the happy path only (foreground
importance, activity launch, first frame drawn as end time).

The standalone transaction shares the same trace ID as the activity transaction
but is not bound to the scope. App start measurements and child spans (process
init, content providers, application.onCreate) are attached to the standalone
transaction instead of the activity transaction.

Includes foreground importance check branching to prepare for the non-activity
launch path (next PR).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the app starts without launching an activity (service, broadcast
receiver, content provider), create a standalone app start transaction
with the end time determined by priority:

1. onApplicationPostCreate (Gradle plugin bytecode instrumentation)
2. ApplicationStartInfo timestamps (API 35+)
3. firstIdle - main thread idle handler (pre-API 35 fallback)

The non-activity app start transaction stores its trace ID so that if
an activity is later launched, the activity transaction reuses the same
trace ID to keep both in the same trace.

Adds OnNoActivityStartedListener callback from AppStartMetrics to
ActivityLifecycleIntegration, triggered by checkCreateTimeOnMain()
when no activity was created after Application.onCreate().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…entation

When an app is launched via broadcast receiver, service, or content provider
(no activity), detect this via Handler.post() and create a standalone app start
transaction. Resolves app start end time with priority: Gradle plugin >
ApplicationStartInfo (API 35+) > process init time. Also attaches child spans
(process init, content providers, Application.onCreate) to standalone
transactions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract the "try appStartSpan, fall back to sdkInitTimeSpan" logic used for
standalone (non-activity) app start transactions into a new
AppStartMetrics.getAppStartTimeSpanDirect() helper, removing the duplicated
inline fallback in ActivityLifecycleIntegration and the private helper in
PerformanceAndroidEventProcessor.

Also cache the API 35+ ApplicationStartInfo on registerLifecycleCallbacks so
onAppStartSpansSent no longer re-queries ActivityManager, and simplify the
non-activity detection path to always use the main-thread IdleHandler.

Regenerates the sentry-android-core API to include method additions missed in
prior commits on this branch (standalone-app-start options, trace id accessors,
OnNoActivityStartedListener).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires up the TestBroadcastReceiver added earlier so the sample app can trigger
a non-activity cold start via `adb shell am broadcast`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tart-tracing

# Conflicts:
#	sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java
…icate emission

Two pre-merge fixes for the standalone app-start tracing path introduced on
this branch (issue #5046):

- AppStartMetrics.checkCreateTimeOnMain() now defaults appStartType to COLD
  when UNKNOWN with no active activities. On API < 35 (where
  ApplicationStartInfo is unavailable) non-activity cold starts were stuck
  at UNKNOWN, which both misclassified the standalone transaction as
  App Start Warm and caused PerformanceAndroidEventProcessor.attachAppStartSpans
  to early-return (dropping process.load / application.load / contentprovider.load
  phase spans).

- ActivityLifecycleIntegration.onActivityPreCreated() now skips emitting a
  second standalone App Start transaction when the non-activity path has
  already reported the process's app start (detected via the stashed
  appStartTraceId). Previously a broadcast followed by an activity launch
  produced two standalone transactions (a spurious App Start Warm in addition
  to the broadcast's App Start Cold), violating one-per-process semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

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

Generated by 🚫 dangerJS against 2bc059f

@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 28, 2026

📲 Install Builds

Android

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

⚙️ sentry-android Build Distribution Settings

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 434.12 ms 545.04 ms 110.92 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
f064536 349.86 ms 417.66 ms 67.80 ms
6b019b7 343.31 ms 417.23 ms 73.91 ms
2195398 319.02 ms 342.38 ms 23.36 ms
27d7cf8 369.82 ms 422.62 ms 52.80 ms
cf708bd 408.35 ms 458.98 ms 50.63 ms
22f4345 325.23 ms 454.66 ms 129.43 ms
ad8da22 339.92 ms 407.37 ms 67.45 ms
d217708 411.22 ms 430.86 ms 19.63 ms
b77456b 393.26 ms 441.10 ms 47.84 ms
22f4345 307.87 ms 354.51 ms 46.64 ms

App size

Revision Plain With Sentry Diff
f064536 1.58 MiB 2.20 MiB 633.90 KiB
6b019b7 0 B 0 B 0 B
2195398 0 B 0 B 0 B
27d7cf8 1.58 MiB 2.12 MiB 549.42 KiB
cf708bd 1.58 MiB 2.11 MiB 539.71 KiB
22f4345 1.58 MiB 2.29 MiB 719.83 KiB
ad8da22 1.58 MiB 2.29 MiB 719.83 KiB
d217708 1.58 MiB 2.10 MiB 532.97 KiB
b77456b 1.58 MiB 2.12 MiB 548.11 KiB
22f4345 1.58 MiB 2.29 MiB 719.83 KiB

Previous results on branch: feat/standalone-app-start-tracing

Startup times

Revision Plain With Sentry Diff
1bfb17d 343.53 ms 407.87 ms 64.34 ms
a419ead 327.47 ms 366.96 ms 39.49 ms
4d58e3f 329.78 ms 405.36 ms 75.58 ms
3bd6ab9 320.04 ms 362.42 ms 42.38 ms
5f100fc 385.33 ms 474.78 ms 89.45 ms
5f2075f 329.13 ms 392.38 ms 63.25 ms
af2e949 322.72 ms 378.08 ms 55.36 ms
3f06ae7 312.60 ms 355.59 ms 42.99 ms
f03cdee 325.54 ms 376.02 ms 50.48 ms
21e3423 315.65 ms 363.04 ms 47.39 ms

App size

Revision Plain With Sentry Diff
1bfb17d 0 B 0 B 0 B
a419ead 0 B 0 B 0 B
4d58e3f 0 B 0 B 0 B
3bd6ab9 0 B 0 B 0 B
5f100fc 0 B 0 B 0 B
5f2075f 0 B 0 B 0 B
af2e949 0 B 0 B 0 B
3f06ae7 0 B 0 B 0 B
f03cdee 0 B 0 B 0 B
21e3423 0 B 0 B 0 B

buenaflor and others added 2 commits May 4, 2026 10:15
Rename the standalone app-start transaction to a single App Start name
so cold and warm starts group consistently while preserving the app.start op.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

🚨 Detected changes in high risk code 🚨

High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

🚨 Detected changes in high risk code 🚨

High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

@github-actions
Copy link
Copy Markdown
Contributor

🚨 Detected changes in high risk code 🚨

High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:

  • sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

buenaflor and others added 2 commits May 11, 2026 13:00
Co-authored-by: Cursor <cursoragent@cursor.com>
buenaflor and others added 2 commits May 18, 2026 15:25
Use the foreground app start fallback for foreground standalone app
start transactions so measurements match the transaction timestamp.
Keep the headless-only span source limited to true headless starts.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve the headless app start end timestamp only when standalone
headless tracing is active. This avoids stopping legacy app start
spans before a later foreground Activity can finish them.

Co-authored-by: Cursor <cursoragent@cursor.com>
buenaflor and others added 2 commits May 18, 2026 16:50
Make the ApplicationStartInfo headless test install the listener that
now gates headless end-time resolution, matching the standalone path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Rename private headless app start flags to distinguish the pending
main-thread check from the one-shot listener invocation guard. No
behavior change.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
0xadam-brown pushed a commit that referenced this pull request May 29, 2026
Squashed representation of PR #5342. Adds standalone Android app-start
transaction support behind the opt-in enableStandaloneAppStartTracing option
and the io.sentry.standalone-app-start-tracing.enable manifest key, including
the non-activity (headless) startup path. Legacy flag-off behavior is preserved:
activity app-start data remains nested under the ui.load transaction.

Refs #5342
0xadam-brown added a commit that referenced this pull request Jun 1, 2026
Move headless standalone app-start emission out of AppStartMetrics into
StandaloneAppStartReporter behind a new AppStartIntegration. Metrics keeps
idle scheduling and shared boot bookkeeping; ActivityLifecycleIntegration
coordinates via StandaloneAppStartCoordinator only when the feature is enabled.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>
0xadam-brown added a commit that referenced this pull request Jun 1, 2026
…tReporter

The standalone app-start feature was spread across ActivityLifecycleIntegration,
AppStartMetrics, and PerformanceAndroidEventProcessor: ALI held the transaction
field, creation branches, and the headless listener callback; AppStartMetrics
acted as a trace-id mailbox; and the event processor re-derived "is this a
headless standalone app start" by sniffing the presence of a span-data key. No
single place owned the feature.

Introduce StandaloneAppStartReporter, which owns the feature end-to-end on the
post-init side of the SDK (it holds an IScopes and can start transactions):
emitting the activity-launch sibling and the headless transaction, keeping the
shared trace id, and classifying transactions for the event processor via
isStandaloneAppStart/isHeadlessAppStart. AppStartMetrics stays a pre-init,
scope-less recorder that only emits the headless signal; ALI delegates to the
reporter instead of carrying the logic; the event processor asks the reporter
how to classify a transaction rather than poking at its data map. Only data
crosses the pre-/post-init seam, not logic.

No behavior change. Net -76 lines and two fewer public methods on AppStartMetrics.

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

ref(android): Consolidate headless app start scheduling to one site

Scheduling of the main-thread idle check lived in two places: registerLifecycleCallbacks
(gated on appStartType == UNKNOWN || listener != null) and setHeadlessAppStartListener
(a side-effect gated on isCallbackRegistered && no activities yet && first draw not done).
The setter's side-effect was load-bearing because the listener is installed during SDK
init, after the early SentryPerformanceProvider registration, so on API 35+ with a known
start type registerLifecycleCallbacks would skip scheduling and the setter had to cover it.

Schedule the idle check unconditionally from the single registration site and make the
setter a plain assignment. The idle handler already no-ops once an Activity exists and reads
the listener when it fires (after init has installed it), so it can serve both consumers —
the pre-API-35 cold/warm heuristic and standalone headless emission — without the setter
needing to know about timing.

The only behavior change: on API 35+ with a known type and standalone disabled, the idle
check is now scheduled (previously skipped). It no-ops for foreground launches and, for a
truly headless launch, correctly marks the start as background and stops app-start profilers.

ref(android): Add AppStartIntegration for standalone app start

Move headless standalone app-start emission out of AppStartMetrics into
StandaloneAppStartReporter behind a new AppStartIntegration. Metrics keeps
idle scheduling and shared boot bookkeeping; ActivityLifecycleIntegration
coordinates via StandaloneAppStartCoordinator only when the feature is enabled.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>

ref(android): Decouple app start from ActivityLifecycleIntegration

ActivityLifecycleIntegration dispatches AppStartLifecycle callbacks instead
of taking a StandaloneAppStartCoordinator. AppStartIntegration registers
StandaloneAppStartReporter as the listener at init time.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>
0xadam-brown added a commit that referenced this pull request Jun 1, 2026
…tReporter

The standalone app-start feature was spread across ActivityLifecycleIntegration,
AppStartMetrics, and PerformanceAndroidEventProcessor: ALI held the transaction
field, creation branches, and the headless listener callback; AppStartMetrics
acted as a trace-id mailbox; and the event processor re-derived "is this a
headless standalone app start" by sniffing the presence of a span-data key. No
single place owned the feature.

Introduce StandaloneAppStartReporter, which owns the feature end-to-end on the
post-init side of the SDK (it holds an IScopes and can start transactions):
emitting the activity-launch sibling and the headless transaction, keeping the
shared trace id, and classifying transactions for the event processor via
isStandaloneAppStart/isHeadlessAppStart. AppStartMetrics stays a pre-init,
scope-less recorder that only emits the headless signal; ALI delegates to the
reporter instead of carrying the logic; the event processor asks the reporter
how to classify a transaction rather than poking at its data map. Only data
crosses the pre-/post-init seam, not logic.

No behavior change. Net -76 lines and two fewer public methods on AppStartMetrics.

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

ref(android): Consolidate headless app start scheduling to one site

Scheduling of the main-thread idle check lived in two places: registerLifecycleCallbacks
(gated on appStartType == UNKNOWN || listener != null) and setHeadlessAppStartListener
(a side-effect gated on isCallbackRegistered && no activities yet && first draw not done).
The setter's side-effect was load-bearing because the listener is installed during SDK
init, after the early SentryPerformanceProvider registration, so on API 35+ with a known
start type registerLifecycleCallbacks would skip scheduling and the setter had to cover it.

Schedule the idle check unconditionally from the single registration site and make the
setter a plain assignment. The idle handler already no-ops once an Activity exists and reads
the listener when it fires (after init has installed it), so it can serve both consumers —
the pre-API-35 cold/warm heuristic and standalone headless emission — without the setter
needing to know about timing.

The only behavior change: on API 35+ with a known type and standalone disabled, the idle
check is now scheduled (previously skipped). It no-ops for foreground launches and, for a
truly headless launch, correctly marks the start as background and stops app-start profilers.

ref(android): Add AppStartIntegration for standalone app start

Move headless standalone app-start emission out of AppStartMetrics into
StandaloneAppStartReporter behind a new AppStartIntegration. Metrics keeps
idle scheduling and shared boot bookkeeping; ActivityLifecycleIntegration
coordinates via StandaloneAppStartCoordinator only when the feature is enabled.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>

ref(android): Decouple app start from ActivityLifecycleIntegration

ActivityLifecycleIntegration dispatches AppStartLifecycle callbacks instead
of taking a StandaloneAppStartCoordinator. AppStartIntegration registers
StandaloneAppStartReporter as the listener at init time.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>

ref(android): Plan first ui.load via standalone app start listener

Use UiLoadStartPlan so ActivityLifecycleIntegration starts ui.load without
headless branching while StandaloneAppStartReporter owns trace reuse and
sibling App Start emission.

Co-Authored-By: Cursor <cursoragent@cursor.com>
0xadam-brown added a commit that referenced this pull request Jun 1, 2026
…tReporter

The standalone app-start feature was spread across ActivityLifecycleIntegration,
AppStartMetrics, and PerformanceAndroidEventProcessor: ALI held the transaction
field, creation branches, and the headless listener callback; AppStartMetrics
acted as a trace-id mailbox; and the event processor re-derived "is this a
headless standalone app start" by sniffing the presence of a span-data key. No
single place owned the feature.

Introduce StandaloneAppStartReporter, which owns the feature end-to-end on the
post-init side of the SDK (it holds an IScopes and can start transactions):
emitting the activity-launch sibling and the headless transaction, keeping the
shared trace id, and classifying transactions for the event processor via
isStandaloneAppStart/isHeadlessAppStart. AppStartMetrics stays a pre-init,
scope-less recorder that only emits the headless signal; ALI delegates to the
reporter instead of carrying the logic; the event processor asks the reporter
how to classify a transaction rather than poking at its data map. Only data
crosses the pre-/post-init seam, not logic.

No behavior change. Net -76 lines and two fewer public methods on AppStartMetrics.

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

ref(android): Consolidate headless app start scheduling to one site

Scheduling of the main-thread idle check lived in two places: registerLifecycleCallbacks
(gated on appStartType == UNKNOWN || listener != null) and setHeadlessAppStartListener
(a side-effect gated on isCallbackRegistered && no activities yet && first draw not done).
The setter's side-effect was load-bearing because the listener is installed during SDK
init, after the early SentryPerformanceProvider registration, so on API 35+ with a known
start type registerLifecycleCallbacks would skip scheduling and the setter had to cover it.

Schedule the idle check unconditionally from the single registration site and make the
setter a plain assignment. The idle handler already no-ops once an Activity exists and reads
the listener when it fires (after init has installed it), so it can serve both consumers —
the pre-API-35 cold/warm heuristic and standalone headless emission — without the setter
needing to know about timing.

The only behavior change: on API 35+ with a known type and standalone disabled, the idle
check is now scheduled (previously skipped). It no-ops for foreground launches and, for a
truly headless launch, correctly marks the start as background and stops app-start profilers.

ref(android): Add AppStartIntegration for standalone app start

Move headless standalone app-start emission out of AppStartMetrics into
StandaloneAppStartReporter behind a new AppStartIntegration. Metrics keeps
idle scheduling and shared boot bookkeeping; ActivityLifecycleIntegration
coordinates via StandaloneAppStartCoordinator only when the feature is enabled.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>

ref(android): Decouple app start from ActivityLifecycleIntegration

ActivityLifecycleIntegration dispatches AppStartLifecycle callbacks instead
of taking a StandaloneAppStartCoordinator. AppStartIntegration registers
StandaloneAppStartReporter as the listener at init time.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>

ref(android): Plan first ui.load via standalone app start listener

Use UiLoadStartPlan so ActivityLifecycleIntegration starts ui.load without
headless branching while StandaloneAppStartReporter owns trace reuse and
sibling App Start emission.

Co-Authored-By: Cursor <cursoragent@cursor.com>

ref(android): Use single ui.load startTransaction path for standalone

planFirstUiLoad now takes isFirstProcessStart so ActivityLifecycleIntegration
always builds one TransactionContext while headless reuse stays in the reporter.

Co-Authored-By: Cursor <cursoragent@cursor.com>
0xadam-brown added a commit that referenced this pull request Jun 1, 2026
…tReporter

The standalone app-start feature was spread across ActivityLifecycleIntegration,
AppStartMetrics, and PerformanceAndroidEventProcessor: ALI held the transaction
field, creation branches, and the headless listener callback; AppStartMetrics
acted as a trace-id mailbox; and the event processor re-derived "is this a
headless standalone app start" by sniffing the presence of a span-data key. No
single place owned the feature.

Introduce StandaloneAppStartReporter, which owns the feature end-to-end on the
post-init side of the SDK (it holds an IScopes and can start transactions):
emitting the activity-launch sibling and the headless transaction, keeping the
shared trace id, and classifying transactions for the event processor via
isStandaloneAppStart/isHeadlessAppStart. AppStartMetrics stays a pre-init,
scope-less recorder that only emits the headless signal; ALI delegates to the
reporter instead of carrying the logic; the event processor asks the reporter
how to classify a transaction rather than poking at its data map. Only data
crosses the pre-/post-init seam, not logic.

No behavior change. Net -76 lines and two fewer public methods on AppStartMetrics.

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

ref(android): Consolidate headless app start scheduling to one site

Scheduling of the main-thread idle check lived in two places: registerLifecycleCallbacks
(gated on appStartType == UNKNOWN || listener != null) and setHeadlessAppStartListener
(a side-effect gated on isCallbackRegistered && no activities yet && first draw not done).
The setter's side-effect was load-bearing because the listener is installed during SDK
init, after the early SentryPerformanceProvider registration, so on API 35+ with a known
start type registerLifecycleCallbacks would skip scheduling and the setter had to cover it.

Schedule the idle check unconditionally from the single registration site and make the
setter a plain assignment. The idle handler already no-ops once an Activity exists and reads
the listener when it fires (after init has installed it), so it can serve both consumers —
the pre-API-35 cold/warm heuristic and standalone headless emission — without the setter
needing to know about timing.

The only behavior change: on API 35+ with a known type and standalone disabled, the idle
check is now scheduled (previously skipped). It no-ops for foreground launches and, for a
truly headless launch, correctly marks the start as background and stops app-start profilers.

ref(android): Add AppStartIntegration for standalone app start

Move headless standalone app-start emission out of AppStartMetrics into
StandaloneAppStartReporter behind a new AppStartIntegration. Metrics keeps
idle scheduling and shared boot bookkeeping; ActivityLifecycleIntegration
coordinates via StandaloneAppStartCoordinator only when the feature is enabled.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>

ref(android): Decouple app start from ActivityLifecycleIntegration

ActivityLifecycleIntegration dispatches AppStartLifecycle callbacks instead
of taking a StandaloneAppStartCoordinator. AppStartIntegration registers
StandaloneAppStartReporter as the listener at init time.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>

ref(android): Plan first ui.load via standalone app start listener

Use UiLoadStartPlan so ActivityLifecycleIntegration starts ui.load without
headless branching while StandaloneAppStartReporter owns trace reuse and
sibling App Start emission.

Co-Authored-By: Cursor <cursoragent@cursor.com>

ref(android): Use single ui.load startTransaction path for standalone

planFirstUiLoad now takes isFirstProcessStart so ActivityLifecycleIntegration
always builds one TransactionContext while headless reuse stays in the reporter.

Co-Authored-By: Cursor <cursoragent@cursor.com>

ref(android): Rename FirstUiLoad types and narrow standalone API

Rename AppStartLifecycle and AppStartLifecycleListener to FirstUiLoad and
FirstUiLoadListener. Make standalone app start wiring package-private and
drop those types from the public api dump.

Co-Authored-By: Cursor <cursoragent@cursor.com>
0xadam-brown added a commit that referenced this pull request Jun 1, 2026
…tReporter

The standalone app-start feature was spread across ActivityLifecycleIntegration,
AppStartMetrics, and PerformanceAndroidEventProcessor: ALI held the transaction
field, creation branches, and the headless listener callback; AppStartMetrics
acted as a trace-id mailbox; and the event processor re-derived "is this a
headless standalone app start" by sniffing the presence of a span-data key. No
single place owned the feature.

Introduce StandaloneAppStartReporter, which owns the feature end-to-end on the
post-init side of the SDK (it holds an IScopes and can start transactions):
emitting the activity-launch sibling and the headless transaction, keeping the
shared trace id, and classifying transactions for the event processor via
isStandaloneAppStart/isHeadlessAppStart. AppStartMetrics stays a pre-init,
scope-less recorder that only emits the headless signal; ALI delegates to the
reporter instead of carrying the logic; the event processor asks the reporter
how to classify a transaction rather than poking at its data map. Only data
crosses the pre-/post-init seam, not logic.

No behavior change. Net -76 lines and two fewer public methods on AppStartMetrics.

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

ref(android): Consolidate headless app start scheduling to one site

Scheduling of the main-thread idle check lived in two places: registerLifecycleCallbacks
(gated on appStartType == UNKNOWN || listener != null) and setHeadlessAppStartListener
(a side-effect gated on isCallbackRegistered && no activities yet && first draw not done).
The setter's side-effect was load-bearing because the listener is installed during SDK
init, after the early SentryPerformanceProvider registration, so on API 35+ with a known
start type registerLifecycleCallbacks would skip scheduling and the setter had to cover it.

Schedule the idle check unconditionally from the single registration site and make the
setter a plain assignment. The idle handler already no-ops once an Activity exists and reads
the listener when it fires (after init has installed it), so it can serve both consumers —
the pre-API-35 cold/warm heuristic and standalone headless emission — without the setter
needing to know about timing.

The only behavior change: on API 35+ with a known type and standalone disabled, the idle
check is now scheduled (previously skipped). It no-ops for foreground launches and, for a
truly headless launch, correctly marks the start as background and stops app-start profilers.

ref(android): Add AppStartIntegration for standalone app start

Move headless standalone app-start emission out of AppStartMetrics into
StandaloneAppStartReporter behind a new AppStartIntegration. Metrics keeps
idle scheduling and shared boot bookkeeping; ActivityLifecycleIntegration
coordinates via StandaloneAppStartCoordinator only when the feature is enabled.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>

ref(android): Decouple app start from ActivityLifecycleIntegration

ActivityLifecycleIntegration dispatches AppStartLifecycle callbacks instead
of taking a StandaloneAppStartCoordinator. AppStartIntegration registers
StandaloneAppStartReporter as the listener at init time.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>

ref(android): Plan first ui.load via standalone app start listener

Use UiLoadStartPlan so ActivityLifecycleIntegration starts ui.load without
headless branching while StandaloneAppStartReporter owns trace reuse and
sibling App Start emission.

Co-Authored-By: Cursor <cursoragent@cursor.com>

ref(android): Use single ui.load startTransaction path for standalone

planFirstUiLoad now takes isFirstProcessStart so ActivityLifecycleIntegration
always builds one TransactionContext while headless reuse stays in the reporter.

Co-Authored-By: Cursor <cursoragent@cursor.com>

ref(android): Rename FirstUiLoad types and narrow standalone API

Rename AppStartLifecycle and AppStartLifecycleListener to FirstUiLoad and
FirstUiLoadListener. Make standalone app start wiring package-private and
drop those types from the public api dump.

Co-Authored-By: Cursor <cursoragent@cursor.com>
* standalone app start transactions that need to belong to the same trace as the activity
* transaction.
*/
@ApiStatus.Internal
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.

Could we open it up? I recall having a need in this so our customers could easily stitch multiple txs together, this would prevent them from using internal APIs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we can open it up but not guarantee sampleRand consistency which only works with the manual continueTrace hack right now, wdyt?

Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md
}

final TransactionOptions txnOptions = new TransactionOptions();
txnOptions.setBindToScope(false);
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.

any specific reason not to bind it to scope here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not sure if it makes much sense, typically the headless transaction will start and end at a defined time (it's not an idle transaciton like ui.load)

Copy link
Copy Markdown
Member

@romtsn romtsn left a comment

Choose a reason for hiding this comment

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

Left some comments, overall nicely done! Let's see where do we go after today's sync.

And one more point I'd like to maybe document is the 1 minute threshold warm starts that we'd discussed in DM. Perhaps we should leave a comment that it's not gonna work as expected for standalone?

0xadam-brown added a commit that referenced this pull request Jun 1, 2026
…tReporter

The standalone app-start feature was spread across ActivityLifecycleIntegration,
AppStartMetrics, and PerformanceAndroidEventProcessor: ALI held the transaction
field, creation branches, and the headless listener callback; AppStartMetrics
acted as a trace-id mailbox; and the event processor re-derived "is this a
headless standalone app start" by sniffing the presence of a span-data key. No
single place owned the feature.

Introduce StandaloneAppStartReporter, which owns the feature end-to-end on the
post-init side of the SDK (it holds an IScopes and can start transactions):
emitting the activity-launch sibling and the headless transaction, keeping the
shared trace id, and classifying transactions for the event processor via
isStandaloneAppStart/isHeadlessAppStart. AppStartMetrics stays a pre-init,
scope-less recorder that only emits the headless signal; ALI delegates to the
reporter instead of carrying the logic; the event processor asks the reporter
how to classify a transaction rather than poking at its data map. Only data
crosses the pre-/post-init seam, not logic.

No behavior change. Net -76 lines and two fewer public methods on AppStartMetrics.

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

ref(android): Consolidate headless app start scheduling to one site

Scheduling of the main-thread idle check lived in two places: registerLifecycleCallbacks
(gated on appStartType == UNKNOWN || listener != null) and setHeadlessAppStartListener
(a side-effect gated on isCallbackRegistered && no activities yet && first draw not done).
The setter's side-effect was load-bearing because the listener is installed during SDK
init, after the early SentryPerformanceProvider registration, so on API 35+ with a known
start type registerLifecycleCallbacks would skip scheduling and the setter had to cover it.

Schedule the idle check unconditionally from the single registration site and make the
setter a plain assignment. The idle handler already no-ops once an Activity exists and reads
the listener when it fires (after init has installed it), so it can serve both consumers —
the pre-API-35 cold/warm heuristic and standalone headless emission — without the setter
needing to know about timing.

The only behavior change: on API 35+ with a known type and standalone disabled, the idle
check is now scheduled (previously skipped). It no-ops for foreground launches and, for a
truly headless launch, correctly marks the start as background and stops app-start profilers.

ref(android): Add AppStartIntegration for standalone app start

Move headless standalone app-start emission out of AppStartMetrics into
StandaloneAppStartReporter behind a new AppStartIntegration. Metrics keeps
idle scheduling and shared boot bookkeeping; ActivityLifecycleIntegration
coordinates via StandaloneAppStartCoordinator only when the feature is enabled.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>

ref(android): Decouple app start from ActivityLifecycleIntegration

ActivityLifecycleIntegration dispatches AppStartLifecycle callbacks instead
of taking a StandaloneAppStartCoordinator. AppStartIntegration registers
StandaloneAppStartReporter as the listener at init time.

Refs #5342
Co-Authored-By: Cursor <cursoragent@cursor.com>

Co-authored-by: Cursor <cursoragent@cursor.com>

ref(android): Plan first ui.load via standalone app start listener

Use UiLoadStartPlan so ActivityLifecycleIntegration starts ui.load without
headless branching while StandaloneAppStartReporter owns trace reuse and
sibling App Start emission.

Co-Authored-By: Cursor <cursoragent@cursor.com>

ref(android): Use single ui.load startTransaction path for standalone

planFirstUiLoad now takes isFirstProcessStart so ActivityLifecycleIntegration
always builds one TransactionContext while headless reuse stays in the reporter.

Co-Authored-By: Cursor <cursoragent@cursor.com>

ref(android): Rename FirstUiLoad types and narrow standalone API

Rename AppStartLifecycle and AppStartLifecycleListener to FirstUiLoad and
FirstUiLoadListener. Make standalone app start wiring package-private and
drop those types from the public api dump.

Co-Authored-By: Cursor <cursoragent@cursor.com>

ref(android): Replace static FirstUiLoad with init-scoped coordinator

Wire the standalone app-start listener through a FirstUiLoadCoordinator
created in AndroidOptionsInitializer and shared by AppStartIntegration
and ActivityLifecycleIntegration instead of a static global holder.

Co-Authored-By: Auto <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…tart-tracing

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	CHANGELOG.md
Set the standalone headless app start transaction origin to
`auto.app.start` instead of `auto.ui.activity`, which was semantically
incorrect for non-activity (broadcast/service/content provider) starts.

Also simplify the API 35+ ApplicationStartInfo onCreate timestamp
resolution by using the reported nanos directly as the uptime base.

Co-authored-by: Cursor <cursoragent@cursor.com>
appStartTransactionOptions.setBindToScope(false);
appStartTransactionOptions.setStartTimestamp(appStartTime);
appStartTransactionOptions.setAppStartTransaction(appStartSamplingDecision != null);
setSpanOrigin(appStartTransactionOptions);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The foreground standalone app start transaction is assigned the wrong origin, "auto.ui.activity", instead of "auto.app.start". This is inconsistent with the headless app start path.
Severity: MEDIUM

Suggested Fix

In ActivityLifecycleIntegration.startTracing(), replace the call to setSpanOrigin(appStartTransactionOptions); with appStartTransactionOptions.setOrigin(APP_START_TRACE_ORIGIN);. This will ensure the foreground app start transaction has the correct origin, consistent with the headless app start path.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java#L291

Potential issue: The foreground standalone app start transaction is incorrectly assigned
an origin of `"auto.ui.activity"` instead of the expected `"auto.app.start"`. In
`ActivityLifecycleIntegration.startTracing()`, the method `setSpanOrigin()` is called,
which sets the origin to `TRACE_ORIGIN` (`"auto.ui.activity"`). This is inconsistent
with the headless app start path, which correctly sets the origin to
`APP_START_TRACE_ORIGIN` (`"auto.app.start"`). This discrepancy results in incorrect
transaction metadata, which can negatively impact backend processing, categorization,
and the monitoring of app start performance. The fix was applied to the headless path
but was missed for the foreground activity launch path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@romtsn I guess we wanna keep auto.ui.activity for the normal foreground app start? or should we change it so both are auto.app.start right now?

Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 2bc059f. Configure here.

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.

feat: Send standalone app start transactions

4 participants