getsentry / getsentry/sentry-dotnet
Implement App Start metrics on iOS
- Dominant language
- C#
- Stars
- 770
- Forks
- 248
- Avg merge
- 3d 4h
- Merged PRs (30d)
- 49
Description
# Description
Part of
* [NET-185](https://linear.app/getsentry/issue/NET-185/mobile-vitals)
See also
* [NET-638](https://linear.app/getsentry/issue/NET-638/implement-app-start-metrics-on-android)
The same concept as Android applies — measure from the earliest native process initialization to first frame rendered. The overall span/transaction structure is identical (`ui.load` transaction, `app.start.cold` / `app.start.warm` child span, measurements). See [app-starts-android.md]() for the shared concepts.
iOS has one additional wrinkle: **pre-warmed starts**. Since iOS 15, the OS can pre-initialize the app process before the user taps the icon, making the measured start appear artificially short. The Cocoa SDK detects this and handles it as a distinct case.
### References
* [Mobile Vitals product docs]()
* [App Starts SDK spec]()
* [React Native initial implementation (PR #1704)]()
* [React Native refactor to standalone integration (PR #3852)]()
* [sentry-dotnet tracking issue]()
---
## How React Native Solves It on iOS
The flow mirrors Android — RN reads timing data from the native Cocoa SDK once and attaches it to the first navigation transaction. The differences are in the data shape and available sub-spans.
### Data Access
RN calls `PrivateSentrySDKOnly.appStartMeasurementWithSpans` (an internal Cocoa SDK API designed specifically for hybrid SDKs). This returns a dictionary like:
```objc
@{
@"type": @"cold" | @"warm" | @"unknown",
@"is_pre_warmed": @YES | @NO,
@"app_start_timestamp_ms": ,
@"runtime_init_timestamp_ms": ,
@"module_initialization_timestamp_ms": ,
@"sdk_start_timestamp_ms": ,
@"spans": @[
@{ @"description": @"...", @"start_timestamp_ms": ..., @"end_timestamp_ms": ... },
...
]
}
```
### iOS Native Sub-Spans
The Cocoa SDK's sub-spans differ from Android and vary based on whether the start was pre-warmed. All sub-spans use the same op as their parent (`app.start.cold` / `app.start.warm`).
**Non-pre-warmed cold start:**
| Description | Timing |
| -- | -- |
| `"Pre Runtime Init"` | App start timestamp → runtime init timestamp |
| `"Runtime Init to Pre Main Initializers"` | Runtime init → module initialization timestamp |
| `"UIKit Init"` | Module initialization → SDK start timestamp |
| `"Application Init"` | SDK start → `didFinishLaunching` timestamp |
| `"Initial Frame Render"` | `didFinishLaunching` → app start end |
**Pre-warmed start (only one sub-span):**
| Description | Timing |
| -- | -- |
| `"UIKit Init"` | Module initialization → SDK start timestamp |
### The Hybrid SDK Limitation
When the Cocoa SDK is initialized by a hybrid SDK (like RN or our .NET SDK), it **misses the** `didFinishLaunchNotification` because the hybrid layer initializes after that point. This means:
* `"Application Init"` and `"Initial Frame Render"` sub-spans are **not available** to hybrid SDKs
* The data returned via `appStartMeasurementWithSpans` only includes the spans up to SDK start time
This is a known limitation and accepted as-is — hybrid SDKs get fewer sub-spans than native-only iOS apps.
### Prerequisite for .NET
The Cocoa bindings in `ApiDefinitions.cs` do **not** currently expose `appStartMeasurementWithSpans` or `appStartMeasurement`. To implement this for iOS, these would need to be added (e.g. via `scripts/patch-cocoa-bindings.cs`).
Contributor guide
Assessment
This issue has not been assessed yet.