@@ -120,3 +120,196 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
120120 } ) ;
121121 } ) ;
122122} ) ;
123+
124+ test . describe ( 'nested SSR routes (client, server, server request)' , ( ) => {
125+ /** The user-page route fetches from an endpoint and creates a deeply nested span structure:
126+ * pageload — /user-page/myUsername123
127+ * ├── browser.** — multiple browser spans
128+ * └── browser.request — /user-page/myUsername123
129+ * └── http.server — GET /user-page/[userId] (SSR page request)
130+ * └── http.client — GET /api/user/myUsername123.json (executing fetch call from SSR page - span)
131+ * └── http.server — GET /api/user/myUsername123.json (server request)
132+ */
133+ test ( 'sends connected server and client pageload and request spans with the same trace id' , async ( { page } ) => {
134+ const clientPageloadTxnPromise = waitForTransaction ( 'astro-4' , txnEvent => {
135+ return txnEvent ?. transaction ?. startsWith ( '/user-page/' ) ?? false ;
136+ } ) ;
137+
138+ const serverPageRequestTxnPromise = waitForTransaction ( 'astro-4' , txnEvent => {
139+ return txnEvent ?. transaction ?. startsWith ( 'GET /user-page/' ) ?? false ;
140+ } ) ;
141+
142+ const serverHTTPServerRequestTxnPromise = waitForTransaction ( 'astro-4' , txnEvent => {
143+ return txnEvent ?. transaction ?. startsWith ( 'GET /api/user/' ) ?? false ;
144+ } ) ;
145+
146+ await page . goto ( '/user-page/myUsername123' ) ;
147+
148+ const clientPageloadTxn = await clientPageloadTxnPromise ;
149+ const serverPageRequestTxn = await serverPageRequestTxnPromise ;
150+ const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise ;
151+ const serverRequestHTTPClientSpan = serverPageRequestTxn . spans ?. find (
152+ span => span . op === 'http.client' && span . description ?. includes ( '/api/user/' ) ,
153+ ) ;
154+
155+ const clientPageloadTraceId = clientPageloadTxn . contexts ?. trace ?. trace_id ;
156+
157+ // Verify all spans have the same trace ID
158+ expect ( clientPageloadTraceId ) . toEqual ( serverPageRequestTxn . contexts ?. trace ?. trace_id ) ;
159+ expect ( clientPageloadTraceId ) . toEqual ( serverHTTPServerRequestTxn . contexts ?. trace ?. trace_id ) ;
160+ expect ( clientPageloadTraceId ) . toEqual ( serverRequestHTTPClientSpan ?. trace_id ) ;
161+
162+ // serverPageRequest has no parent (root span)
163+ expect ( serverPageRequestTxn . contexts ?. trace ?. parent_span_id ) . toBeUndefined ( ) ;
164+
165+ // clientPageload's parent and serverRequestHTTPClient's parent is serverPageRequest
166+ const serverPageRequestSpanId = serverPageRequestTxn . contexts ?. trace ?. span_id ;
167+ expect ( clientPageloadTxn . contexts ?. trace ?. parent_span_id ) . toEqual ( serverPageRequestSpanId ) ;
168+ expect ( serverRequestHTTPClientSpan ?. parent_span_id ) . toEqual ( serverPageRequestSpanId ) ;
169+
170+ // serverHTTPServerRequest's parent is serverRequestHTTPClient
171+ expect ( serverHTTPServerRequestTxn . contexts ?. trace ?. parent_span_id ) . toEqual ( serverRequestHTTPClientSpan ?. span_id ) ;
172+ } ) ;
173+
174+ test ( 'sends parametrized pageload, server and API request transaction names' , async ( { page } ) => {
175+ const clientPageloadTxnPromise = waitForTransaction ( 'astro-4' , txnEvent => {
176+ return txnEvent ?. transaction ?. startsWith ( '/user-page/' ) ?? false ;
177+ } ) ;
178+
179+ const serverPageRequestTxnPromise = waitForTransaction ( 'astro-4' , txnEvent => {
180+ return txnEvent ?. transaction ?. startsWith ( 'GET /user-page/' ) ?? false ;
181+ } ) ;
182+
183+ const serverHTTPServerRequestTxnPromise = waitForTransaction ( 'astro-4' , txnEvent => {
184+ return txnEvent ?. transaction ?. startsWith ( 'GET /api/user/' ) ?? false ;
185+ } ) ;
186+
187+ await page . goto ( '/user-page/myUsername123' ) ;
188+
189+ const clientPageloadTxn = await clientPageloadTxnPromise ;
190+ const serverPageRequestTxn = await serverPageRequestTxnPromise ;
191+ const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise ;
192+
193+ const serverRequestHTTPClientSpan = serverPageRequestTxn . spans ?. find (
194+ span => span . op === 'http.client' && span . description ?. includes ( '/api/user/' ) ,
195+ ) ;
196+
197+ // Client pageload transaction - actual URL with pageload operation
198+ expect ( clientPageloadTxn ) . toMatchObject ( {
199+ transaction : '/user-page/myUsername123' , // todo: parametrize
200+ transaction_info : { source : 'url' } ,
201+ contexts : {
202+ trace : {
203+ op : 'pageload' ,
204+ origin : 'auto.pageload.browser' ,
205+ data : {
206+ 'sentry.op' : 'pageload' ,
207+ 'sentry.origin' : 'auto.pageload.browser' ,
208+ 'sentry.source' : 'url' ,
209+ } ,
210+ } ,
211+ } ,
212+ } ) ;
213+
214+ // Server page request transaction - parametrized transaction name with actual URL in data
215+ expect ( serverPageRequestTxn ) . toMatchObject ( {
216+ transaction : 'GET /user-page/[userId]' ,
217+ transaction_info : { source : 'route' } ,
218+ contexts : {
219+ trace : {
220+ op : 'http.server' ,
221+ origin : 'auto.http.astro' ,
222+ data : {
223+ 'sentry.op' : 'http.server' ,
224+ 'sentry.origin' : 'auto.http.astro' ,
225+ 'sentry.source' : 'route' ,
226+ url : expect . stringContaining ( '/user-page/myUsername123' ) ,
227+ } ,
228+ } ,
229+ } ,
230+ request : { url : expect . stringContaining ( '/user-page/myUsername123' ) } ,
231+ } ) ;
232+
233+ // HTTP client span - actual API URL with client operation
234+ expect ( serverRequestHTTPClientSpan ) . toMatchObject ( {
235+ op : 'http.client' ,
236+ origin : 'auto.http.otel.node_fetch' ,
237+ description : 'GET http://localhost:3030/api/user/myUsername123.json' , // http.client does not need to be parametrized
238+ data : {
239+ 'sentry.op' : 'http.client' ,
240+ 'sentry.origin' : 'auto.http.otel.node_fetch' ,
241+ 'url.full' : expect . stringContaining ( '/api/user/myUsername123.json' ) ,
242+ 'url.path' : '/api/user/myUsername123.json' ,
243+ url : expect . stringContaining ( '/api/user/myUsername123.json' ) ,
244+ } ,
245+ } ) ;
246+
247+ // Server HTTP request transaction - should be parametrized
248+ expect ( serverHTTPServerRequestTxn ) . toMatchObject ( {
249+ transaction : 'GET /api/user/myUsername123.json' , // todo: parametrize
250+ transaction_info : { source : 'route' } ,
251+ contexts : {
252+ trace : {
253+ op : 'http.server' ,
254+ origin : 'auto.http.astro' ,
255+ data : {
256+ 'sentry.op' : 'http.server' ,
257+ 'sentry.origin' : 'auto.http.astro' ,
258+ 'sentry.source' : 'route' ,
259+ url : expect . stringContaining ( '/api/user/myUsername123.json' ) ,
260+ } ,
261+ } ,
262+ } ,
263+ request : { url : expect . stringContaining ( '/api/user/myUsername123.json' ) } ,
264+ } ) ;
265+ } ) ;
266+
267+ test ( 'sends parametrized pageload and server transaction names for catch-all routes' , async ( { page } ) => {
268+ const clientPageloadTxnPromise = waitForTransaction ( 'astro-4' , txnEvent => {
269+ return txnEvent ?. transaction ?. startsWith ( '/catchAll/' ) ?? false ;
270+ } ) ;
271+
272+ const serverPageRequestTxnPromise = waitForTransaction ( 'astro-4' , txnEvent => {
273+ return txnEvent ?. transaction ?. startsWith ( 'GET /catchAll/' ) ?? false ;
274+ } ) ;
275+
276+ await page . goto ( '/catchAll/hell0/whatever-do' ) ;
277+
278+ const clientPageloadTxn = await clientPageloadTxnPromise ;
279+ const serverPageRequestTxn = await serverPageRequestTxnPromise ;
280+
281+ expect ( clientPageloadTxn ) . toMatchObject ( {
282+ transaction : '/catchAll/hell0/whatever-do' , // todo: parametrize
283+ transaction_info : { source : 'url' } ,
284+ contexts : {
285+ trace : {
286+ op : 'pageload' ,
287+ origin : 'auto.pageload.browser' ,
288+ data : {
289+ 'sentry.op' : 'pageload' ,
290+ 'sentry.origin' : 'auto.pageload.browser' ,
291+ 'sentry.source' : 'url' ,
292+ } ,
293+ } ,
294+ } ,
295+ } ) ;
296+
297+ expect ( serverPageRequestTxn ) . toMatchObject ( {
298+ transaction : 'GET /catchAll/[path]' ,
299+ transaction_info : { source : 'route' } ,
300+ contexts : {
301+ trace : {
302+ op : 'http.server' ,
303+ origin : 'auto.http.astro' ,
304+ data : {
305+ 'sentry.op' : 'http.server' ,
306+ 'sentry.origin' : 'auto.http.astro' ,
307+ 'sentry.source' : 'route' ,
308+ url : expect . stringContaining ( '/catchAll/hell0/whatever-do' ) ,
309+ } ,
310+ } ,
311+ } ,
312+ request : { url : expect . stringContaining ( '/catchAll/hell0/whatever-do' ) } ,
313+ } ) ;
314+ } ) ;
315+ } ) ;
0 commit comments