-
-
Notifications
You must be signed in to change notification settings - Fork 468
feat: Send standalone app start transactions #5046
Description
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 ColdorApp 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.coldorapp.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.loadtransaction. 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.enableStandaloneAppStartTracingshould 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
AppStartMetricsdata directly viaTransactionOptionsinstead 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
appStartSpanchild on the first Activity transaction (lines 256-268) - The
ui.loadtransaction 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 theui.loadtransaction), 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 sametraceIdso they appear linked in the trace view. The app start transaction should be created with a trace ID that the subsequent firstui.loadtransaction inherits. - Mobile Vitals dashboard: The sentry product's Mobile Vitals page hard-filters on
transaction.op:[ui.load,navigation], which excludesapp.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 usesop: 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.