Desrciption
Part of
What Is TTID?
TTID measures the time from when a new screen begins loading until the screen draws its first frame. It gives a more accurate picture of screen creation time than raw transaction timing.
- Span op:
ui.load.initial_display
- Measurement key:
time_to_initial_display (milliseconds)
- The span is a child of the
ui.load transaction (not a separate transaction, so it doesn't count against quota)
- Status must be
ok for the measurement to be recorded — if the span finishes with any other status the measurement is omitted
- The span starts when the screen begins loading. During app startup this is adjusted back to the native app start timestamp (same alignment as App Starts)
- The span ends automatically when the first frame is drawn — no manual intervention needed
References
How React Native Implements TTID on iOS
Like Android, RN has two parallel native paths plus a JS fallback. All paths store a timestamp in a shared static map (RNSentryTimeToDisplay) keyed by span ID. The JS layer reads this map after the transaction ends and constructs the TTID span.
The iOS implementation delegates entirely to the Cocoa SDK's SentryFramesTracker (backed by CADisplayLink) rather than implementing its own frame detection.
Path A — View-based (RNSentryOnDrawReporter)
A special view component (<RNSentryOnDrawReporter initialDisplay parentSpanId={...} />) is embedded in the screen's component tree. When rendered with initialDisplay=true:
- A
RNSentryFramesTrackerListener is attached to SentryFramesTracker from the Cocoa SDK
SentryFramesTracker uses a CADisplayLink internally. On the next frame callback it calls framesTrackerHasNewFrame:(NSDate *)newFrameDate
- The listener removes itself immediately (one-shot), then stores
[newFrameDate timeIntervalSince1970] in the map under "ttid-{parentSpanId}"
Path B — Navigation integration (swizzleViewDidAppear + RNSentryFramesTrackerListener)
Activated when enableTimeToInitialDisplay=true in the React Navigation integration:
- At init time,
[RNSentryRNSScreen swizzleViewDidAppear] is called on the main thread — this swizzles viewDidAppear on react-native-screens screen classes
- A
RNSentryFramesTrackerListener is created with a callback that calls [RNSentryTimeToDisplay putTimeToInitialDisplayForActiveSpan:timestamp]
- JS calls
NATIVE.setActiveSpanId(spanId) to register the current span
- On
viewDidAppear, the listener starts listening on SentryFramesTracker
- On the next frame, the timestamp is stored under
"ttid-navigation-{activeSpanId}"
Fallback — CADisplayLink bridge (getNewScreenTimeToDisplay)
If neither native path produces a timestamp in time:
- Creates a one-shot
CADisplayLink targeting a handleDisplayLink: selector
- Added to
NSRunLoop.mainRunLoop with NSRunLoopCommonModes
- On the next display link callback, captures
[NSDate date] timeIntervalSince1970 and resolves the promise, then invalidates the CADisplayLink
- Only available on
TARGET_OS_IOS — returns empty on macOS / Catalyst
JS-Side Span Construction
Identical to Android — after the transaction ends, processEvent calls NATIVE.popTimeToDisplayFor(...), constructs the span, and sets the time_to_initial_display measurement. See the Android notes for the shared JS flow.
App Start Timestamp Alignment
Same as Android — both the transaction's start_timestamp and the TTID span's start_timestamp are overwritten with the native app start timestamp, and the measurement is recalculated.
Key iOS API: SentryFramesTracker / CADisplayLink
The Cocoa SDK's SentryFramesTracker is the frame detection mechanism on iOS. It runs a CADisplayLink that fires on every rendered frame. RN taps into this via PrivateSentrySDKOnly — the same internal API used for slow/frozen frames and App Start data.
SentryFramesTracker must be running for any of this to work. In the Cocoa SDK this is controlled by enableAutoPerformanceTracing.
Quirks and Limitations
SentryFramesTracker must be active — if enableAutoPerformanceTracing is not set in the Cocoa SDK, all paths fail silently and no TTID is reported
- The
CADisplayLink fallback is iOS-only (TARGET_OS_IOS) — it returns empty on Mac Catalyst
swizzleViewDidAppear requires react-native-screens — without it the navigation integration path is unavailable
activeSpanId is a single global — same potential span ID mismatch risk as Android (mitigated by keyed map)
- A 30-second timeout in the JS layer bounds how long it waits for a native timestamp
Implications for .NET/MAUI (iOS)
- The core mechanism is
SentryFramesTracker / CADisplayLink via PrivateSentrySDKOnly. The Cocoa bindings need to expose the relevant API — specifically SentryFramesTracker or a listener protocol (SentryFramesTrackerListener)
PrivateSentrySDKOnly.isFramesTrackingRunning and currentScreenFrames already appear in ApiDefinitions.cs (used for slow/frozen frames), but the listener protocol for one-shot frame callbacks likely needs adding
- The equivalent of
viewDidAppear in MAUI is a page's OnAppearing lifecycle event — this is a natural hook point for starting the frame listener
- The Cocoa binding additions would need to go through
scripts/patch-cocoa-bindings.cs since ApiDefinitions.cs is auto-generated
enableAutoPerformanceTracing must be set on the Cocoa SDK options for SentryFramesTracker to run
Desrciption
Part of
What Is TTID?
TTID measures the time from when a new screen begins loading until the screen draws its first frame. It gives a more accurate picture of screen creation time than raw transaction timing.
ui.load.initial_displaytime_to_initial_display(milliseconds)ui.loadtransaction (not a separate transaction, so it doesn't count against quota)okfor the measurement to be recorded — if the span finishes with any other status the measurement is omittedReferences
How React Native Implements TTID on iOS
Like Android, RN has two parallel native paths plus a JS fallback. All paths store a timestamp in a shared static map (
RNSentryTimeToDisplay) keyed by span ID. The JS layer reads this map after the transaction ends and constructs the TTID span.The iOS implementation delegates entirely to the Cocoa SDK's
SentryFramesTracker(backed byCADisplayLink) rather than implementing its own frame detection.Path A — View-based (
RNSentryOnDrawReporter)A special view component (
<RNSentryOnDrawReporter initialDisplay parentSpanId={...} />) is embedded in the screen's component tree. When rendered withinitialDisplay=true:RNSentryFramesTrackerListeneris attached toSentryFramesTrackerfrom the Cocoa SDKSentryFramesTrackeruses aCADisplayLinkinternally. On the next frame callback it callsframesTrackerHasNewFrame:(NSDate *)newFrameDate[newFrameDate timeIntervalSince1970]in the map under"ttid-{parentSpanId}"Path B — Navigation integration (
swizzleViewDidAppear+RNSentryFramesTrackerListener)Activated when
enableTimeToInitialDisplay=truein the React Navigation integration:[RNSentryRNSScreen swizzleViewDidAppear]is called on the main thread — this swizzlesviewDidAppearonreact-native-screensscreen classesRNSentryFramesTrackerListeneris created with a callback that calls[RNSentryTimeToDisplay putTimeToInitialDisplayForActiveSpan:timestamp]NATIVE.setActiveSpanId(spanId)to register the current spanviewDidAppear, the listener starts listening onSentryFramesTracker"ttid-navigation-{activeSpanId}"Fallback —
CADisplayLinkbridge (getNewScreenTimeToDisplay)If neither native path produces a timestamp in time:
CADisplayLinktargeting ahandleDisplayLink:selectorNSRunLoop.mainRunLoopwithNSRunLoopCommonModes[NSDate date] timeIntervalSince1970and resolves the promise, then invalidates theCADisplayLinkTARGET_OS_IOS— returns empty on macOS / CatalystJS-Side Span Construction
Identical to Android — after the transaction ends,
processEventcallsNATIVE.popTimeToDisplayFor(...), constructs the span, and sets thetime_to_initial_displaymeasurement. See the Android notes for the shared JS flow.App Start Timestamp Alignment
Same as Android — both the transaction's
start_timestampand the TTID span'sstart_timestampare overwritten with the native app start timestamp, and the measurement is recalculated.Key iOS API:
SentryFramesTracker/CADisplayLinkThe Cocoa SDK's
SentryFramesTrackeris the frame detection mechanism on iOS. It runs aCADisplayLinkthat fires on every rendered frame. RN taps into this viaPrivateSentrySDKOnly— the same internal API used for slow/frozen frames and App Start data.SentryFramesTrackermust be running for any of this to work. In the Cocoa SDK this is controlled byenableAutoPerformanceTracing.Quirks and Limitations
SentryFramesTrackermust be active — ifenableAutoPerformanceTracingis not set in the Cocoa SDK, all paths fail silently and no TTID is reportedCADisplayLinkfallback is iOS-only (TARGET_OS_IOS) — it returns empty on Mac CatalystswizzleViewDidAppearrequiresreact-native-screens— without it the navigation integration path is unavailableactiveSpanIdis a single global — same potential span ID mismatch risk as Android (mitigated by keyed map)Implications for .NET/MAUI (iOS)
SentryFramesTracker/CADisplayLinkviaPrivateSentrySDKOnly. The Cocoa bindings need to expose the relevant API — specificallySentryFramesTrackeror a listener protocol (SentryFramesTrackerListener)PrivateSentrySDKOnly.isFramesTrackingRunningandcurrentScreenFramesalready appear inApiDefinitions.cs(used for slow/frozen frames), but the listener protocol for one-shot frame callbacks likely needs addingviewDidAppearin MAUI is a page'sOnAppearinglifecycle event — this is a natural hook point for starting the frame listenerscripts/patch-cocoa-bindings.cssinceApiDefinitions.csis auto-generatedenableAutoPerformanceTracingmust be set on the Cocoa SDK options forSentryFramesTrackerto run