@@ -31,6 +31,7 @@ import {
3131} from '../../src/js/tracing/semanticAttributes' ;
3232import { SPAN_THREAD_NAME , SPAN_THREAD_NAME_JAVASCRIPT } from '../../src/js/tracing/span' ;
3333import {
34+ reportFullyDisplayed ,
3435 startTimeToFullDisplaySpan ,
3536 startTimeToInitialDisplaySpan ,
3637 TimeToFullDisplay ,
@@ -990,3 +991,161 @@ describe('Frame Data', () => {
990991 } ) ;
991992 } ) ;
992993} ) ;
994+
995+ describe ( 'reportFullyDisplayed' , ( ) => {
996+ let client : TestClient ;
997+
998+ beforeEach ( ( ) => {
999+ clearMockedOnDrawReportedProps ( ) ;
1000+ _resetTimeToDisplayCoordinator ( ) ;
1001+ getCurrentScope ( ) . clear ( ) ;
1002+ getIsolationScope ( ) . clear ( ) ;
1003+ getGlobalScope ( ) . clear ( ) ;
1004+
1005+ const options = getDefaultTestClientOptions ( {
1006+ tracesSampleRate : 1.0 ,
1007+ } ) ;
1008+ client = new TestClient ( {
1009+ ...options ,
1010+ integrations : [ ...options . integrations , timeToDisplayIntegration ( ) ] ,
1011+ } ) ;
1012+ setCurrentClient ( client ) ;
1013+ client . init ( ) ;
1014+ } ) ;
1015+
1016+ afterEach ( ( ) => {
1017+ jest . clearAllMocks ( ) ;
1018+ mockWrapper . NATIVE . enableNative = true ;
1019+ } ) ;
1020+
1021+ test ( 'creates full display span with manual origin and measurement' , async ( ) => {
1022+ await startSpanManual (
1023+ {
1024+ name : 'Root Manual Span' ,
1025+ startTime : secondAgoTimestampMs ( ) ,
1026+ } ,
1027+ async ( activeSpan : Span | undefined ) => {
1028+ const ttidSpan = startTimeToInitialDisplaySpan ( ) ;
1029+ render ( < TimeToInitialDisplay record = { true } /> ) ;
1030+
1031+ await Promise . resolve ( ) ;
1032+ await Promise . resolve ( ) ;
1033+ await Promise . resolve ( ) ;
1034+
1035+ updateInitialDisplaySpan ( nowInSeconds ( ) , { activeSpan, span : ttidSpan } ) ;
1036+
1037+ for ( let i = 0 ; i < 15 ; i ++ ) {
1038+ await Promise . resolve ( ) ;
1039+ }
1040+
1041+ reportFullyDisplayed ( ) ;
1042+
1043+ for ( let i = 0 ; i < 15 ; i ++ ) {
1044+ await Promise . resolve ( ) ;
1045+ }
1046+
1047+ activeSpan ?. end ( ) ;
1048+ } ,
1049+ ) ;
1050+
1051+ await jest . runOnlyPendingTimersAsync ( ) ;
1052+ await client . flush ( ) ;
1053+
1054+ const ttfdSpan = getFullDisplaySpanJSON ( client . event ! . spans ! ) ;
1055+ expect ( ttfdSpan ) . toBeDefined ( ) ;
1056+ expect ( ttfdSpan ! . op ) . toBe ( 'ui.load.full_display' ) ;
1057+ expect ( ttfdSpan ! . status ) . toBe ( 'ok' ) ;
1058+ expect ( ttfdSpan ! . timestamp ) . toBeDefined ( ) ;
1059+ expect ( ttfdSpan ! . data [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] ) . toBe ( SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY ) ;
1060+ expectFullDisplayMeasurementOnSpan ( client . event ! ) ;
1061+ } ) ;
1062+
1063+ test ( 'defers full display when called before initial display with correct origin' , async ( ) => {
1064+ await startSpanManual (
1065+ {
1066+ name : 'Root Manual Span' ,
1067+ startTime : secondAgoTimestampMs ( ) ,
1068+ } ,
1069+ async ( activeSpan : Span | undefined ) => {
1070+ const ttidSpan = startTimeToInitialDisplaySpan ( ) ;
1071+ render ( < TimeToInitialDisplay record = { true } /> ) ;
1072+
1073+ await Promise . resolve ( ) ;
1074+ await Promise . resolve ( ) ;
1075+ await Promise . resolve ( ) ;
1076+
1077+ reportFullyDisplayed ( ) ;
1078+
1079+ updateInitialDisplaySpan ( nowInSeconds ( ) , { activeSpan, span : ttidSpan } ) ;
1080+
1081+ for ( let i = 0 ; i < 15 ; i ++ ) {
1082+ await Promise . resolve ( ) ;
1083+ }
1084+
1085+ activeSpan ?. end ( ) ;
1086+ } ,
1087+ ) ;
1088+
1089+ await jest . runOnlyPendingTimersAsync ( ) ;
1090+ await client . flush ( ) ;
1091+
1092+ const ttfdSpan = getFullDisplaySpanJSON ( client . event ! . spans ! ) ;
1093+ expect ( ttfdSpan ) . toBeDefined ( ) ;
1094+ expect ( ttfdSpan ! . op ) . toBe ( 'ui.load.full_display' ) ;
1095+ expect ( ttfdSpan ! . status ) . toBe ( 'ok' ) ;
1096+ expect ( ttfdSpan ! . data [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] ) . toBe ( SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY ) ;
1097+
1098+ const ttidSpanJSON = getInitialDisplaySpanJSON ( client . event ! . spans ! ) ;
1099+ expect ( ttfdSpan ! . timestamp ) . toEqual ( ttidSpanJSON ! . timestamp ) ;
1100+
1101+ expectFullDisplayMeasurementOnSpan ( client . event ! ) ;
1102+ } ) ;
1103+
1104+ test ( 'does nothing without an active span' , ( ) => {
1105+ expect ( ( ) => reportFullyDisplayed ( ) ) . not . toThrow ( ) ;
1106+ expect ( debug . warn ) . toHaveBeenCalledWith ( expect . stringContaining ( 'No active span found' ) ) ;
1107+ } ) ;
1108+
1109+ test ( 'second call is ignored after span already ended' , async ( ) => {
1110+ await startSpanManual (
1111+ {
1112+ name : 'Root Manual Span' ,
1113+ startTime : secondAgoTimestampMs ( ) ,
1114+ } ,
1115+ async ( activeSpan : Span | undefined ) => {
1116+ const ttidSpan = startTimeToInitialDisplaySpan ( ) ;
1117+ render ( < TimeToInitialDisplay record = { true } /> ) ;
1118+
1119+ await Promise . resolve ( ) ;
1120+ await Promise . resolve ( ) ;
1121+ await Promise . resolve ( ) ;
1122+
1123+ updateInitialDisplaySpan ( nowInSeconds ( ) , { activeSpan, span : ttidSpan } ) ;
1124+
1125+ for ( let i = 0 ; i < 15 ; i ++ ) {
1126+ await Promise . resolve ( ) ;
1127+ }
1128+
1129+ reportFullyDisplayed ( ) ;
1130+
1131+ for ( let i = 0 ; i < 15 ; i ++ ) {
1132+ await Promise . resolve ( ) ;
1133+ }
1134+
1135+ reportFullyDisplayed ( ) ;
1136+
1137+ for ( let i = 0 ; i < 15 ; i ++ ) {
1138+ await Promise . resolve ( ) ;
1139+ }
1140+
1141+ activeSpan ?. end ( ) ;
1142+ } ,
1143+ ) ;
1144+
1145+ await jest . runOnlyPendingTimersAsync ( ) ;
1146+ await client . flush ( ) ;
1147+
1148+ const ttfdSpans = client . event ! . spans ! . filter ( ( s : SpanJSON ) => s . op === 'ui.load.full_display' ) ;
1149+ expect ( ttfdSpans ) . toHaveLength ( 1 ) ;
1150+ } ) ;
1151+ } ) ;
0 commit comments