11import { expect , test } from '@playwright/test' ;
2- import { waitForTransaction } from '@sentry-internal/test-utils' ;
3-
4- test ( 'Tunnel route should proxy pageload transaction to Sentry' , async ( { page } ) => {
5- // Wait for the pageload transaction to be sent through the tunnel
6- const pageloadTransactionPromise = waitForTransaction ( 'nextjs-16-tunnel' , async transactionEvent => {
7- return transactionEvent ?. contexts ?. trace ?. op === 'pageload' && transactionEvent ?. transaction === '/' ;
2+ import { getSpanOp , waitForStreamedSpan , waitForStreamedSpans } from '@sentry-internal/test-utils' ;
3+ import type { SerializedStreamedSpan } from '@sentry/core' ;
4+
5+ function getStringAttribute ( span : SerializedStreamedSpan , key : string ) : string | undefined {
6+ const attribute = span . attributes [ key ] ;
7+ return attribute ?. type === 'string' ? attribute . value : undefined ;
8+ }
9+
10+ test ( 'Tunnel route should proxy pageload span to Sentry' , async ( { page } ) => {
11+ // Wait for the pageload span to be sent through the tunnel
12+ const pageloadSpanPromise = waitForStreamedSpan ( 'nextjs-16-tunnel' , span => {
13+ return getSpanOp ( span ) === 'pageload' && span . name === '/' && span . is_segment ;
814 } ) ;
915
1016 // Navigate to the page
1117 await page . goto ( '/' ) ;
1218
13- const pageloadTransaction = await pageloadTransactionPromise ;
19+ const pageloadSpan = await pageloadSpanPromise ;
1420
15- // Verify the pageload transaction was received successfully
16- expect ( pageloadTransaction ) . toBeDefined ( ) ;
17- expect ( pageloadTransaction . transaction ) . toBe ( '/' ) ;
18- expect ( pageloadTransaction . contexts ?. trace ?. op ) . toBe ( 'pageload' ) ;
19- expect ( pageloadTransaction . contexts ?. trace ?. status ) . toBe ( 'ok' ) ;
20- expect ( pageloadTransaction . type ) . toBe ( 'transaction' ) ;
21+ // Verify the pageload span was received successfully
22+ expect ( pageloadSpan ) . toBeDefined ( ) ;
23+ expect ( pageloadSpan . name ) . toBe ( '/' ) ;
24+ expect ( getSpanOp ( pageloadSpan ) ) . toBe ( 'pageload' ) ;
25+ expect ( pageloadSpan . status ) . toBe ( 'ok' ) ;
2126} ) ;
2227
23- test ( 'Tunnel route should send multiple pageload transactions consistently' , async ( { page } ) => {
28+ test ( 'Tunnel route should send multiple pageload spans consistently' , async ( { page } ) => {
2429 // This test verifies that the tunnel route remains consistent across multiple page loads
2530 // (important for Turbopack which could generate different tunnel routes for client/server)
2631
2732 // First pageload
28- const firstPageloadPromise = waitForTransaction ( 'nextjs-16-tunnel' , async transactionEvent => {
29- return transactionEvent ?. contexts ?. trace ?. op === 'pageload' && transactionEvent ?. transaction === '/' ;
33+ const firstPageloadPromise = waitForStreamedSpan ( 'nextjs-16-tunnel' , span => {
34+ return getSpanOp ( span ) === 'pageload' && span . name === '/' && span . is_segment ;
3035 } ) ;
3136
3237 await page . goto ( '/' ) ;
3338 const firstPageload = await firstPageloadPromise ;
3439
3540 expect ( firstPageload ) . toBeDefined ( ) ;
36- expect ( firstPageload . transaction ) . toBe ( '/' ) ;
37- expect ( firstPageload . contexts ?. trace ?. op ) . toBe ( 'pageload' ) ;
38- expect ( firstPageload . contexts ?. trace ?. status ) . toBe ( 'ok' ) ;
41+ expect ( firstPageload . name ) . toBe ( '/' ) ;
42+ expect ( getSpanOp ( firstPageload ) ) . toBe ( 'pageload' ) ;
43+ expect ( firstPageload . status ) . toBe ( 'ok' ) ;
3944
4045 // Second pageload (reload)
41- const secondPageloadPromise = waitForTransaction ( 'nextjs-16-tunnel' , async transactionEvent => {
42- return transactionEvent ?. contexts ?. trace ?. op === 'pageload' && transactionEvent ?. transaction === '/' ;
46+ const secondPageloadPromise = waitForStreamedSpan ( 'nextjs-16-tunnel' , span => {
47+ return getSpanOp ( span ) === 'pageload' && span . name === '/' && span . is_segment ;
4348 } ) ;
4449
4550 await page . reload ( ) ;
4651 const secondPageload = await secondPageloadPromise ;
4752
4853 expect ( secondPageload ) . toBeDefined ( ) ;
49- expect ( secondPageload . transaction ) . toBe ( '/' ) ;
50- expect ( secondPageload . contexts ?. trace ?. op ) . toBe ( 'pageload' ) ;
51- expect ( secondPageload . contexts ?. trace ?. status ) . toBe ( 'ok' ) ;
54+ expect ( secondPageload . name ) . toBe ( '/' ) ;
55+ expect ( getSpanOp ( secondPageload ) ) . toBe ( 'pageload' ) ;
56+ expect ( secondPageload . status ) . toBe ( 'ok' ) ;
5257} ) ;
5358
5459test ( 'Tunnel requests should not create middleware or fetch spans' , async ( { page } ) => {
5560 // This test verifies that our span filtering logic works correctly
56- // The proxy runs on all routes, so we'll get a middleware transaction for `/`
57- // But we should NOT get middleware or fetch transactions for the tunnel route itself
58-
59- const allTransactions : any [ ] = [ ] ;
60-
61- // Collect all transactions
62- const collectPromise = ( async ( ) => {
63- // Keep collecting for 3 seconds after pageload
64- const endTime = Date . now ( ) + 3000 ;
65- while ( Date . now ( ) < endTime ) {
66- try {
67- const tx = await Promise . race ( [
68- waitForTransaction ( 'nextjs-16-tunnel' , ( ) => true ) ,
69- new Promise ( ( _ , reject ) => setTimeout ( ( ) => reject ( ) , 500 ) ) ,
70- ] ) ;
71- allTransactions . push ( tx ) ;
72- } catch {
73- // Timeout, continue collecting
74- }
75- }
76- } ) ( ) ;
77-
78- // Wait for pageload transaction
79- const pageloadPromise = waitForTransaction ( 'nextjs-16-tunnel' , async transactionEvent => {
80- return transactionEvent ?. contexts ?. trace ?. op === 'pageload' ;
61+ // The proxy runs on all routes, so we'll get a middleware span for `/`
62+ // But we should NOT get middleware or fetch spans for the tunnel route itself
63+
64+ // Accumulate every streamed span for the duration of the test. The callback never returns true,
65+ // so this promise is deliberately left unsettled - the assertions below read the array instead.
66+ const allSpans : SerializedStreamedSpan [ ] = [ ] ;
67+ void waitForStreamedSpans ( 'nextjs-16-tunnel' , spans => {
68+ allSpans . push ( ...spans ) ;
69+ return false ;
70+ } ) ;
71+
72+ // Wait for pageload span
73+ const pageloadPromise = waitForStreamedSpan ( 'nextjs-16-tunnel' , span => {
74+ return getSpanOp ( span ) === 'pageload' && span . is_segment ;
8175 } ) ;
8276
8377 await page . goto ( '/' ) ;
84- const pageloadTransaction = await pageloadPromise ;
78+ const pageloadSpan = await pageloadPromise ;
8579
8680 // Trigger errors to force tunnel POST requests
8781 await page
@@ -100,33 +94,29 @@ test('Tunnel requests should not create middleware or fetch spans', async ({ pag
10094 // Expected to throw
10195 } ) ;
10296
103- // Wait for events to be sent through tunnel
104- await page . waitForTimeout ( 2000 ) ;
97+ // Wait for events to be sent through tunnel, and for the spans they would wrongly produce to arrive
98+ await page . waitForTimeout ( 3000 ) ;
10599
106- // Continue collecting for a bit
107- await collectPromise ;
100+ // We should have received the pageload span
101+ expect ( pageloadSpan ) . toBeDefined ( ) ;
102+ expect ( getSpanOp ( pageloadSpan ) ) . toBe ( 'pageload' ) ;
108103
109- // We should have received the pageload transaction
110- expect ( pageloadTransaction ) . toBeDefined ( ) ;
111- expect ( pageloadTransaction . contexts ?. trace ?. op ) . toBe ( 'pageload' ) ;
104+ const middlewareSpans = allSpans . filter ( span => getSpanOp ( span ) === 'middleware' ) ;
112105
113- const middlewareTransactions = allTransactions . filter ( tx => tx . contexts ?. trace ?. op === 'middleware' ) ;
114-
115- // We WILL have a middleware transaction for GET / (the pageload)
116- // But we should NOT have middleware transactions for POST requests (tunnel route)
117- const postMiddlewareTransactions = middlewareTransactions . filter (
118- tx => tx . transaction ?. includes ( 'POST' ) || tx . contexts ?. trace ?. data ?. [ 'http.request.method' ] === 'POST' ,
106+ // We WILL have a middleware span for GET / (the pageload)
107+ // But we should NOT have middleware spans for POST requests (tunnel route)
108+ const postMiddlewareSpans = middlewareSpans . filter (
109+ span => span . name . includes ( 'POST' ) || getStringAttribute ( span , 'http.request.method' ) === 'POST' ,
119110 ) ;
120111
121- expect ( postMiddlewareTransactions ) . toHaveLength ( 0 ) ;
112+ expect ( postMiddlewareSpans ) . toHaveLength ( 0 ) ;
122113
123- // We should NOT have any fetch transactions to Sentry ingest
124- const sentryFetchTransactions = allTransactions . filter (
125- tx =>
126- tx . contexts ?. trace ?. op === 'http.client' &&
127- ( tx . contexts ?. trace ?. data ?. [ 'url.full' ] ?. includes ( 'sentry.io' ) ||
128- tx . contexts ?. trace ?. data ?. [ 'url.full' ] ?. includes ( 'ingest' ) ) ,
129- ) ;
114+ // We should NOT have any fetch spans to Sentry ingest. Matched on the host attribute rather than a
115+ // substring of the full URL, which would also match an arbitrary host with `sentry.io` elsewhere in it.
116+ const sentryFetchSpans = allSpans . filter ( span => {
117+ const host = getStringAttribute ( span , 'server.address' ) ?? '' ;
118+ return getSpanOp ( span ) === 'http.client' && ( host === 'sentry.io' || host . endsWith ( '.sentry.io' ) ) ;
119+ } ) ;
130120
131- expect ( sentryFetchTransactions ) . toHaveLength ( 0 ) ;
121+ expect ( sentryFetchSpans ) . toHaveLength ( 0 ) ;
132122} ) ;
0 commit comments