@@ -55,14 +55,16 @@ test('Propagates trace context from Server-Timing header to client pageload (wit
5555 expect ( serverTimingHeader ) . toContain ( 'sentry-trace' ) ;
5656 expect ( serverTimingHeader ) . toContain ( 'baggage' ) ;
5757
58- // Extract the trace ID from the Server-Timing header
58+ // Extract the trace ID and span ID from the Server-Timing header
5959 const sentryTraceMatch = serverTimingHeader ?. match ( / s e n t r y - t r a c e ; d e s c = " ( [ ^ " ] + ) " / ) ;
6060 expect ( sentryTraceMatch ) . toBeTruthy ( ) ;
6161
6262 const serverTimingTraceValue = sentryTraceMatch ?. [ 1 ] ;
63- const serverTimingTraceId = serverTimingTraceValue ?. split ( '-' ) [ 0 ] ;
63+ const [ serverTimingTraceId , serverTimingSpanId ] = serverTimingTraceValue ?. split ( '-' ) || [ ] ;
6464 expect ( serverTimingTraceId ) . toBeDefined ( ) ;
6565 expect ( serverTimingTraceId ) . toHaveLength ( 32 ) ;
66+ expect ( serverTimingSpanId ) . toBeDefined ( ) ;
67+ expect ( serverTimingSpanId ) . toHaveLength ( 16 ) ;
6668
6769 const pageloadTransaction = await pageLoadTransactionPromise ;
6870
@@ -76,8 +78,9 @@ test('Propagates trace context from Server-Timing header to client pageload (wit
7678 // This proves that the client correctly read and used the Server-Timing header for trace propagation
7779 expect ( pageLoadTraceId ) . toEqual ( serverTimingTraceId ) ;
7880
79- // Verify that the pageload has a parent span (indicating it's continuing a trace, not starting a new one)
80- expect ( pageLoadParentSpanId ) . toBeDefined ( ) ;
81+ // Verify that the pageload's parent_span_id matches the span_id from Server-Timing
82+ // This proves the client is continuing from the exact server span, not just using the trace_id
83+ expect ( pageLoadParentSpanId ) . toEqual ( serverTimingSpanId ) ;
8184} ) ;
8285
8386test ( 'Propagates trace context via Server-Timing for parameterized routes' , async ( { page } ) => {
@@ -124,7 +127,7 @@ test('Propagates trace context via Server-Timing for parameterized routes', asyn
124127 expect ( pageLoadTraceId ) . toEqual ( serverTimingTraceId ) ;
125128} ) ;
126129
127- test ( 'No sentry-trace meta tag is present (testing Server-Timing-only propagation )' , async ( { page } ) => {
130+ test ( 'Test app does not render sentry-trace meta tags (precondition for Server-Timing tests )' , async ( { page } ) => {
128131 await page . goto ( '/' ) ;
129132
130133 // Verify that NO sentry-trace meta tag is present
@@ -136,3 +139,115 @@ test('No sentry-trace meta tag is present (testing Server-Timing-only propagatio
136139 expect ( sentryTraceMetaTag ) . toBeNull ( ) ;
137140 expect ( baggageMetaTag ) . toBeNull ( ) ;
138141} ) ;
142+
143+ test ( 'Propagates baggage/DSC from server to client via Server-Timing header' , async ( { page } ) => {
144+ const testTag = crypto . randomUUID ( ) ;
145+
146+ const responsePromise = page . waitForResponse (
147+ response => response . url ( ) . includes ( `tag=${ testTag } ` ) && response . status ( ) === 200 ,
148+ ) ;
149+
150+ const pageLoadTransactionPromise = waitForTransaction ( 'remix-server-timing' , transactionEvent => {
151+ return transactionEvent . contexts ?. trace ?. op === 'pageload' && transactionEvent . tags ?. [ 'sentry_test' ] === testTag ;
152+ } ) ;
153+
154+ await page . goto ( `/?tag=${ testTag } ` ) ;
155+
156+ const response = await responsePromise ;
157+ const serverTimingHeader = response . headers ( ) [ 'server-timing' ] ;
158+
159+ // Extract sentry-trace from Server-Timing header
160+ const sentryTraceMatch = serverTimingHeader ?. match ( / s e n t r y - t r a c e ; d e s c = " ( [ ^ " ] + ) " / ) ;
161+ expect ( sentryTraceMatch ) . toBeTruthy ( ) ;
162+ const sentryTraceValue = sentryTraceMatch ?. [ 1 ] ;
163+ const [ headerTraceId ] = sentryTraceValue ?. split ( '-' ) || [ ] ;
164+
165+ // Extract baggage from Server-Timing header
166+ const baggageMatch = serverTimingHeader ?. match ( / b a g g a g e ; d e s c = " ( [ ^ " ] + ) " / ) ;
167+ expect ( baggageMatch ) . toBeTruthy ( ) ;
168+
169+ // Baggage is URL-encoded in Server-Timing header
170+ const encodedBaggage = baggageMatch ?. [ 1 ] ;
171+ const decodedBaggage = decodeURIComponent ( encodedBaggage || '' ) ;
172+
173+ // Parse baggage string into key-value pairs
174+ const baggageEntries = decodedBaggage . split ( ',' ) . reduce (
175+ ( acc , entry ) => {
176+ const [ key , value ] = entry . split ( '=' ) ;
177+ if ( key && value ) {
178+ acc [ key ] = value ;
179+ }
180+ return acc ;
181+ } ,
182+ { } as Record < string , string > ,
183+ ) ;
184+
185+ // Verify essential DSC fields are present in baggage
186+ expect ( baggageEntries [ 'sentry-environment' ] ) . toBeDefined ( ) ;
187+ expect ( baggageEntries [ 'sentry-trace_id' ] ) . toBeDefined ( ) ;
188+ expect ( baggageEntries [ 'sentry-public_key' ] ) . toBeDefined ( ) ;
189+
190+ // CRITICAL: The trace_id in baggage MUST match the trace_id in sentry-trace header
191+ // Both are generated from the same span in generateSentryServerTimingHeader()
192+ // If they differ, there's a bug in DSC/trace context handling
193+ expect ( baggageEntries [ 'sentry-trace_id' ] ) . toEqual ( headerTraceId ) ;
194+
195+ const pageloadTransaction = await pageLoadTransactionPromise ;
196+
197+ // Verify the client transaction uses the trace_id from sentry-trace header
198+ expect ( pageloadTransaction . contexts ?. trace ?. trace_id ) . toEqual ( headerTraceId ) ;
199+ } ) ;
200+
201+ test ( 'Client pageload continues server trace with correct parent span ID' , async ( { page } ) => {
202+ // This test verifies the complete trace chain:
203+ // 1. Server generates Server-Timing header with trace_id and span_id
204+ // 2. Client receives and parses the header
205+ // 3. Client pageload transaction uses the same trace_id
206+ // 4. Client pageload's parent_span_id matches the server's span_id
207+ //
208+ // Note: We verify trace continuity via the Server-Timing header rather than
209+ // waiting for the server transaction, as the header is the propagation mechanism.
210+
211+ const testTag = crypto . randomUUID ( ) ;
212+
213+ const responsePromise = page . waitForResponse (
214+ response => response . url ( ) . includes ( `tag=${ testTag } ` ) && response . status ( ) === 200 ,
215+ ) ;
216+
217+ const clientTransactionPromise = waitForTransaction ( 'remix-server-timing' , transactionEvent => {
218+ return transactionEvent . contexts ?. trace ?. op === 'pageload' && transactionEvent . tags ?. [ 'sentry_test' ] === testTag ;
219+ } ) ;
220+
221+ await page . goto ( `/?tag=${ testTag } ` ) ;
222+
223+ const response = await responsePromise ;
224+ const serverTimingHeader = response . headers ( ) [ 'server-timing' ] ;
225+
226+ // Extract trace info from Server-Timing header (this represents the server span)
227+ const sentryTraceMatch = serverTimingHeader ?. match ( / s e n t r y - t r a c e ; d e s c = " ( [ ^ " ] + ) " / ) ;
228+ expect ( sentryTraceMatch ) . toBeTruthy ( ) ;
229+ const serverTimingTraceValue = sentryTraceMatch ?. [ 1 ] ;
230+ const [ serverTraceId , serverSpanId , sampled ] = serverTimingTraceValue ?. split ( '-' ) || [ ] ;
231+
232+ // Verify server trace info is valid
233+ expect ( serverTraceId ) . toBeDefined ( ) ;
234+ expect ( serverTraceId ) . toHaveLength ( 32 ) ;
235+ expect ( serverSpanId ) . toBeDefined ( ) ;
236+ expect ( serverSpanId ) . toHaveLength ( 16 ) ;
237+ expect ( sampled ) . toBe ( '1' ) ; // Should be sampled
238+
239+ const clientTransaction = await clientTransactionPromise ;
240+
241+ // Verify client transaction
242+ expect ( clientTransaction ) . toBeDefined ( ) ;
243+ const clientTraceId = clientTransaction . contexts ?. trace ?. trace_id ;
244+ const clientParentSpanId = clientTransaction . contexts ?. trace ?. parent_span_id ;
245+
246+ // CRITICAL: Client trace_id must match server trace_id
247+ // This proves trace propagation worked
248+ expect ( clientTraceId ) . toEqual ( serverTraceId ) ;
249+
250+ // CRITICAL: Client's parent_span_id must match the server's span_id
251+ // This proves the client is continuing from the exact server span that sent the header
252+ expect ( clientParentSpanId ) . toEqual ( serverSpanId ) ;
253+ } ) ;
0 commit comments