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