@@ -25,6 +25,7 @@ import { BrowserClient } from '../../src/client';
2525import { WINDOW } from '../../src/helpers' ;
2626import {
2727 browserTracingIntegration ,
28+ getServerTiming ,
2829 startBrowserTracingNavigationSpan ,
2930 startBrowserTracingPageLoadSpan ,
3031} from '../../src/tracing/browserTracingIntegration' ;
@@ -1029,6 +1030,224 @@ describe('browserTracingIntegration', () => {
10291030 } ) ;
10301031 } ) ;
10311032
1033+ describe ( 'getServerTiming' , ( ) => {
1034+ it ( 'retrieves server timing description when available' , ( ) => {
1035+ // Mock the performance API
1036+ const mockServerTiming = [
1037+ { name : 'sentry-trace' , duration : 0 , description : '12312012123120121231201212312012-1121201211212012-1' } ,
1038+ { name : 'baggage' , duration : 0 , description : 'sentry-release=2.1.14,sentry-sample_rand=0.456' } ,
1039+ ] ;
1040+
1041+ const mockNavigationEntry = {
1042+ serverTiming : mockServerTiming ,
1043+ } ;
1044+
1045+ vi . spyOn ( WINDOW . performance , 'getEntriesByType' ) . mockReturnValue ( [
1046+ mockNavigationEntry as any ,
1047+ ] ) ;
1048+
1049+ const sentryTrace = getServerTiming ( 'sentry-trace' ) ;
1050+ const baggage = getServerTiming ( 'baggage' ) ;
1051+
1052+ expect ( sentryTrace ) . toBe ( '12312012123120121231201212312012-1121201211212012-1' ) ;
1053+ expect ( baggage ) . toBe ( 'sentry-release=2.1.14,sentry-sample_rand=0.456' ) ;
1054+ } ) ;
1055+
1056+ it ( 'returns undefined when server timing entry is not found' , ( ) => {
1057+ const mockServerTiming = [ { name : 'other-timing' , duration : 0 , description : 'some-value' } ] ;
1058+
1059+ const mockNavigationEntry = {
1060+ serverTiming : mockServerTiming ,
1061+ } ;
1062+
1063+ vi . spyOn ( WINDOW . performance , 'getEntriesByType' ) . mockReturnValue ( [
1064+ mockNavigationEntry as any ,
1065+ ] ) ;
1066+
1067+ const result = getServerTiming ( 'sentry-trace' ) ;
1068+
1069+ expect ( result ) . toBeUndefined ( ) ;
1070+ } ) ;
1071+
1072+ it ( 'returns undefined when performance API is not available' , ( ) => {
1073+ const originalPerformance = WINDOW . performance ;
1074+ // @ts -expect-error - intentionally setting to undefined
1075+ WINDOW . performance = undefined ;
1076+
1077+ const result = getServerTiming ( 'sentry-trace' ) ;
1078+
1079+ expect ( result ) . toBeUndefined ( ) ;
1080+
1081+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1082+ // @ts -ignore its read only
1083+ WINDOW . performance = originalPerformance ;
1084+ } ) ;
1085+
1086+ it ( 'returns undefined when serverTiming is not available' , ( ) => {
1087+ const mockNavigationEntry = {
1088+ serverTiming : undefined ,
1089+ } ;
1090+
1091+ vi . spyOn ( WINDOW . performance , 'getEntriesByType' ) . mockReturnValue ( [
1092+ mockNavigationEntry as any ,
1093+ ] ) ;
1094+
1095+ const result = getServerTiming ( 'sentry-trace' ) ;
1096+
1097+ expect ( result ) . toBeUndefined ( ) ;
1098+ } ) ;
1099+ } ) ;
1100+
1101+ describe ( 'using Server-Timing headers' , ( ) => {
1102+ it ( 'uses Server-Timing headers for pageload span' , ( ) => {
1103+ // Mock the performance API with Server-Timing data
1104+ const mockServerTiming = [
1105+ { name : 'sentry-trace' , duration : 0 , description : '12312012123120121231201212312012-1121201211212012-0' } ,
1106+ { name : 'baggage' , duration : 0 , description : 'sentry-release=2.1.14,foo=bar,sentry-sample_rand=0.123' } ,
1107+ ] ;
1108+
1109+ const mockNavigationEntry = {
1110+ serverTiming : mockServerTiming ,
1111+ } ;
1112+
1113+ vi . spyOn ( WINDOW . performance , 'getEntriesByType' ) . mockReturnValue ( [
1114+ mockNavigationEntry as any ,
1115+ ] ) ;
1116+
1117+ const client = new BrowserClient (
1118+ getDefaultBrowserClientOptions ( {
1119+ tracesSampleRate : 1 ,
1120+ integrations : [ browserTracingIntegration ( ) ] ,
1121+ } ) ,
1122+ ) ;
1123+ setCurrentClient ( client ) ;
1124+
1125+ // pageload transactions are created as part of the browserTracingIntegration's initialization
1126+ client . init ( ) ;
1127+
1128+ const idleSpan = getActiveSpan ( ) ! ;
1129+ expect ( idleSpan ) . toBeDefined ( ) ;
1130+
1131+ const dynamicSamplingContext = getDynamicSamplingContextFromSpan ( idleSpan ) ;
1132+ const propagationContext = getCurrentScope ( ) . getPropagationContext ( ) ;
1133+
1134+ // Span is correct
1135+ expect ( spanToJSON ( idleSpan ) . op ) . toBe ( 'pageload' ) ;
1136+ expect ( spanToJSON ( idleSpan ) . trace_id ) . toEqual ( '12312012123120121231201212312012' ) ;
1137+ expect ( spanToJSON ( idleSpan ) . parent_span_id ) . toEqual ( '1121201211212012' ) ;
1138+ expect ( spanIsSampled ( idleSpan ) ) . toBe ( false ) ;
1139+
1140+ expect ( dynamicSamplingContext ) . toBeDefined ( ) ;
1141+ expect ( dynamicSamplingContext ) . toStrictEqual ( { release : '2.1.14' , sample_rand : '0.123' } ) ;
1142+
1143+ // Propagation context keeps the Server-Timing trace data for later events on the same route to add them to the trace
1144+ expect ( propagationContext . traceId ) . toEqual ( '12312012123120121231201212312012' ) ;
1145+ expect ( propagationContext . parentSpanId ) . toEqual ( '1121201211212012' ) ;
1146+ expect ( propagationContext . sampleRand ) . toBe ( 0.123 ) ;
1147+ } ) ;
1148+
1149+ it ( 'meta tags take precedence over Server-Timing headers' , ( ) => {
1150+ // Set up both meta tags and Server-Timing headers
1151+ document . head . innerHTML =
1152+ '<meta name="sentry-trace" content="11111111111111111111111111111111-2222222222222222-1">' +
1153+ '<meta name="baggage" content="sentry-release=3.0.0,sentry-sample_rand=0.999">' ;
1154+
1155+ const mockServerTiming = [
1156+ { name : 'sentry-trace' , duration : 0 , description : '12312012123120121231201212312012-1121201211212012-0' } ,
1157+ { name : 'baggage' , duration : 0 , description : 'sentry-release=2.1.14,sentry-sample_rand=0.123' } ,
1158+ ] ;
1159+
1160+ const mockNavigationEntry = {
1161+ serverTiming : mockServerTiming ,
1162+ } ;
1163+
1164+ vi . spyOn ( WINDOW . performance , 'getEntriesByType' ) . mockReturnValue ( [
1165+ mockNavigationEntry as any ,
1166+ ] ) ;
1167+
1168+ const client = new BrowserClient (
1169+ getDefaultBrowserClientOptions ( {
1170+ tracesSampleRate : 1 ,
1171+ integrations : [ browserTracingIntegration ( ) ] ,
1172+ } ) ,
1173+ ) ;
1174+ setCurrentClient ( client ) ;
1175+
1176+ client . init ( ) ;
1177+
1178+ const idleSpan = getActiveSpan ( ) ! ;
1179+ expect ( idleSpan ) . toBeDefined ( ) ;
1180+
1181+ const dynamicSamplingContext = getDynamicSamplingContextFromSpan ( idleSpan ) ;
1182+ const propagationContext = getCurrentScope ( ) . getPropagationContext ( ) ;
1183+
1184+ // Span should use meta tag data, not Server-Timing data
1185+ expect ( spanToJSON ( idleSpan ) . trace_id ) . toEqual ( '11111111111111111111111111111111' ) ;
1186+ expect ( spanToJSON ( idleSpan ) . parent_span_id ) . toEqual ( '2222222222222222' ) ;
1187+ expect ( spanIsSampled ( idleSpan ) ) . toBe ( true ) ;
1188+
1189+ expect ( dynamicSamplingContext ) . toStrictEqual ( { release : '3.0.0' , sample_rand : '0.999' } ) ;
1190+
1191+ expect ( propagationContext . traceId ) . toEqual ( '11111111111111111111111111111111' ) ;
1192+ expect ( propagationContext . parentSpanId ) . toEqual ( '2222222222222222' ) ;
1193+ expect ( propagationContext . sampleRand ) . toBe ( 0.999 ) ;
1194+ } ) ;
1195+
1196+ it ( 'uses passed in tracing data over Server-Timing headers' , ( ) => {
1197+ const mockServerTiming = [
1198+ { name : 'sentry-trace' , duration : 0 , description : '12312012123120121231201212312012-1121201211212012-0' } ,
1199+ { name : 'baggage' , duration : 0 , description : 'sentry-release=2.1.14,sentry-sample_rand=0.123' } ,
1200+ ] ;
1201+
1202+ const mockNavigationEntry = {
1203+ serverTiming : mockServerTiming ,
1204+ } ;
1205+
1206+ vi . spyOn ( WINDOW . performance , 'getEntriesByType' ) . mockReturnValue ( [
1207+ mockNavigationEntry as any ,
1208+ ] ) ;
1209+
1210+ const client = new BrowserClient (
1211+ getDefaultBrowserClientOptions ( {
1212+ tracesSampleRate : 1 ,
1213+ integrations : [ browserTracingIntegration ( { instrumentPageLoad : false } ) ] ,
1214+ } ) ,
1215+ ) ;
1216+ setCurrentClient ( client ) ;
1217+
1218+ client . init ( ) ;
1219+
1220+ // manually create a pageload span with tracing data
1221+ startBrowserTracingPageLoadSpan (
1222+ client ,
1223+ {
1224+ name : 'test span' ,
1225+ } ,
1226+ {
1227+ sentryTrace : '99999999999999999999999999999999-8888888888888888-1' ,
1228+ baggage : 'sentry-release=4.0.0,sentry-sample_rand=0.777' ,
1229+ } ,
1230+ ) ;
1231+
1232+ const idleSpan = getActiveSpan ( ) ! ;
1233+ expect ( idleSpan ) . toBeDefined ( ) ;
1234+
1235+ const dynamicSamplingContext = getDynamicSamplingContextFromSpan ( idleSpan ) ;
1236+ const propagationContext = getCurrentScope ( ) . getPropagationContext ( ) ;
1237+
1238+ // Span should use passed-in data, not Server-Timing data
1239+ expect ( spanToJSON ( idleSpan ) . trace_id ) . toEqual ( '99999999999999999999999999999999' ) ;
1240+ expect ( spanToJSON ( idleSpan ) . parent_span_id ) . toEqual ( '8888888888888888' ) ;
1241+ expect ( spanIsSampled ( idleSpan ) ) . toBe ( true ) ;
1242+
1243+ expect ( dynamicSamplingContext ) . toStrictEqual ( { release : '4.0.0' , sample_rand : '0.777' } ) ;
1244+
1245+ expect ( propagationContext . traceId ) . toEqual ( '99999999999999999999999999999999' ) ;
1246+ expect ( propagationContext . parentSpanId ) . toEqual ( '8888888888888888' ) ;
1247+ expect ( propagationContext . sampleRand ) . toBe ( 0.777 ) ;
1248+ } ) ;
1249+ } ) ;
1250+
10321251 describe ( 'idleTimeout' , ( ) => {
10331252 it ( 'is created by default' , ( ) => {
10341253 vi . useFakeTimers ( ) ;
0 commit comments