@@ -377,3 +377,124 @@ test('Allows legitimate POP navigation (back/forward) after pageload completes',
377377 expect ( backNavigationEvent . transaction ) . toBe ( '/' ) ;
378378 expect ( backNavigationEvent . contexts ?. trace ?. op ) . toBe ( 'navigation' ) ;
379379} ) ;
380+
381+ test ( 'Updates pageload transaction name correctly when span is cancelled early (document.hidden simulation)' , async ( {
382+ page,
383+ } ) => {
384+ const transactionPromise = waitForTransaction ( 'react-router-7-lazy-routes' , async transactionEvent => {
385+ return (
386+ ! ! transactionEvent ?. transaction &&
387+ transactionEvent . contexts ?. trace ?. op === 'pageload' &&
388+ transactionEvent . transaction === '/lazy/inner/:id/:anotherId/:someAnotherId'
389+ ) ;
390+ } ) ;
391+
392+ // Set up the page to simulate document.hidden before navigation
393+ await page . addInitScript ( ( ) => {
394+ // Wait a bit for Sentry to initialize and start the pageload span
395+ setTimeout ( ( ) => {
396+ // Override document.hidden to simulate tab switching
397+ Object . defineProperty ( document , 'hidden' , {
398+ configurable : true ,
399+ get : function ( ) {
400+ return true ;
401+ } ,
402+ } ) ;
403+
404+ // Dispatch visibilitychange event to trigger the idle span cancellation logic
405+ document . dispatchEvent ( new Event ( 'visibilitychange' ) ) ;
406+ } , 100 ) ; // Small delay to ensure the span has started
407+ } ) ;
408+
409+ // Navigate to the lazy route URL
410+ await page . goto ( '/lazy/inner/1/2/3' ) ;
411+
412+ const event = await transactionPromise ;
413+
414+ // Verify the lazy route content eventually loads (even though span was cancelled early)
415+ const lazyRouteContent = page . locator ( 'id=innermost-lazy-route' ) ;
416+ await expect ( lazyRouteContent ) . toBeVisible ( ) ;
417+
418+ // Validate that the transaction event has the correct parameterized route name
419+ // even though the span was cancelled early due to document.hidden
420+ expect ( event . transaction ) . toBe ( '/lazy/inner/:id/:anotherId/:someAnotherId' ) ;
421+ expect ( event . type ) . toBe ( 'transaction' ) ;
422+ expect ( event . contexts ?. trace ?. op ) . toBe ( 'pageload' ) ;
423+
424+ // Check if the span was indeed cancelled (should have idle_span_finish_reason attribute)
425+ const idleSpanFinishReason = event . contexts ?. trace ?. data ?. [ 'sentry.idle_span_finish_reason' ] ;
426+ if ( idleSpanFinishReason ) {
427+ // If the span was cancelled due to visibility change, verify it still got the right name
428+ expect ( [ 'externalFinish' , 'cancelled' ] ) . toContain ( idleSpanFinishReason ) ;
429+ }
430+ } ) ;
431+
432+ test ( 'Updates navigation transaction name correctly when span is cancelled early (document.hidden simulation)' , async ( {
433+ page,
434+ } ) => {
435+ // First go to home page
436+ await page . goto ( '/' ) ;
437+
438+ const navigationPromise = waitForTransaction ( 'react-router-7-lazy-routes' , async transactionEvent => {
439+ return (
440+ ! ! transactionEvent ?. transaction &&
441+ transactionEvent . contexts ?. trace ?. op === 'navigation' &&
442+ transactionEvent . transaction === '/lazy/inner/:id/:anotherId/:someAnotherId'
443+ ) ;
444+ } ) ;
445+
446+ // Set up a listener to simulate document.hidden after clicking the navigation link
447+ await page . evaluate ( ( ) => {
448+ // Override document.hidden to simulate tab switching
449+ let hiddenValue = false ;
450+ Object . defineProperty ( document , 'hidden' , {
451+ configurable : true ,
452+ get : function ( ) {
453+ return hiddenValue ;
454+ } ,
455+ } ) ;
456+
457+ // Listen for clicks on the navigation link and simulate document.hidden shortly after
458+ document . addEventListener (
459+ 'click' ,
460+ ( ) => {
461+ setTimeout ( ( ) => {
462+ hiddenValue = true ;
463+ // Dispatch visibilitychange event to trigger the idle span cancellation logic
464+ document . dispatchEvent ( new Event ( 'visibilitychange' ) ) ;
465+ } , 50 ) ; // Small delay to ensure the navigation span has started
466+ } ,
467+ { once : true } ,
468+ ) ;
469+ } ) ;
470+
471+ // Click the navigation link to navigate to the lazy route
472+ const navigationLink = page . locator ( 'id=navigation' ) ;
473+ await expect ( navigationLink ) . toBeVisible ( ) ;
474+ await navigationLink . click ( ) ;
475+
476+ const event = await navigationPromise ;
477+
478+ // Verify the lazy route content eventually loads (even though span was cancelled early)
479+ const lazyRouteContent = page . locator ( 'id=innermost-lazy-route' ) ;
480+ await expect ( lazyRouteContent ) . toBeVisible ( ) ;
481+
482+ // Validate that the transaction event has the correct parameterized route name
483+ // even though the span was cancelled early due to document.hidden
484+ expect ( event . transaction ) . toBe ( '/lazy/inner/:id/:anotherId/:someAnotherId' ) ;
485+ expect ( event . type ) . toBe ( 'transaction' ) ;
486+ expect ( event . contexts ?. trace ?. op ) . toBe ( 'navigation' ) ;
487+
488+ // Check if the span was indeed cancelled (should have cancellation_reason attribute or idle_span_finish_reason)
489+ const cancellationReason = event . contexts ?. trace ?. data ?. [ 'sentry.cancellation_reason' ] ;
490+ const idleSpanFinishReason = event . contexts ?. trace ?. data ?. [ 'sentry.idle_span_finish_reason' ] ;
491+
492+ // Verify that the span was cancelled due to document.hidden
493+ if ( cancellationReason ) {
494+ expect ( cancellationReason ) . toBe ( 'document.hidden' ) ;
495+ }
496+
497+ if ( idleSpanFinishReason ) {
498+ expect ( [ 'externalFinish' , 'cancelled' ] ) . toContain ( idleSpanFinishReason ) ;
499+ }
500+ } ) ;
0 commit comments