Skip to content

Commit c251654

Browse files
buenaflorclaudecursoragent
authored andcommitted
feat(android): Add standalone app start tracing (#5342)
* feat: Add standalone app start transaction (happy path) 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> * feat: Add non-activity app start path with end time resolution 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> * feat: Support non-activity app start tracing without bytecode instrumentation 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> * refactor: Consolidate non-activity app start time-span resolution 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> * chore(samples): Register TestBroadcastReceiver in manifest 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> * fix(app-start): resolve standalone tracing misclassification and duplicate 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> * fix(android): refine standalone app start tracing * chore: Update generated files * style(core): Apply spotless formatting * changelog * fix(android): Use stable app start transaction name 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> * feat(android): Add standalone app start tracing Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): Handle non-activity app starts below API 24 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): Guard app start timestamp clock base Co-authored-by: Cursor <cursoragent@cursor.com> * ref(android): Remove app start reason plumbing Co-authored-by: Cursor <cursoragent@cursor.com> * ref(android): Clarify no-activity app start handling Rename the private app start helper to reflect that it conditionally handles non-activity starts. Keep comments and tests focused on behavior. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * docs(android): Clarify non-activity app start fallback Explain why unresolved non-activity starts default to cold when Activity signals or ApplicationStartInfo classification are unavailable. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): Preserve legacy no-activity app start guard Only run the no-activity startup check for unresolved app starts or when standalone app start tracing registered a listener. This keeps API 35 ApplicationStartInfo classifications from triggering legacy side effects. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * test(android): Opt into standalone no-activity API 35 tests Register a no-op no-activity listener for API 35 end-time resolution tests so they exercise the standalone path under the restored legacy guard. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): Schedule no-activity idle check when standalone listener is set on API 35+ On API 35+, ApplicationStartInfo resolves appStartType before the standalone app start listener is installed, causing the idle handler condition to be false and skipping the no-activity detection entirely. Register the idle handler from setOnNoActivityStartedListener when the type is already resolved, ensuring onNoActivityStarted() fires for standalone app start tracing on API 35+ devices. Co-authored-by: Cursor <cursoragent@cursor.com> * ref(android): Remove dead foregroundImportance check in standalone app start path The foregroundImportance guard was always true at that point because appStartTime is only set to non-null inside the foregroundImportance branch. Remove the redundant check and the misleading else comment that described an unreachable code path. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): Prevent duplicate standalone app start measurements Require the app-start pending flag even when standalone app-start transactions bypass foreground checks. Preserve completed non-activity app-start timings so fallback resolution does not overwrite stopped spans. Co-authored-by: Cursor <cursoragent@cursor.com> * ref(android): Remove unused app start application context Drop dead AppStartMetrics state that was assigned during lifecycle callback registration but never read. Co-authored-by: Cursor <cursoragent@cursor.com> * ref(android): Rename getAppStartTimeSpanDirect to getAppStartTimeSpanForStandalone Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): Do not set TTID/TTFD contributing flags on standalone app start spans Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): Add volatile to noActivityStartedListener for cross-thread visibility The field is written by setOnNoActivityStartedListener (called during Sentry.init(), potentially on a background thread) and read on the main thread in handleNoActivityStartIfNeededOnMain. Without volatile, the JMM permits the main thread to see a stale null, silently skipping the listener and preventing standalone app-start transaction creation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): Clear stale app start sampling decision in non-activity start path onNoActivityStarted() did not clear the appStartSamplingDecision, which could leak to the first ui.load transaction when an activity eventually starts after a non-activity process launch. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: Format adb test commands in TestBroadcastReceiver JavaDoc Co-authored-by: Cursor <cursoragent@cursor.com> * ref(android): Rename headless app start handling Use headless terminology for app starts that do not reach an Activity and schedule the headless check from lifecycle callback registration. This removes listener setter side effects while preserving standalone app-start behavior. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): Align foreground app start measurements 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> * fix(android): Gate headless app start end time 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> * test(android): Update API 35 headless app start expectation 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> * Fix headless app-start idle scheduling * ref(android): Clarify headless app start state names 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> * ref(android): Use app.start origin for headless app start transaction 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> * ref(android): refine standalone app start trace continuation Drop the redundant trace-id sharing TransactionContext constructor; the ui.load now shares the app.start trace solely through continueTrace. Don't connect a headless app.start and a following activity's ui.load into the same trace when they are more than 1 minute apart, since such a large gap means they no longer belong to the same launch. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): align headless app start tests with uptime-based onCreate timestamp ApplicationStartInfo's START_TIMESTAMP_APPLICATION_ONCREATE is captured via SystemClock.uptimeNanos(), the same base as TimeSpan, so no clock re-anchoring is needed. Add the missing headless test setup (foreground-importance stubbing) and fix the API 35 timestamp test to use uptime semantics. Co-authored-by: Cursor <cursoragent@cursor.com> * test(android): add standalone app start E2E harness Wire the Android sample app for manual standalone app-start validation and add a reusable harness plus notes for the scenarios verified locally. Trim redundant comments around app-start trace continuation while keeping the non-obvious sampling and parentage details. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * chore(android): remove standalone app start report Co-authored-by: Cursor <cursoragent@cursor.com> * test(android): clarify app start transaction shapes Co-authored-by: Cursor <cursoragent@cursor.com> * chore(android): remove standalone app start harness Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): Preserve app start activity counter Keep the foreground headless guard from faking an observed activity so late standalone app start init still lets the first real activity classify startup and reset warm-start state correctly. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(android): Finish app start after activity spans Keep standalone app-start transactions open until activity lifecycle spans are attached so early app-start completion does not drop activity spans. Co-Authored-By: Cursor <cursoragent@cursor.com> * feat(samples): enable standalone app start tracing and add headless-start broadcast receiver Co-authored-by: Cursor <cursoragent@cursor.com> * fix(changelog): resolve merge conflict and keep standalone app start entry under Unreleased Co-authored-by: Cursor <cursoragent@cursor.com> * docs(options): Clarify standalone app start javadoc per review - Use plain quotes for the "App Start" transaction name instead of {@code} - Clarify that the API 35 gate refers to the device's runtime OS version Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(android): Clarify ApplicationStartInfo onCreate timestamp marks onCreate start START_TIMESTAMP_APPLICATION_ONCREATE is captured right before Application.onCreate is invoked (ActivityThread.handleBindApplication), so it is the onCreate start, not its end. Rename locals, fix comments and javadoc accordingly, and drop the applicationOnCreate.setStoppedAt branch which could have recorded a zero-length application.load span. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1f42c06 commit c251654

