From 8dc3f3d6c63c6cc58266e53910cdc0b3c2d3ccde Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 24 Aug 2026 15:42:25 -0400 Subject: [PATCH] feat(tanstack): Register a route provider from the TanStack router matcher Hoists the existing `resolveRouteMatch` helper out of `afterAllSetup` in each of the three packages so the provider can reuse it, rather than restating the match rules. The matcher is already stateless, so nothing else had to change. --- packages/react/src/tanstackrouter.ts | 36 +++++++++++++++++++--------- packages/solid/src/tanstackrouter.ts | 28 +++++++++++++++------- packages/vue/src/tanstackrouter.ts | 30 ++++++++++++++++------- 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/packages/react/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts index e6d1a1e0e681..033864c56b99 100644 --- a/packages/react/src/tanstackrouter.ts +++ b/packages/react/src/tanstackrouter.ts @@ -7,10 +7,12 @@ import { } from '@sentry/browser'; import type { Integration } from '@sentry/core/browser'; import { + createUrlRouteProvider, filterCollectedUrl, hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, + setRouteProvider, } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core/browser'; import type { VendoredTanstackRouter, VendoredTanstackRouterRouteMatch } from './vendor/tanstackrouter-types'; @@ -54,22 +56,34 @@ export function tanstackRouterBrowserTracingIntegration( const { instrumentPageLoad = true, instrumentNavigation = true } = options; + const resolveRouteMatch = (pathname: string, search: unknown): VendoredTanstackRouterRouteMatch | undefined => { + const matchedRoutes = castRouterInstance.matchRoutes(pathname, search as {}, { + preload: false, + throwOnError: false, + }); + const lastMatch = matchedRoutes[matchedRoutes.length - 1]; + // If we only match __root__, we ended up not matching any route at all, so + // we fall back to the pathname. + return lastMatch?.routeId !== '__root__' ? lastMatch : undefined; + }; + return { ...browserTracingIntegrationInstance, + setup(client) { + // Registered before `afterAllSetup` so the provider is in place by the time the pageload span + // is named. + setRouteProvider( + createUrlRouteProvider( + url => resolveRouteMatch(url.pathname, castRouterInstance.options.parseSearch(url.search))?.routeId, + ), + client, + ); + + browserTracingIntegrationInstance.setup?.(client); + }, afterAllSetup(client) { browserTracingIntegrationInstance.afterAllSetup(client); - const resolveRouteMatch = (pathname: string, search: unknown): VendoredTanstackRouterRouteMatch | undefined => { - const matchedRoutes = castRouterInstance.matchRoutes(pathname, search as {}, { - preload: false, - throwOnError: false, - }); - const lastMatch = matchedRoutes[matchedRoutes.length - 1]; - // If we only match __root__, we ended up not matching any route at all, so - // we fall back to the pathname. - return lastMatch?.routeId !== '__root__' ? lastMatch : undefined; - }; - const applyRouteMatch = ( span: NonNullable>, match: VendoredTanstackRouterRouteMatch | undefined, diff --git a/packages/solid/src/tanstackrouter.ts b/packages/solid/src/tanstackrouter.ts index 29dd7874924a..bd923afc15b6 100644 --- a/packages/solid/src/tanstackrouter.ts +++ b/packages/solid/src/tanstackrouter.ts @@ -17,9 +17,11 @@ import { import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; import type { Integration } from '@sentry/core'; import { + createUrlRouteProvider, hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, + setRouteProvider, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, filterCollectedUrl, } from '@sentry/core'; @@ -53,19 +55,29 @@ export function tanstackRouterBrowserTracingIntegration( const { instrumentPageLoad = true, instrumentNavigation = true } = options; + const resolveRouteMatch = (pathname: string, search: Record): RouteMatch | undefined => { + const matchedRoutes = router.matchRoutes(pathname, search, { preload: false, throwOnError: false }); + const lastMatch = matchedRoutes[matchedRoutes.length - 1]; + // If we only match __root__, we ended up not matching any route at all, so + // we fall back to the pathname. + return lastMatch?.routeId !== '__root__' ? lastMatch : undefined; + }; + return { ...browserTracingIntegrationInstance, + setup(client) { + // Registered before `afterAllSetup` so the provider is in place by the time the pageload span + // is named. + setRouteProvider( + createUrlRouteProvider(url => resolveRouteMatch(url.pathname, router.options.parseSearch(url.search))?.routeId), + client, + ); + + browserTracingIntegrationInstance.setup?.(client); + }, afterAllSetup(client) { browserTracingIntegrationInstance.afterAllSetup(client); - const resolveRouteMatch = (pathname: string, search: Record): RouteMatch | undefined => { - const matchedRoutes = router.matchRoutes(pathname, search, { preload: false, throwOnError: false }); - const lastMatch = matchedRoutes[matchedRoutes.length - 1]; - // If we only match __root__, we ended up not matching any route at all, so - // we fall back to the pathname. - return lastMatch?.routeId !== '__root__' ? lastMatch : undefined; - }; - const applyRouteMatch = ( span: NonNullable>, match: RouteMatch | undefined, diff --git a/packages/vue/src/tanstackrouter.ts b/packages/vue/src/tanstackrouter.ts index ed2c659c76f9..86a1a75b7cd1 100644 --- a/packages/vue/src/tanstackrouter.ts +++ b/packages/vue/src/tanstackrouter.ts @@ -17,9 +17,11 @@ import { import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; import type { Integration } from '@sentry/core'; import { + createUrlRouteProvider, hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, + setRouteProvider, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, filterCollectedUrl, } from '@sentry/core'; @@ -58,20 +60,30 @@ export function tanstackRouterBrowserTracingIntegration( const { instrumentPageLoad = true, instrumentNavigation = true } = options; + const resolveRouteMatch = (pathname: string, search: unknown): RouteMatch | undefined => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const matchedRoutes = router.matchRoutes(pathname, search as any, { preload: false, throwOnError: false }); + const lastMatch = matchedRoutes[matchedRoutes.length - 1]; + // If we only match __root__, we ended up not matching any route at all, so + // we fall back to the pathname. + return lastMatch?.routeId !== '__root__' ? lastMatch : undefined; + }; + return { ...browserTracingIntegrationInstance, + setup(client) { + // Registered before `afterAllSetup` so the provider is in place by the time the pageload span + // is named. + setRouteProvider( + createUrlRouteProvider(url => resolveRouteMatch(url.pathname, router.options.parseSearch(url.search))?.routeId), + client, + ); + + browserTracingIntegrationInstance.setup?.(client); + }, afterAllSetup(client) { browserTracingIntegrationInstance.afterAllSetup(client); - const resolveRouteMatch = (pathname: string, search: unknown): RouteMatch | undefined => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const matchedRoutes = router.matchRoutes(pathname, search as any, { preload: false, throwOnError: false }); - const lastMatch = matchedRoutes[matchedRoutes.length - 1]; - // If we only match __root__, we ended up not matching any route at all, so - // we fall back to the pathname. - return lastMatch?.routeId !== '__root__' ? lastMatch : undefined; - }; - const applyRouteMatch = ( span: NonNullable>, match: RouteMatch | undefined,