Status
Proof of concept shipped in #7660 (experimental, disabled by default). Before we can close this issue, we need to:
- Product validation: Verify that dashboards, mobile vitals, and insights handle standalone app start transactions correctly.
- Transaction naming: Align with the product on naming (
App Start Cold vs app_start_cold).
- Same TraceID: Link the standalone app start transaction and the first screen load transaction.
- Hybrid SDK compatibility: Validate interaction with hybrid SDK (React Native, Flutter, .NET, Unity) app start flows.
- Move the option out of
experimental (default in v10).
Currently, app start data is only captured when a ui.load transaction (UIViewController tracing) starts within 5 seconds of app start. If no qualifying transaction occurs, app start data is lost entirely.
Proposal
Instead of attaching app start spans and measurements to the first UIViewController 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, frame stats), and context (app.start_type) that are currently attached to the first ui.load transaction.
Transaction / Span Tree
Transaction: "app_start_cold" (op: app.start.cold)
├── "Pre Runtime Init" (op: app.start.cold)
├── "Runtime Init to Pre Main Initializers" (op: app.start.cold)
├── "UIKit Init" (op: app.start.cold)
├── "Application Init" (op: app.start.cold)
└── "Initial Frame Render" (op: app.start.cold)
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. In v10 this becomes the default behavior.
- Backwards compatibility: When the option is disabled, existing behavior is unchanged — app start data continues to be attached to the first UIViewController
ui.load transaction. When enabled, the first UIViewController 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 UIViewController transaction. App start data is only sent via the standalone transaction to avoid duplication.
- Hybrid SDKs: This change must account for hybrid SDKs (React Native, Flutter) that build on top of sentry-cocoa. These SDKs currently consume app start data from the native layer and attach it to their own transactions. Before implementing, we need to research and design how standalone app start transactions interact with the hybrid SDK app start flow to ensure it works correctly for both native and hybrid use cases.
- Product validation: We have confirmation that dashboards and product mostly rely on spans rather than the transaction itself, but we still need to verify end-to-end that nothing breaks when app start data arrives as its own transaction.
Follow-up work
- App start sample rate: After completing this issue, we should tackle #7235 — allowing users to set a different sample rate for app start transactions. With standalone app start transactions, we know at transaction creation time that it's an app start, which simplifies the sampling approach proposed in this comment. The implementation of this issue should ensure compatibility with that sampling approach.
- Cross-platform rollout: Once this concept is validated in the Cocoa SDK, we need to create corresponding issues for Android, Flutter, and React Native so they also implement standalone app start transactions and the specific sample rate.
- Documentation: After shipping this feature, we must update the App Start Tracing docs to reflect the new standalone transaction behavior and the
enableStandaloneAppStartTracing option.
- Same TraceID: The
appStartTransaction and the firstScreenLoadTransaction should ideally use the same traceID so they get linked in the product.
Hacked prototype (for reference only — not the target implementation)
This is a quick proof-of-concept that was used to validate the idea. It manually starts a transaction and creates child spans via the public SDK API. This is not how the actual implementation should work. The proper implementation should reuse the existing internal logic for building app start spans (e.g. sentryBuildAppStartSpans) and constructing transactions, rather than manually starting a transaction and creating spans through the public API.
In SentryAppStartTracker.buildAppStartMeasurement(_:), we replaced SentrySDKInternal.setAppStartMeasurement(appStartMeasurement) with self.createAppStartTransaction(appStartMeasurement), and added the following method:
private func createAppStartTransaction(_ measurement: SentryAppStartMeasurement) {
let operation: String
let measurementName: String
switch measurement.type {
case .cold:
operation = "app.start.cold"
measurementName = "app_start_cold"
case .warm:
operation = "app.start.warm"
measurementName = "app_start_warm"
default:
return
}
let tracer = SentrySDK.startTransaction(name: "AppStartTransaction", operation: operation)
tracer.startTimestamp = measurement.appStartTimestamp
let appStartEndTimestamp = measurement.appStartTimestamp.addingTimeInterval(measurement.duration)
if !measurement.isPreWarmed {
let preRuntimeInit = tracer.startChild(operation: operation, description: "Pre Runtime Init")
preRuntimeInit.finish()
preRuntimeInit.startTimestamp = measurement.appStartTimestamp
preRuntimeInit.timestamp = measurement.runtimeInitTimestamp
let runtimeInit = tracer.startChild(operation: operation, description: "Runtime Init to Pre Main Initializers")
runtimeInit.finish()
runtimeInit.startTimestamp = measurement.runtimeInitTimestamp
runtimeInit.timestamp = measurement.moduleInitializationTimestamp
}
let uiKitInit = tracer.startChild(operation: operation, description: "UIKit Init")
uiKitInit.finish()
uiKitInit.startTimestamp = measurement.moduleInitializationTimestamp
uiKitInit.timestamp = measurement.sdkStartTimestamp
let appInit = tracer.startChild(operation: operation, description: "Application Init")
appInit.finish()
appInit.startTimestamp = measurement.sdkStartTimestamp
appInit.timestamp = measurement.didFinishLaunchingTimestamp
let frameRender = tracer.startChild(operation: operation, description: "Initial Frame Render")
frameRender.finish()
frameRender.startTimestamp = measurement.didFinishLaunchingTimestamp
frameRender.timestamp = appStartEndTimestamp
tracer.setMeasurement(name: measurementName, value: NSNumber(value: measurement.duration * 1000))
tracer.finish()
}
Known limitations of this prototype:
- Missing
app.start_type context on the transaction (cold/warm/prewarmed)
- Missing debug meta images
- Uses default manual trace origin instead of
auto.app.start
- Does not clean up old app-start-on-ViewController code in
SentryTracer
- Does not handle hybrid SDK compatibility (
onAppStartMeasurementAvailable callback)
- No unit tests
Related Java issue getsentry/sentry-java#5046
The specification was updated by @philipphofmann, so if you have any clarifying questions, please ping him.
Status
Proof of concept shipped in #7660 (experimental, disabled by default). Before we can close this issue, we need to:
App Start Coldvsapp_start_cold).experimental(default in v10).Currently, app start data is only captured when a
ui.loadtransaction (UIViewController tracing) starts within 5 seconds of app start. If no qualifying transaction occurs, app start data is lost entirely.Proposal
Instead of attaching app start spans and measurements to the first UIViewController 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, frame stats), and context (app.start_type) that are currently attached to the firstui.loadtransaction.Transaction / Span Tree
Details
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.app.start.coldorapp.start.warm, matching the start type.enableStandaloneAppStartTracing, disabled by default. In v10 this becomes the default behavior.ui.loadtransaction. When enabled, the first UIViewController transaction still works correctly on its own, just without the app start spans/measurements attached.Follow-up work
enableStandaloneAppStartTracingoption.appStartTransactionand thefirstScreenLoadTransactionshould ideally use the sametraceIDso they get linked in the product.Hacked prototype (for reference only — not the target implementation)
This is a quick proof-of-concept that was used to validate the idea. It manually starts a transaction and creates child spans via the public SDK API. This is not how the actual implementation should work. The proper implementation should reuse the existing internal logic for building app start spans (e.g.
sentryBuildAppStartSpans) and constructing transactions, rather than manually starting a transaction and creating spans through the public API.In
SentryAppStartTracker.buildAppStartMeasurement(_:), we replacedSentrySDKInternal.setAppStartMeasurement(appStartMeasurement)withself.createAppStartTransaction(appStartMeasurement), and added the following method:Known limitations of this prototype:
app.start_typecontext on the transaction (cold/warm/prewarmed)auto.app.startSentryTraceronAppStartMeasurementAvailablecallback)Related Java issue getsentry/sentry-java#5046
The specification was updated by @philipphofmann, so if you have any clarifying questions, please ping him.