17 files changed

Lines changed: 1695 additions & 113 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Add `enableStandaloneAppStartTracing` option to send app start as a standalone transaction instead of attaching it as a child span of the first activity transaction ([#5342](https://github.com/getsentry/sentry-java/pull/5342))
8+
- Disabled by default; opt in via `options.isEnableStandaloneAppStartTracing = true` or manifest meta-data `io.sentry.standalone-app-start-tracing.enable`
9+
- Emits a transaction named `App Start` with op `app.start`, carrying the existing app start measurements and phase spans (`process.load`, `contentprovider.load`, `application.load`, activity lifecycle spans) as direct children of the root
10+
- The standalone transaction shares the same `traceId` as the first `ui.load` activity transaction so they remain linked in the trace view
11+
- Also covers non-activity starts (broadcast receivers, services, content providers)
12+
513
### Improvements
614

715
- Reduce boxing to improve performance ([#5523](https://github.com/getsentry/sentry-java/pull/5523), [#5527](https://github.com/getsentry/sentry-java/pull/5527))

sentry-android-core/api/sentry-android-core.api

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
392392
public fun isEnablePerformanceV2 ()Z
393393
public fun isEnableRootCheck ()Z
394394
public fun isEnableScopeSync ()Z
395+
public fun isEnableStandaloneAppStartTracing ()Z
395396
public fun isEnableSystemEventBreadcrumbs ()Z
396397
public fun isEnableSystemEventBreadcrumbsExtras ()Z
397398
public fun isReportHistoricalAnrs ()Z
@@ -423,6 +424,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
423424
public fun setEnablePerformanceV2 (Z)V
424425
public fun setEnableRootCheck (Z)V
425426
public fun setEnableScopeSync (Z)V
427+
public fun setEnableStandaloneAppStartTracing (Z)V
426428
public fun setEnableSystemEventBreadcrumbs (Z)V
427429
public fun setEnableSystemEventBreadcrumbsExtras (Z)V
428430
public fun setFrameMetricsCollector (Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V
@@ -740,11 +742,16 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
740742
public fun clear ()V
741743
public fun createProcessInitSpan ()Lio/sentry/android/core/performance/TimeSpan;
742744
public fun getActivityLifecycleTimeSpans ()Ljava/util/List;
745+
public fun getAppStartBaggageHeader ()Ljava/lang/String;
743746
public fun getAppStartContinuousProfiler ()Lio/sentry/IContinuousProfiler;
747+
public fun getAppStartEndTime ()Lio/sentry/SentryDate;
744748
public fun getAppStartProfiler ()Lio/sentry/ITransactionProfiler;
745749
public fun getAppStartSamplingDecision ()Lio/sentry/TracesSamplingDecision;
750+
public fun getAppStartSentryTraceHeader ()Ljava/lang/String;
746751
public fun getAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
752+
public fun getAppStartTimeSpanForHeadless ()Lio/sentry/android/core/performance/TimeSpan;
747753
public fun getAppStartTimeSpanWithFallback (Lio/sentry/android/core/SentryAndroidOptions;)Lio/sentry/android/core/performance/TimeSpan;
754+
public fun getAppStartTraceId ()Lio/sentry/protocol/SentryId;
748755
public fun getAppStartType ()Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;
749756
public fun getApplicationOnCreateTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
750757
public fun getClassLoadedUptimeMs ()J
@@ -765,12 +772,18 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
765772
public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V
766773
public fun registerLifecycleCallbacks (Landroid/app/Application;)V
767774
public fun setAppLaunchedInForeground (Z)V
775+
public fun setAppStartBaggageHeader (Ljava/lang/String;)V
768776
public fun setAppStartContinuousProfiler (Lio/sentry/IContinuousProfiler;)V
777+
public fun setAppStartEndTime (Lio/sentry/SentryDate;)V
769778
public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V
770779
public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V
780+
public fun setAppStartSentryTraceHeader (Ljava/lang/String;)V
781+
public fun setAppStartTraceId (Lio/sentry/protocol/SentryId;)V
771782
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V
772783
public fun setClassLoadedUptimeMs (J)V
784+
public fun setHeadlessAppStartListener (Lio/sentry/android/core/performance/AppStartMetrics$HeadlessAppStartListener;)V
773785
public fun shouldSendStartMeasurements ()Z
786+
public fun shouldSendStartMeasurements (Z)Z
774787
}
775788

776789
public final class io/sentry/android/core/performance/AppStartMetrics$AppStartType : java/lang/Enum {
@@ -781,6 +794,10 @@ public final class io/sentry/android/core/performance/AppStartMetrics$AppStartTy
781794
public static fun values ()[Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;
782795
}
783796

797+
public abstract interface class io/sentry/android/core/performance/AppStartMetrics$HeadlessAppStartListener {
798+
public abstract fun onHeadlessAppStart ()V
799+
}
800+
784801
public class io/sentry/android/core/performance/TimeSpan : java/lang/Comparable {
785802
public fun <init> ()V
786803
public fun compareTo (Lio/sentry/android/core/performance/TimeSpan;)I

0 commit comments

Comments
 (0)