1- import type { Span , StartSpanOptions } from '@sentry/core' ;
1+ /* eslint-disable max-lines */
2+ import type { Span , StartSpanOptions } from '@sentry/core' ;
23import { fill , getActiveSpan , getSpanDescendants , logger , SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN , SPAN_STATUS_ERROR , SPAN_STATUS_OK , spanToJSON , startInactiveSpan } from '@sentry/core' ;
34import * as React from 'react' ;
45import { useState } from 'react' ;
56
7+ import type { NativeFramesResponse } from '../NativeRNSentry' ;
68import { isTurboModuleEnabled } from '../utils/environment' ;
9+ import { NATIVE } from '../wrapper' ;
710import { SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY , SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY } from './origin' ;
811import { getRNSentryOnDrawReporter , nativeComponentExists } from './timetodisplaynative' ;
912import { setSpanDurationAsMeasurement , setSpanDurationAsMeasurementOnSpan } from './utils' ;
1013
1114let nativeComponentMissingLogged = false ;
1215
16+ /**
17+ * Timeout for fetching native frames
18+ */
19+ const FETCH_FRAMES_TIMEOUT_MS = 2_000 ;
20+
1321/**
1422 * Flags of active spans with manual initial display.
1523 */
@@ -20,6 +28,13 @@ export const manualInitialDisplaySpans = new WeakMap<Span, true>();
2028 */
2129const fullDisplayBeforeInitialDisplay = new WeakMap < Span , true > ( ) ;
2230
31+ interface FrameDataForSpan {
32+ startFrames : NativeFramesResponse | null ;
33+ endFrames : NativeFramesResponse | null ;
34+ }
35+
36+ const spanFrameDataMap = new Map < string , FrameDataForSpan > ( ) ;
37+
2338export type TimeToDisplayProps = {
2439 children ?: React . ReactNode ;
2540 record ?: boolean ;
@@ -106,7 +121,7 @@ export function startTimeToInitialDisplaySpan(
106121 const existingSpan = getSpanDescendants ( activeSpan ) . find ( ( span ) => spanToJSON ( span ) . op === 'ui.load.initial_display' ) ;
107122 if ( existingSpan ) {
108123 logger . debug ( `[TimeToDisplay] Found existing ui.load.initial_display span.` ) ;
109- return existingSpan
124+ return existingSpan ;
110125 }
111126
112127 const initialDisplaySpan = startInactiveSpan ( {
@@ -120,6 +135,10 @@ export function startTimeToInitialDisplaySpan(
120135 return undefined ;
121136 }
122137
138+ captureStartFramesForSpan ( initialDisplaySpan . spanContext ( ) . spanId ) . catch ( ( error ) => {
139+ logger . debug ( `[TimeToDisplay] Failed to capture start frames for initial display span (${ initialDisplaySpan . spanContext ( ) . spanId } ).` , error ) ;
140+ } ) ;
141+
123142 if ( options ?. isAutoInstrumented ) {
124143 initialDisplaySpan . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN , SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY ) ;
125144 } else {
@@ -176,13 +195,26 @@ export function startTimeToFullDisplaySpan(
176195 return undefined ;
177196 }
178197
198+ captureStartFramesForSpan ( fullDisplaySpan . spanContext ( ) . spanId ) . catch ( ( error ) => {
199+ logger . debug ( `[TimeToDisplay] Failed to capture start frames for full display span(${ fullDisplaySpan . spanContext ( ) . spanId } ).` , error ) ;
200+ } ) ;
201+
179202 const timeout = setTimeout ( ( ) => {
180203 if ( spanToJSON ( fullDisplaySpan ) . timestamp ) {
181204 return ;
182205 }
183206 fullDisplaySpan . setStatus ( { code : SPAN_STATUS_ERROR , message : 'deadline_exceeded' } ) ;
184- fullDisplaySpan . end ( spanToJSON ( initialDisplaySpan ) . timestamp ) ;
185- setSpanDurationAsMeasurement ( 'time_to_full_display' , fullDisplaySpan ) ;
207+
208+ captureEndFramesAndAttachToSpan ( fullDisplaySpan ) . then ( ( ) => {
209+ logger . debug ( `[TimeToDisplay] ${ fullDisplaySpan . spanContext ( ) . spanId } span updated with frame data.` ) ;
210+ fullDisplaySpan . end ( spanToJSON ( initialDisplaySpan ) . timestamp ) ;
211+ setSpanDurationAsMeasurement ( 'time_to_full_display' , fullDisplaySpan ) ;
212+ } ) . catch ( ( ) => {
213+ logger . warn ( `[TimeToDisplay] Failed to capture end frames for full display span (${ fullDisplaySpan . spanContext ( ) . spanId } ).` ) ;
214+ fullDisplaySpan . end ( spanToJSON ( initialDisplaySpan ) . timestamp ) ;
215+ setSpanDurationAsMeasurement ( 'time_to_full_display' , fullDisplaySpan ) ;
216+ } ) ;
217+
186218 logger . warn ( `[TimeToDisplay] Full display span deadline_exceeded.` ) ;
187219 } , options . timeoutMs ) ;
188220
@@ -235,17 +267,31 @@ export function updateInitialDisplaySpan(
235267 return ;
236268 }
237269
238- span . end ( frameTimestampSeconds ) ;
239- span . setStatus ( { code : SPAN_STATUS_OK } ) ;
240- logger . debug ( `[TimeToDisplay] ${ spanToJSON ( span ) . description } span updated with end timestamp.` ) ;
270+ captureEndFramesAndAttachToSpan ( span ) . then ( ( ) => {
271+ span . end ( frameTimestampSeconds ) ;
272+ span . setStatus ( { code : SPAN_STATUS_OK } ) ;
273+ logger . debug ( `[TimeToDisplay] ${ spanToJSON ( span ) . description } span updated with end timestamp and frame data.` ) ;
241274
242- if ( fullDisplayBeforeInitialDisplay . has ( activeSpan ) ) {
243- fullDisplayBeforeInitialDisplay . delete ( activeSpan ) ;
244- logger . debug ( `[TimeToDisplay] Updating full display with initial display (${ span . spanContext ( ) . spanId } ) end.` ) ;
245- updateFullDisplaySpan ( frameTimestampSeconds , span ) ;
246- }
275+ if ( fullDisplayBeforeInitialDisplay . has ( activeSpan ) ) {
276+ fullDisplayBeforeInitialDisplay . delete ( activeSpan ) ;
277+ logger . debug ( `[TimeToDisplay] Updating full display with initial display (${ span . spanContext ( ) . spanId } ) end.` ) ;
278+ updateFullDisplaySpan ( frameTimestampSeconds , span ) ;
279+ }
247280
248- setSpanDurationAsMeasurementOnSpan ( 'time_to_initial_display' , span , activeSpan ) ;
281+ setSpanDurationAsMeasurementOnSpan ( 'time_to_initial_display' , span , activeSpan ) ;
282+ } ) . catch ( ( error ) => {
283+ logger . debug ( `[TimeToDisplay] Failed to capture frame data for initial display span.` , error ) ;
284+ span . end ( frameTimestampSeconds ) ;
285+ span . setStatus ( { code : SPAN_STATUS_OK } ) ;
286+
287+ if ( fullDisplayBeforeInitialDisplay . has ( activeSpan ) ) {
288+ fullDisplayBeforeInitialDisplay . delete ( activeSpan ) ;
289+ logger . debug ( `[TimeToDisplay] Updating full display with initial display (${ span . spanContext ( ) . spanId } ) end.` ) ;
290+ updateFullDisplaySpan ( frameTimestampSeconds , span ) ;
291+ }
292+
293+ setSpanDurationAsMeasurementOnSpan ( 'time_to_initial_display' , span , activeSpan ) ;
294+ } ) ;
249295}
250296
251297function updateFullDisplaySpan ( frameTimestampSeconds : number , passedInitialDisplaySpan ?: Span ) : void {
@@ -278,17 +324,26 @@ function updateFullDisplaySpan(frameTimestampSeconds: number, passedInitialDispl
278324 return ;
279325 }
280326
281- if ( initialDisplayEndTimestamp > frameTimestampSeconds ) {
282- logger . warn ( `[TimeToDisplay] Using initial display end. Full display end frame timestamp is before initial display end.` ) ;
283- span . end ( initialDisplayEndTimestamp ) ;
284- } else {
285- span . end ( frameTimestampSeconds ) ;
286- }
327+ captureEndFramesAndAttachToSpan ( span ) . then ( ( ) => {
328+ const endTimestamp = initialDisplayEndTimestamp > frameTimestampSeconds ? initialDisplayEndTimestamp : frameTimestampSeconds ;
287329
288- span . setStatus ( { code : SPAN_STATUS_OK } ) ;
289- logger . debug ( `[TimeToDisplay] ${ spanJSON . description } (${ spanJSON . span_id } ) span updated with end timestamp.` ) ;
330+ if ( initialDisplayEndTimestamp > frameTimestampSeconds ) {
331+ logger . warn ( `[TimeToDisplay] Using initial display end. Full display end frame timestamp is before initial display end.` ) ;
332+ }
290333
291- setSpanDurationAsMeasurement ( 'time_to_full_display' , span ) ;
334+ span . end ( endTimestamp ) ;
335+ span . setStatus ( { code : SPAN_STATUS_OK } ) ;
336+ logger . debug ( `[TimeToDisplay] ${ spanJSON . description } (${ spanJSON . span_id } ) span updated with end timestamp and frame data.` ) ;
337+
338+ setSpanDurationAsMeasurement ( 'time_to_full_display' , span ) ;
339+ } ) . catch ( ( error ) => {
340+ logger . debug ( `[TimeToDisplay] Failed to capture frame data for full display span.` , error ) ;
341+ const endTimestamp = initialDisplayEndTimestamp > frameTimestampSeconds ? initialDisplayEndTimestamp : frameTimestampSeconds ;
342+
343+ span . end ( endTimestamp ) ;
344+ span . setStatus ( { code : SPAN_STATUS_OK } ) ;
345+ setSpanDurationAsMeasurement ( 'time_to_full_display' , span ) ;
346+ } ) ;
292347}
293348
294349/**
@@ -342,3 +397,103 @@ function createTimeToDisplay({
342397 TimeToDisplayWrapper . displayName = `TimeToDisplayWrapper` ;
343398 return TimeToDisplayWrapper ;
344399}
400+
401+ /**
402+ * Attaches frame data to a span's data object.
403+ */
404+ function attachFrameDataToSpan ( span : Span , startFrames : NativeFramesResponse , endFrames : NativeFramesResponse ) : void {
405+ const totalFrames = endFrames . totalFrames - startFrames . totalFrames ;
406+ const slowFrames = endFrames . slowFrames - startFrames . slowFrames ;
407+ const frozenFrames = endFrames . frozenFrames - startFrames . frozenFrames ;
408+
409+ if ( totalFrames <= 0 && slowFrames <= 0 && frozenFrames <= 0 ) {
410+ logger . warn ( `[TimeToDisplay] Detected zero slow or frozen frames. Not adding measurements to span (${ span . spanContext ( ) . spanId } ).` ) ;
411+ return ;
412+ }
413+ span . setAttribute ( 'frames.total' , totalFrames ) ;
414+ span . setAttribute ( 'frames.slow' , slowFrames ) ;
415+ span . setAttribute ( 'frames.frozen' , frozenFrames ) ;
416+
417+ logger . debug ( '[TimeToDisplay] Attached frame data to span.' , {
418+ spanId : span . spanContext ( ) . spanId ,
419+ frameData : {
420+ total : totalFrames ,
421+ slow : slowFrames ,
422+ frozen : frozenFrames ,
423+ } ,
424+ } ) ;
425+ }
426+
427+ /**
428+ * Captures start frames for a time-to-display span
429+ */
430+ async function captureStartFramesForSpan ( spanId : string ) : Promise < void > {
431+ if ( ! NATIVE . enableNative ) {
432+ return ;
433+ }
434+
435+ try {
436+ const startFrames = await fetchNativeFramesWithTimeout ( ) ;
437+ if ( ! spanFrameDataMap . has ( spanId ) ) {
438+ spanFrameDataMap . set ( spanId , { startFrames : null , endFrames : null } ) ;
439+ }
440+ const frameData = spanFrameDataMap . get ( spanId ) ! ;
441+ frameData . startFrames = startFrames ;
442+ logger . debug ( `[TimeToDisplay] Captured start frames for span ${ spanId } .` , startFrames ) ;
443+ } catch ( error ) {
444+ logger . debug ( `[TimeToDisplay] Failed to capture start frames for span ${ spanId } .` , error ) ;
445+ }
446+ }
447+
448+ /**
449+ * Captures end frames and attaches frame data to span
450+ */
451+ async function captureEndFramesAndAttachToSpan ( span : Span ) : Promise < void > {
452+ if ( ! NATIVE . enableNative ) {
453+ return ;
454+ }
455+
456+ const spanId = span . spanContext ( ) . spanId ;
457+ const frameData = spanFrameDataMap . get ( spanId ) ;
458+
459+ if ( ! frameData ?. startFrames ) {
460+ logger . debug ( `[TimeToDisplay] No start frames found for span ${ spanId } , skipping frame data collection.` ) ;
461+ return ;
462+ }
463+
464+ try {
465+ const endFrames = await fetchNativeFramesWithTimeout ( ) ;
466+ frameData . endFrames = endFrames ;
467+
468+ attachFrameDataToSpan ( span , frameData . startFrames , endFrames ) ;
469+
470+ logger . debug ( `[TimeToDisplay] Captured and attached end frames for span ${ spanId } .` , endFrames ) ;
471+ } catch ( error ) {
472+ logger . debug ( `[TimeToDisplay] Failed to capture end frames for span ${ spanId } .` , error ) ;
473+ } finally {
474+ spanFrameDataMap . delete ( spanId ) ;
475+ }
476+ }
477+
478+ /**
479+ * Fetches native frames with a timeout
480+ */
481+ function fetchNativeFramesWithTimeout ( ) : Promise < NativeFramesResponse > {
482+ return new Promise < NativeFramesResponse > ( ( resolve , reject ) => {
483+ NATIVE . fetchNativeFrames ( )
484+ . then ( value => {
485+ if ( ! value ) {
486+ reject ( 'Native frames response is null.' ) ;
487+ return ;
488+ }
489+ resolve ( value ) ;
490+ } )
491+ . then ( undefined , ( error : unknown ) => {
492+ reject ( error ) ;
493+ } ) ;
494+
495+ setTimeout ( ( ) => {
496+ reject ( 'Fetching native frames took too long. Dropping frames.' ) ;
497+ } , FETCH_FRAMES_TIMEOUT_MS ) ;
498+ } ) ;
499+ }
0 commit comments