1- import type { Integration , SpanJSON } from '@sentry/core' ;
1+ import type { Event , Integration , SpanJSON } from '@sentry/core' ;
22import { logger } from '@sentry/core' ;
3+
34import { NATIVE } from '../../wrapper' ;
4- import { createSpanJSON } from '../utils ' ;
5+ import { UI_LOAD_FULL_DISPLAY , UI_LOAD_INITIAL_DISPLAY } from '../ops ' ;
56import { SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY } from '../origin' ;
6- import { UI_LOAD_INITIAL_DISPLAY , UI_LOAD_FULL_DISPLAY } from '../ops' ;
7+ import { SPAN_THREAD_NAME , SPAN_THREAD_NAME_JAVASCRIPT } from '../span' ;
8+ import { createSpanJSON } from '../utils' ;
79export const INTEGRATION_NAME = 'TimeToDisplay' ;
810
11+ const TIME_TO_DISPLAY_TIMEOUT_MS = 30_000 ;
12+ const isDeadlineExceeded = ( durationMs : number ) : boolean => durationMs > TIME_TO_DISPLAY_TIMEOUT_MS ;
13+
914export const timeToDisplayIntegration = ( ) : Integration => {
1015 return {
1116 name : INTEGRATION_NAME ,
@@ -29,54 +34,27 @@ export const timeToDisplayIntegration = (): Integration => {
2934 }
3035
3136 event . spans = event . spans || [ ] ;
37+ event . measurements = event . measurements || { } ;
3238
33- const ttidEndTimestampSeconds = await NATIVE . popTimeToDisplayFor ( `ttid-${ rootSpanId } ` ) ;
34- let ttidSpan : SpanJSON | undefined = event . spans ?. find ( span => span . op === UI_LOAD_INITIAL_DISPLAY ) ;
35- if ( ttidEndTimestampSeconds ) {
36- if ( ttidSpan && ttidSpan . status && ttidSpan . status !== 'ok' ) {
37- ttidSpan . status = 'ok' ;
38- ttidSpan . timestamp = ttidEndTimestampSeconds ;
39- logger . debug ( `[${ INTEGRATION_NAME } ] Updated existing ttid span.` , ttidSpan ) ;
40- } else {
41- ttidSpan = createSpanJSON ( {
42- op : UI_LOAD_INITIAL_DISPLAY ,
43- description : 'NEW Time To Initial Display' ,
44- start_timestamp : transactionStartTimestampSeconds ,
45- timestamp : ttidEndTimestampSeconds ,
46- origin : SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY ,
47- parent_span_id : rootSpanId ,
48- // TODO: Add data
49- } ) ;
50- logger . debug ( `[${ INTEGRATION_NAME } ] Added ttid span to transaction.` , ttidSpan ) ;
51- event . spans ?. push ( ttidSpan ) ;
52- }
39+ const ttidSpan = await addTimeToInitialDisplay ( { event, rootSpanId, transactionStartTimestampSeconds } ) ;
40+ const ttfdSpan = await addTimeToFullDisplay ( { event, rootSpanId, transactionStartTimestampSeconds, ttidSpan } ) ;
41+
42+ if ( ttidSpan && ttidSpan . start_timestamp && ttidSpan . timestamp ) {
43+ event . measurements [ 'time_to_initial_display' ] = {
44+ value : ( ttidSpan . timestamp - ttidSpan . start_timestamp ) * 1000 ,
45+ unit : 'millisecond' ,
46+ } ;
5347 }
5448
55- // TODO: Should we trim it to 30s a.k.a max timeout?
56- const ttfdEndTimestampSeconds = await NATIVE . popTimeToDisplayFor ( `ttfd-${ rootSpanId } ` ) ;
57- let ttfdSpan : SpanJSON | undefined ;
58- if ( ttfdEndTimestampSeconds && ttidSpan ) {
59- ttfdSpan = event . spans ?. find ( span => span . op === UI_LOAD_FULL_DISPLAY ) ;
60- const ttfdAdjustedEndTimestampSeconds =
61- ttidSpan ?. timestamp && ttfdEndTimestampSeconds < ttidSpan . timestamp
62- ? ttidSpan . timestamp
63- : ttfdEndTimestampSeconds ;
64- if ( ttfdSpan && ttfdSpan . status && ttfdSpan . status !== 'ok' ) {
65- ttfdSpan . status = 'ok' ;
66- ttfdSpan . timestamp = ttfdAdjustedEndTimestampSeconds ;
67- logger . debug ( `[${ INTEGRATION_NAME } ] Updated existing ttfd span.` , ttfdSpan ) ;
49+ if ( ttfdSpan && ttfdSpan . start_timestamp && ttfdSpan . timestamp ) {
50+ const durationMs = ( ttfdSpan . timestamp - ttfdSpan . start_timestamp ) * 1000 ;
51+ if ( isDeadlineExceeded ( durationMs ) ) {
52+ event . measurements [ 'time_to_full_display' ] = event . measurements [ 'time_to_initial_display' ] ;
6853 } else {
69- ttfdSpan = createSpanJSON ( {
70- op : UI_LOAD_FULL_DISPLAY ,
71- description : 'Time To Full Display' ,
72- start_timestamp : transactionStartTimestampSeconds ,
73- timestamp : ttfdAdjustedEndTimestampSeconds ,
74- origin : SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY ,
75- parent_span_id : rootSpanId ,
76- // TODO: Add data
77- } ) ;
78- logger . debug ( `[${ INTEGRATION_NAME } ] Added ttfd span to transaction.` , ttfdSpan ) ;
79- event . spans ?. push ( ttfdSpan ) ;
54+ event . measurements [ 'time_to_full_display' ] = {
55+ value : durationMs ,
56+ unit : 'millisecond' ,
57+ } ;
8058 }
8159 }
8260
@@ -93,3 +71,100 @@ export const timeToDisplayIntegration = (): Integration => {
9371 } ,
9472 } ;
9573} ;
74+
75+ async function addTimeToInitialDisplay ( {
76+ event,
77+ rootSpanId,
78+ transactionStartTimestampSeconds,
79+ } : {
80+ event : Event ;
81+ rootSpanId : string ;
82+ transactionStartTimestampSeconds : number ;
83+ } ) : Promise < SpanJSON | undefined > {
84+ const ttidEndTimestampSeconds = await NATIVE . popTimeToDisplayFor ( `ttid-${ rootSpanId } ` ) ;
85+
86+ let ttidSpan : SpanJSON | undefined = event . spans ?. find ( span => span . op === UI_LOAD_INITIAL_DISPLAY ) ;
87+
88+ if ( ttidSpan && ( ttidSpan . status === undefined || ttidSpan . status === 'ok' ) && ! ttidEndTimestampSeconds ) {
89+ logger . debug ( `[${ INTEGRATION_NAME } ] Ttid span already exists and is ok.` , ttidSpan ) ;
90+ return ttidSpan ;
91+ }
92+
93+ if ( ! ttidEndTimestampSeconds ) {
94+ logger . debug ( `[${ INTEGRATION_NAME } ] No ttid end timestamp found for span ${ rootSpanId } .` ) ;
95+ return undefined ;
96+ }
97+
98+ if ( ttidSpan && ttidSpan . status && ttidSpan . status !== 'ok' ) {
99+ ttidSpan . status = 'ok' ;
100+ ttidSpan . timestamp = ttidEndTimestampSeconds ;
101+ logger . debug ( `[${ INTEGRATION_NAME } ] Updated existing ttid span.` , ttidSpan ) ;
102+ return ttidSpan ;
103+ }
104+
105+ ttidSpan = createSpanJSON ( {
106+ op : UI_LOAD_INITIAL_DISPLAY ,
107+ description : 'Time To Initial Display' ,
108+ start_timestamp : transactionStartTimestampSeconds ,
109+ timestamp : ttidEndTimestampSeconds ,
110+ origin : SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY ,
111+ parent_span_id : rootSpanId ,
112+ data : {
113+ [ SPAN_THREAD_NAME ] : SPAN_THREAD_NAME_JAVASCRIPT ,
114+ } ,
115+ } ) ;
116+ logger . debug ( `[${ INTEGRATION_NAME } ] Added ttid span to transaction.` , ttidSpan ) ;
117+ event . spans . push ( ttidSpan ) ;
118+ return ttidSpan ;
119+ }
120+
121+ async function addTimeToFullDisplay ( {
122+ event,
123+ rootSpanId,
124+ transactionStartTimestampSeconds,
125+ ttidSpan,
126+ } : {
127+ event : Event ;
128+ rootSpanId : string ;
129+ transactionStartTimestampSeconds : number ;
130+ ttidSpan : SpanJSON | undefined ;
131+ } ) : Promise < SpanJSON | undefined > {
132+ const ttfdEndTimestampSeconds = await NATIVE . popTimeToDisplayFor ( `ttfd-${ rootSpanId } ` ) ;
133+
134+ if ( ! ttidSpan || ! ttfdEndTimestampSeconds ) {
135+ return undefined ;
136+ }
137+
138+ let ttfdSpan = event . spans ?. find ( span => span . op === UI_LOAD_FULL_DISPLAY ) ;
139+
140+ let ttfdAdjustedEndTimestampSeconds = ttfdEndTimestampSeconds ;
141+ const ttfdIsBeforeTtid = ttidSpan ?. timestamp && ttfdEndTimestampSeconds < ttidSpan . timestamp ;
142+ if ( ttfdIsBeforeTtid ) {
143+ ttfdAdjustedEndTimestampSeconds = ttidSpan . timestamp ;
144+ }
145+
146+ const durationMs = ( ttfdAdjustedEndTimestampSeconds - transactionStartTimestampSeconds ) * 1000 ;
147+
148+ if ( ttfdSpan && ttfdSpan . status && ttfdSpan . status !== 'ok' ) {
149+ ttfdSpan . status = 'ok' ;
150+ ttfdSpan . timestamp = ttfdAdjustedEndTimestampSeconds ;
151+ logger . debug ( `[${ INTEGRATION_NAME } ] Updated existing ttfd span.` , ttfdSpan ) ;
152+ return ttfdSpan ;
153+ }
154+
155+ ttfdSpan = createSpanJSON ( {
156+ status : isDeadlineExceeded ( durationMs ) ? 'deadline_exceeded' : 'ok' ,
157+ op : UI_LOAD_FULL_DISPLAY ,
158+ description : 'Time To Full Display' ,
159+ start_timestamp : transactionStartTimestampSeconds ,
160+ timestamp : ttfdAdjustedEndTimestampSeconds ,
161+ origin : SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY ,
162+ parent_span_id : rootSpanId ,
163+ data : {
164+ [ SPAN_THREAD_NAME ] : SPAN_THREAD_NAME_JAVASCRIPT ,
165+ } ,
166+ } ) ;
167+ logger . debug ( `[${ INTEGRATION_NAME } ] Added ttfd span to transaction.` , ttfdSpan ) ;
168+ event . spans . push ( ttfdSpan ) ;
169+ return ttfdSpan ;
170+ }
0 commit comments