Skip to content

feat: Send standalone app start transactions #6883

@noahsmartin

Description

@noahsmartin

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.

Metadata

Metadata

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