@@ -230,22 +230,27 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
230230 try {
231231 const a = new URL ( fromUrl , window . location . origin ) ;
232232 const b = new URL ( toUrl , window . location . origin ) ;
233- // Only consider it a hash change if the base URL (origin + pathname + search) is identical
234- // and only the hash portion differs
235- return (
236- a . origin === b . origin &&
237- a . pathname === b . pathname &&
238- a . search === b . search &&
239- a . hash !== b . hash
240- ) ;
233+ // Only consider it a hash change if:
234+ // 1. Base URL (origin + pathname + search) is identical
235+ // 2. Both URLs have hashes and they're different, OR we're adding a hash
236+ const sameBase = a . origin === b . origin && a . pathname === b . pathname && a . search === b . search ;
237+ const fromHasHash = a . hash !== '' ;
238+ const toHasHash = b . hash !== '' ;
239+ const hashesAreDifferent = a . hash !== b . hash ;
240+
241+ return sameBase && hashesAreDifferent && ( fromHasHash && toHasHash || ! fromHasHash && toHasHash ) ;
241242 } catch {
242- // Fallback: check if base URLs are identical and hash parts differ
243+ // Fallback: check if base URLs are identical and we're changing/adding hash (not removing)
243244 const fromBase = fromUrl . split ( '#' ) [ 0 ] ;
244245 const toBase = toUrl . split ( '#' ) [ 0 ] ;
245246 const fromHash = fromUrl . split ( '#' ) [ 1 ] || '' ;
246247 const toHash = toUrl . split ( '#' ) [ 1 ] || '' ;
247248
248- return fromBase === toBase && fromHash !== toHash ;
249+ const sameBase = fromBase === toBase ;
250+ const hashesAreDifferent = fromHash !== toHash ;
251+ const notRemovingHash = toHash !== '' ; // Only true if we're not removing the hash
252+
253+ return sameBase && hashesAreDifferent && notRemovingHash ;
249254 }
250255 }
251256
@@ -266,19 +271,22 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
266271 const fromURL = new URL ( fromUrl ) ;
267272 const toURL = new URL ( toUrl ) ;
268273 // Same document if origin is the same (cross-origin navigations are always different documents)
274+ // In SPAs, route changes via pushState/replaceState are same-document navigations
269275 return fromURL . origin === toURL . origin ;
270276 } catch {
271277 // Fallback: assume same document for relative URLs or parsing errors
272278 return true ;
273279 }
274280 }
275281
276- // Default to true for soft navigations (pushState, replaceState, popstate, hashchange)
282+ // Default: if we can't determine URLs, assume it's a same-document navigation
283+ // This handles cases where URL comparison fails
277284 return true ;
278285 }
279286
280287 /**
281- * Determines if navigation is a hash change according to Navigation API specification
288+ * Determines if navigation is a hash change based on URL comparison
289+ * A hash change is true if the URLs are the same except for the hash part
282290 */
283291 private _determineHashChange (
284292 changeState ?: string | null ,
@@ -291,21 +299,7 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
291299 return navigationEvent . hashChange ;
292300 }
293301
294- // For hashchange events, it's always a hash change
295- if ( changeState === 'hashchange' ) {
296- return true ;
297- }
298-
299- // For popstate events (back/forward), only consider it a hash change if URLs actually differ by hash
300- // This prevents back navigation from hash to non-hash being incorrectly marked as hash change
301- if ( changeState === 'popstate' ) {
302- if ( fromUrl && toUrl ) {
303- return this . _isHashChange ( fromUrl , toUrl ) ;
304- }
305- return false ;
306- }
307-
308- // For other navigation types (pushState, replaceState), determine based on URL comparison
302+ // For all other cases, determine based on URL comparison
309303 if ( fromUrl && toUrl ) {
310304 return this . _isHashChange ( fromUrl , toUrl ) ;
311305 }
@@ -398,7 +392,9 @@ export class BrowserNavigationInstrumentation extends InstrumentationBase<Browse
398392 case 'replaceState' :
399393 return 'replace' ;
400394 case 'popstate' :
401- return 'traverse' ;
395+ // For popstate, we need to check if it's a hash change to determine type
396+ // This is called after _determineHashChange, so we need to check URLs here too
397+ return 'traverse' ; // Default to traverse, but hash changes will be handled specially
402398 case 'hashchange' :
403399 return 'push' ;
404400 case 'navigate' :
0 commit comments