@@ -11,7 +11,13 @@ type FakeSpan = ReturnType<typeof fakeSpan>;
1111
1212let navigationSpan : FakeSpan | undefined ;
1313let activeSpan : FakeSpan | undefined ;
14- let fetchHandler : ( ( data : { fetchData : { url : string } ; response ?: { headers : Headers } } ) => void ) | undefined ;
14+ let fetchHandler :
15+ | ( ( data : {
16+ fetchData : { url : string } ;
17+ endTimestamp ?: number ;
18+ response ?: { url : string ; headers : Headers } ;
19+ } ) => void )
20+ | undefined ;
1521
1622const startBrowserTracingNavigationSpan = vi . fn ( ( ) => navigationSpan ) ;
1723const upstreamIntegration = { name : 'BrowserTracing' , afterAllSetup : vi . fn ( ) } ;
@@ -57,13 +63,22 @@ vi.mock('@sentry/core', async importOriginal => ({
5763const { browserTracingIntegration } = await import ( '../../src/v3/client/browserTracingIntegration' ) ;
5864
5965/** A response to the runtime's frame fetch, as the fetch instrumentation reports it. */
60- function frameResponse ( url : string , serverTiming ?: string ) {
66+ function frameResponse ( url : string , serverTiming ?: string , servedUrl = url ) {
6167 return {
6268 fetchData : { url } ,
63- response : { headers : new Headers ( serverTiming ? { 'server-timing' : serverTiming } : { } ) } ,
69+ endTimestamp : 1 ,
70+ response : {
71+ url : new URL ( servedUrl , 'https://app.test' ) . href ,
72+ headers : new Headers ( serverTiming ? { 'server-timing' : serverTiming } : { } ) ,
73+ } ,
6474 } ;
6575}
6676
77+ /** A frame fetch that failed: settled, but with no response. */
78+ function failedFrameFetch ( url : string ) {
79+ return { fetchData : { url } , endTimestamp : 1 } ;
80+ }
81+
6782interface FakeNavigateEvent {
6883 canIntercept : boolean ;
6984 info ?: unknown ;
@@ -224,6 +239,53 @@ describe('browserTracingIntegration', () => {
224239 expect ( navigationSpan ?. updateName ) . toHaveBeenCalledWith ( '/files/:name("a\\b")' ) ;
225240 } ) ;
226241
242+ it ( 'records a redirected response under the path that served it, not the one requested' , ( ) => {
243+ const navigate = setup ( ) ;
244+ navigate ( { canIntercept : true , destination : { url : 'https://app.test/old/5' } } ) ;
245+ activeSpan = navigationSpan ;
246+
247+ fetchHandler ?.( frameResponse ( '/old/5' , 'sentry-route;desc="/users/:id"' , '/users/5' ) ) ;
248+
249+ expect ( recorded . get ( '/users/5' ) ) . toBe ( '/users/:id' ) ;
250+ expect ( recorded . has ( '/old/5' ) ) . toBe ( false ) ;
251+ expect ( navigationSpan ?. updateName ) . toHaveBeenCalledWith ( '/users/:id' ) ;
252+ } ) ;
253+
254+ it ( 'still names a repeated navigation after the aborted first attempt reports its failure' , ( ) => {
255+ const first = fakeSpan ( 'navigation' ) ;
256+ const second = fakeSpan ( 'navigation' ) ;
257+ const navigate = setup ( ) ;
258+ navigationSpan = first ;
259+ navigate ( { canIntercept : true , destination : { url : 'https://app.test/users/1' } } ) ;
260+ navigationSpan = second ;
261+ navigate ( { canIntercept : true , destination : { url : 'https://app.test/users/1' } } ) ;
262+ activeSpan = second ;
263+
264+ // The Navigation API aborted the first intercept; its rejection arrives after the second queued.
265+ fetchHandler ?.( failedFrameFetch ( '/users/1' ) ) ;
266+ fetchHandler ?.( frameResponse ( '/users/1' , 'sentry-route;desc="/users/:id"' ) ) ;
267+
268+ expect ( second . updateName ) . toHaveBeenCalledWith ( '/users/:id' ) ;
269+ expect ( first . updateName ) . not . toHaveBeenCalled ( ) ;
270+ } ) ;
271+
272+ it ( 'keeps the pending set bounded when navigations never get a response' , ( ) => {
273+ const navigate = setup ( ) ;
274+ for ( let i = 0 ; i < 30 ; i ++ ) {
275+ navigationSpan = fakeSpan ( 'navigation' ) ;
276+ navigate ( { canIntercept : true , destination : { url : `https://app.test/never/${ i } ` } } ) ;
277+ }
278+ const last = navigationSpan ;
279+ activeSpan = last ;
280+
281+ // The oldest entries were dropped; the newest still resolves.
282+ fetchHandler ?.( frameResponse ( '/never/0' , 'sentry-route;desc="/never/:n"' ) ) ;
283+ fetchHandler ?.( frameResponse ( '/never/29' , 'sentry-route;desc="/never/:n"' ) ) ;
284+
285+ expect ( last ?. updateName ) . toHaveBeenCalledWith ( '/never/:n' ) ;
286+ expect ( recorded . has ( '/never/0' ) ) . toBe ( false ) ;
287+ } ) ;
288+
227289 it ( "ignores the app's own fetches during a navigation" , ( ) => {
228290 const navigate = setup ( ) ;
229291 navigate ( { canIntercept : true , destination : { url : 'https://app.test/users/12345' } } ) ;
0 commit comments