From 197b859803ddeb2a6b1e5cf0b3655b604ad58969 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 9 Jul 2026 14:42:27 +0200 Subject: [PATCH 1/2] fix(vue): Refresh pageload URL attrs on TanStack onResolved Pageload onResolved only updated url.path, url.full, and param attributes when the resolved routeId differed from the initial match. Redirects to the same route template with different params left stale URL fields on the span. Co-Authored-By: Cursor --- packages/vue/src/tanstackrouter.ts | 40 +++++++++------------ packages/vue/test/tanstackrouter.test.ts | 44 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/packages/vue/src/tanstackrouter.ts b/packages/vue/src/tanstackrouter.ts index e7f6c6efaef7..6521bbe201a5 100644 --- a/packages/vue/src/tanstackrouter.ts +++ b/packages/vue/src/tanstackrouter.ts @@ -61,6 +61,21 @@ export function tanstackRouterBrowserTracingIntegration( return lastMatch?.routeId !== '__root__' ? lastMatch : undefined; }; + const applyRouteMatch = ( + span: NonNullable>, + match: RouteMatch | undefined, + toLocation: TanstackRouterLocation, + fallbackName: string, + ): void => { + span.updateName(match ? match.routeId : fallbackName); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, match ? 'route' : 'url'); + span.setAttributes({ + [URL_TEMPLATE]: match?.routeId, + ...locationToSpanUrlAttributes(router, toLocation), + ...routeMatchToParamSpanAttributes(match), + }); + }; + const initialWindowLocation = WINDOW.location; if (instrumentPageLoad && initialWindowLocation) { const routeMatch = resolveRouteMatch( @@ -89,15 +104,7 @@ export function tanstackRouterBrowserTracingIntegration( } const { toLocation } = onResolvedArgs as TanstackRouterSubscribeArgs; const resolvedMatch = resolveRouteMatch(toLocation.pathname, toLocation.search); - if (resolvedMatch && resolvedMatch.routeId !== routeMatch?.routeId) { - pageloadSpan.updateName(resolvedMatch.routeId); - pageloadSpan.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [URL_TEMPLATE]: resolvedMatch.routeId, - ...locationToSpanUrlAttributes(router, toLocation), - ...routeMatchToParamSpanAttributes(resolvedMatch), - }); - } + applyRouteMatch(pageloadSpan, resolvedMatch, toLocation, toLocation.pathname); }); } @@ -108,21 +115,6 @@ export function tanstackRouterBrowserTracingIntegration( // span on the first `onBeforeLoad`, rename it on later ones, and clear it on `onResolved`. let inFlightNavigationSpan: ReturnType | undefined; - const applyRouteMatch = ( - span: NonNullable, - match: RouteMatch | undefined, - toLocation: TanstackRouterLocation, - fallbackName: string, - ): void => { - span.updateName(match ? match.routeId : fallbackName); - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, match ? 'route' : 'url'); - span.setAttributes({ - [URL_TEMPLATE]: match?.routeId, - ...locationToSpanUrlAttributes(router, toLocation), - ...routeMatchToParamSpanAttributes(match), - }); - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any router.subscribe('onBeforeLoad', (onBeforeLoadArgs: any) => { const { toLocation, fromLocation } = onBeforeLoadArgs as TanstackRouterSubscribeArgs; diff --git a/packages/vue/test/tanstackrouter.test.ts b/packages/vue/test/tanstackrouter.test.ts index 7f6700098d2d..a6432ecc1c59 100644 --- a/packages/vue/test/tanstackrouter.test.ts +++ b/packages/vue/test/tanstackrouter.test.ts @@ -27,6 +27,12 @@ const mockNavigationSpan = { setAttributes: vi.fn(), }; +const mockPageloadSpan = { + updateName: vi.fn(), + setAttribute: vi.fn(), + setAttributes: vi.fn(), +}; + describe('tanstackRouterBrowserTracingIntegration', () => { const mockMatchedRoutes = [ { @@ -72,6 +78,7 @@ describe('tanstackRouterBrowserTracingIntegration', () => { beforeEach(() => { vi.clearAllMocks(); startBrowserTracingNavigationSpanSpy.mockReturnValue(mockNavigationSpan as any); + startBrowserTracingPageLoadSpanSpy.mockReturnValue(mockPageloadSpan as any); // Mock window.location vi.stubGlobal('window', { @@ -130,6 +137,43 @@ describe('tanstackRouterBrowserTracingIntegration', () => { expect(startBrowserTracingPageLoadSpanSpy).not.toHaveBeenCalled(); }); + it('updates pageload span URL attributes on redirect to the same route template', () => { + const integration = tanstackRouterBrowserTracingIntegration(mockRouter, { + instrumentPageLoad: true, + instrumentNavigation: false, + }); + + integration.afterAllSetup(mockClient as any); + + const onResolvedCallback = getSubscribeCallback('onResolved'); + expect(onResolvedCallback).toBeDefined(); + + (mockRouter.matchRoutes as any).mockReturnValueOnce([ + { + routeId: '/test/:id', + pathname: '/test/456', + params: { id: '456' }, + }, + ]); + + onResolvedCallback({ + toLocation: { + pathname: '/test/456', + search: {}, + }, + }); + + expect(mockPageloadSpan.setAttributes).toHaveBeenCalledWith( + expect.objectContaining({ + [URL_TEMPLATE]: '/test/:id', + 'url.path': '/test/456', + 'url.full': expect.any(String), + 'url.path.parameter.id': '456', + 'params.id': '456', + }), + ); + }); + const getSubscribeCallback = (eventType: string): ((...args: any[]) => void) => (mockRouter.subscribe as any).mock.calls.find( (call: [string, (...args: any[]) => void]) => call[0] === eventType, From 82a356b63fdb161a6c21693847ef094fd1e00a40 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 10 Jul 2026 15:37:39 +0200 Subject: [PATCH 2/2] add test --- packages/vue/test/tanstackrouter.test.ts | 56 ++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/packages/vue/test/tanstackrouter.test.ts b/packages/vue/test/tanstackrouter.test.ts index a6432ecc1c59..e622a3e7ba33 100644 --- a/packages/vue/test/tanstackrouter.test.ts +++ b/packages/vue/test/tanstackrouter.test.ts @@ -174,6 +174,62 @@ describe('tanstackRouterBrowserTracingIntegration', () => { ); }); + it('clears url.template on pageload onResolved when the final destination does not match a route', () => { + const integration = tanstackRouterBrowserTracingIntegration(mockRouter, { + instrumentPageLoad: true, + instrumentNavigation: false, + }); + + integration.afterAllSetup(mockClient as any); + + const onResolvedCallback = getSubscribeCallback('onResolved'); + expect(onResolvedCallback).toBeDefined(); + + // A redirect during pageload lands on a URL that no longer matches a route. + (mockRouter.matchRoutes as any).mockReturnValueOnce([{ routeId: '__root__', params: {} }]); + + onResolvedCallback({ + toLocation: { pathname: '/unknown/path', search: {} }, + }); + + expect(mockPageloadSpan.updateName).toHaveBeenCalledWith('/unknown/path'); + expect(mockPageloadSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); + expect(mockPageloadSpan.setAttributes).toHaveBeenLastCalledWith( + expect.objectContaining({ + [URL_TEMPLATE]: undefined, + 'url.path': '/unknown/path', + 'url.full': expect.any(String), + }), + ); + }); + + it('unsubscribes the pageload onResolved handler after the first resolution', () => { + const unsubscribe = vi.fn(); + (mockRouter.subscribe as any).mockImplementation((eventType: string) => { + if (eventType === 'onResolved') { + return unsubscribe; + } + return vi.fn(); + }); + + const integration = tanstackRouterBrowserTracingIntegration(mockRouter, { + instrumentPageLoad: true, + instrumentNavigation: false, + }); + + integration.afterAllSetup(mockClient as any); + + const onResolvedCallback = getSubscribeCallback('onResolved'); + expect(onResolvedCallback).toBeDefined(); + + onResolvedCallback({ toLocation: { pathname: '/test/456', search: {} } }); + + // The handler must detach itself so later resolutions (e.g. subsequent navigations) + // don't touch the pageload span again. + expect(unsubscribe).toHaveBeenCalledTimes(1); + expect(mockPageloadSpan.updateName).toHaveBeenCalledTimes(1); + }); + const getSubscribeCallback = (eventType: string): ((...args: any[]) => void) => (mockRouter.subscribe as any).mock.calls.find( (call: [string, (...args: any[]) => void]) => call[0] === eventType,