feat(android): Add standalone app start tracing#5342
Conversation
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>
|
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| 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 |
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>
🚨 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:
|
Co-authored-by: Cursor <cursoragent@cursor.com>
🚨 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:
|
Co-authored-by: Cursor <cursoragent@cursor.com>
🚨 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:
|
Co-authored-by: Cursor <cursoragent@cursor.com>
…g' into feat/standalone-app-start-tracing
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>
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>
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
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>
…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>
…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>
…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>
…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>
…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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
we can open it up but not guarantee sampleRand consistency which only works with the manual continueTrace hack right now, wdyt?
| } | ||
|
|
||
| final TransactionOptions txnOptions = new TransactionOptions(); | ||
| txnOptions.setBindToScope(false); |
There was a problem hiding this comment.
any specific reason not to bind it to scope here?
There was a problem hiding this comment.
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)
romtsn
left a comment
There was a problem hiding this comment.
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?
…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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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?
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2bc059f. Configure here.

📜 Description
Adds standalone Android app-start transaction support behind the opt-in
enableStandaloneAppStartTracingoption and theio.sentry.standalone-app-start-tracing.enablemanifest 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/Warmtransaction without waiting for an activity. The legacy flag-off behavior is preserved: activity app-start data remains nested under theui.loadtransaction.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
enableStandaloneAppStartTracingis enabled.ActivityLifecycleIntegrationstarts a standaloneApp Starttransaction and the normal activityui.loadtransaction on the same trace.app.startfor app-start phases, andui.loadfor TTID/TTFD. Theui.loadtransaction does not contain anapp.start.cold/warmchild.ui.loadtransaction with nestedapp.start.cold/warm, plus app-start phase spans under that child.Application.onCreatestart/end throughAppStartMetrics.onApplicationCreate/PostCreate. When no activity starts,ActivityLifecycleIntegrationemits from that completed app-start span.App Starttransaction and noui.load; includesprocess.loadandapplication.loadphase spans.AppStartMetricsreadsApplicationStartInfofromActivityManagerto determine start type and use the platformAPPLICATION_ONCREATEtimestamp as the app-start end time.App Starttransaction and noui.load; includesprocess.load, but notapplication.loadbecause the SDK does not have plugin instrumentation for that phase.ApplicationStartInfois unavailable, soAppStartMetricsdefaults the no-activity process start to cold and uses the class-loaded timestamp fallback for the app-start end time.App Starttransaction and noui.load; includesprocess.loadonly and remains classified as cold.App Starttransaction stores its trace ID for a later activity.ui.loadreuses the stored trace ID and does not create a second standalone app-start transaction.💚 How did you test it?
./gradlew spotlessApply apiDumpscripts/test-standalone-app-start.sh, summarized below.End-to-end Sentry verification
App Startemits alongside siblingMainActivity ui.load; shared trace ID;ui.loadhas noapp.start.*child.app.start(app_start_cold=1156ms) +activity.loadx2 +process.load+application.load; siblingui.load(time_to_initial_display=1156ms)52154e91...ui.loadwithapp.start.coldnested inside.ui.load(time_to_initial_display=1536ms) -> nestedapp.start.cold(app_start_cold=1537ms) ->process.load+activity.loadx2d3f0e049...app.start(app_start_cold=1408ms) +process.load+application.load; noui.load81ae80f6...ApplicationStartInfo.app.start(app_start_cold=525ms) +process.loadonly; noui.load0dcd120e...CLASS_LOADED_UPTIME_MS.app.start(app_start_cold=614ms) +process.loadonly; correctly classified Coldac8a9189...app.start(app_start_cold=4605ms) +process.load+application.load; noui.load905262ce...app.start(app_start_cold=1751ms) + later siblingui.load(time_to_initial_display=4563ms); no duplicate standalone5da4a88f...📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps