Skip to content

feat: Send standalone app start transactions #5046

@noahsmartin

Description

@noahsmartin

Currently, app start data is only captured when a ui.load transaction (Activity tracing) starts within the app start window. If no qualifying transaction occurs, app start data is lost entirely.

Proposal

Instead of attaching app start spans and measurements to the first Activity transaction, the SDK should send a standalone app start transaction as soon as app start data is available. This transaction carries all the same spans, measurements (app_start_cold/app_start_warm, TTID, TTFD), and context (app.start_type) that are currently attached to the first ui.load transaction.

Note: This proposal was generated with Claude Code based on analysis of the Cocoa SDK implementation. Please double-check the details before implementing.

Transaction / Span Tree

Transaction: "App Start Cold" (op: app.start.cold)
├── process.load
├── contentprovider.load
├── application.load
└── activity lifecycle spans

Details

  • Transaction name: App Start Cold or App Start Warm, matching the start type. The mobile vitals insights dashboard groups transactions by name, so including the app start type allows users to inspect cold and warm starts separately.
  • Transaction operation: app.start.cold or app.start.warm, matching the start type.
  • Rollout: Opt-in via a new option enableStandaloneAppStartTracing, disabled by default.
  • Backwards compatibility: When the option is disabled, existing behavior is unchanged — app start data continues to be attached to the first Activity ui.load transaction. When enabled, the first Activity transaction still works correctly on its own, just without the app start spans/measurements attached.
  • No duplicate data: When this feature is enabled, the SDK must not attach app start spans and measurements to the first Activity transaction. App start data is only sent via the standalone transaction to avoid duplication.
  • Hybrid SDKs: Flutter and React Native pull raw app start data from the native layer via InternalSentrySdk.getAppStartMeasurement() and construct their own transactions. enableStandaloneAppStartTracing should not activate when a hybrid SDK is consuming native app start data, to avoid duplicate transactions. See the hybrid SDK analysis on the Cocoa issue for details.
Detailed breakdown of what needs to change

Current transaction structure (attached to first Activity)

Transaction: "MainActivity" (op: ui.load)
  ├── Cold Start (op: app.start.cold)           ← grouping span
  │     ├── process.load
  │     ├── contentprovider.load
  │     ├── application.load
  │     └── activity lifecycle spans
  ├── MainActivity initial display (op: ui.load.initial_display)
  └── MainActivity full display (op: ui.load.full_display)

Target transaction structure (standalone)

Transaction: "App Start Cold" (op: app.start.cold)     ← root
  ├── process.load
  ├── contentprovider.load
  ├── application.load
  └── activity lifecycle spans

Changes required

1. New option
Add enableStandaloneAppStartTracing to SentryAndroidOptions (experimental, disabled by default).

2. Standalone transaction creation
New class (e.g., StandaloneAppStartStrategy) that:

  • Creates a transaction with op: app.start.cold / app.start.warm, name "App Start Cold" / "App Start Warm"
  • Passes AppStartMetrics data directly via TransactionOptions instead of relying on the global singleton
  • Finishes immediately (timing data is already fully collected)
  • Phase spans are direct children of the root (no intermediate grouping span)

3. ActivityLifecycleIntegration changes
When standalone mode is enabled:

  • Skip creating the appStartSpan child on the first Activity transaction (lines 256-268)
  • The ui.load transaction still gets created normally, just without app start data attached
  • Trigger the standalone transaction from onFirstFrameDrawn() when app start timing is finalized

4. PerformanceAndroidEventProcessor changes
When standalone mode is enabled:

  • The hasAppStartSpan() check naturally returns false (no app start span on the ui.load transaction), so measurements and child spans are not attached
  • For the standalone transaction, the processor needs to attach the child spans (process init, content providers, application.load) as direct children of the root instead of under a grouping span

5. Hybrid SDK guard
InternalSentrySdk.getAppStartMeasurement() is used by Flutter/RN to pull raw app start data. The standalone option should not activate when a hybrid SDK is consuming native app start data, to avoid duplicate transactions.

Key code locations

File Role
AppStartMetrics.java Singleton collecting all timing data via instrumentation hooks
ActivityLifecycleIntegration.java:160-313 Creates ui.load transaction + app.start.cold/warm child span
PerformanceAndroidEventProcessor.java:76-141 Adds measurements + child spans to transaction before sending
PerformanceAndroidEventProcessor.java:224-277 attachAppStartSpans() — adds process init, content provider, app.load spans
InternalSentrySdk.java:216-240 getAppStartMeasurement() — hybrid SDK bridge
SentryPerformanceProvider.java ContentProvider that captures process start time

Reference: Cocoa SDK implementation

A proof of concept has already been implemented in the Cocoa SDK: PR #7660 (tracking issue). The Java SDK's architecture (AppStartMetrics singleton + ActivityLifecycleIntegration attachment) maps directly onto the Cocoa approach (SentryAppStartMeasurementProvider + SentryTracer attachment), so the same pattern applies.

Follow-up work

  • App start sample rate: With standalone app start transactions, we know at transaction creation time that it's an app start, which makes it possible to set a different (e.g., 100%) sample rate for app start transactions specifically. This should be a follow-up issue.
  • Same trace ID: The standalone app start transaction and the first screen load (ui.load) transaction should share the same traceId so they appear linked in the trace view. The app start transaction should be created with a trace ID that the subsequent first ui.load transaction inherits.
  • Mobile Vitals dashboard: The sentry product's Mobile Vitals page hard-filters on transaction.op:[ui.load,navigation], which excludes app.start.* ops. Dashboard changes are needed — see the detailed analysis on the Cocoa issue.
  • Cross-platform alignment: Once validated, Flutter and React Native should also adopt standalone app start transactions with op: app.start.cold/warm (React Native already has a standalone mode but uses op: ui.load — see hybrid SDK analysis).

The specification was updated by @philipphofmann based on the Cocoa SDK implementation. If you have any clarifying questions, please ping him.

Metadata

Metadata

Assignees

Labels

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions