From ad1b053cc5162f9af7ccf4b8edadd2b36ad802c7 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 | 42 ++++++++++++++++++++-------- packages/solid/src/tanstackrouter.ts | 28 +++++++++++++------ packages/vue/src/tanstackrouter.ts | 30 ++++++++++++++------ 3 files changed, 71 insertions(+), 29 deletions(-) diff --git a/packages/react/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts index f79592599f74..b7bc54b3d848 100644 --- a/packages/react/src/tanstackrouter.ts +++ b/packages/react/src/tanstackrouter.ts @@ -6,7 +6,13 @@ import { WINDOW, } from '@sentry/browser'; import type { Integration } from '@sentry/core/browser'; -import { filterCollectedUrl, hasSpanStreamingEnabled, PAGELOAD_SPAN_NAME_FALLBACK } from '@sentry/core'; +import { + createUrlRouteProvider, + filterCollectedUrl, + hasSpanStreamingEnabled, + PAGELOAD_SPAN_NAME_FALLBACK, + setRouteProvider, +} from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -50,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 3ea2644f2e38..7e290da77350 100644 --- a/packages/solid/src/tanstackrouter.ts +++ b/packages/solid/src/tanstackrouter.ts @@ -14,8 +14,10 @@ import { } from '@sentry/conventions/attributes'; import type { Integration } from '@sentry/core'; import { + createUrlRouteProvider, hasSpanStreamingEnabled, PAGELOAD_SPAN_NAME_FALLBACK, + setRouteProvider, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -51,19 +53,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 4aa4bd1880e9..2a361c6c9606 100644 --- a/packages/vue/src/tanstackrouter.ts +++ b/packages/vue/src/tanstackrouter.ts @@ -14,8 +14,10 @@ import { } from '@sentry/conventions/attributes'; import type { Integration } from '@sentry/core'; import { + createUrlRouteProvider, hasSpanStreamingEnabled, PAGELOAD_SPAN_NAME_FALLBACK, + setRouteProvider, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -56,20 +58,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,