|
| 1 | +import { |
| 2 | + browserTracingIntegration as originalBrowserTracingIntegration, |
| 3 | + startBrowserTracingNavigationSpan, |
| 4 | + startBrowserTracingPageLoadSpan, |
| 5 | + WINDOW, |
| 6 | +} from '@sentry/browser'; |
| 7 | +import type { Integration } from '@sentry/core'; |
| 8 | +import { |
| 9 | + SEMANTIC_ATTRIBUTE_SENTRY_OP, |
| 10 | + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, |
| 11 | + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, |
| 12 | +} from '@sentry/core'; |
| 13 | +import type { VendoredTanstackRouter, VendoredTanstackRouterRouteMatch } from './vendor/tanstackrouter-types'; |
| 14 | + |
| 15 | +/** |
| 16 | + * A custom browser tracing integration for TanStack Router. |
| 17 | + * |
| 18 | + * The minimum compatible version of `@tanstack/solid-router` is `1.64.0`. |
| 19 | + * |
| 20 | + * @param router A TanStack Router `Router` instance that should be used for routing instrumentation. |
| 21 | + * @param options Sentry browser tracing configuration. |
| 22 | + */ |
| 23 | +export function tanstackRouterBrowserTracingIntegration( |
| 24 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 25 | + router: any, // This is `any` because we don't want any type mismatches if TanStack Router changes their types |
| 26 | + options: Parameters<typeof originalBrowserTracingIntegration>[0] = {}, |
| 27 | +): Integration { |
| 28 | + const castRouterInstance: VendoredTanstackRouter = router; |
| 29 | + |
| 30 | + const browserTracingIntegrationInstance = originalBrowserTracingIntegration({ |
| 31 | + ...options, |
| 32 | + instrumentNavigation: false, |
| 33 | + instrumentPageLoad: false, |
| 34 | + }); |
| 35 | + |
| 36 | + const { instrumentPageLoad = true, instrumentNavigation = true } = options; |
| 37 | + |
| 38 | + return { |
| 39 | + ...browserTracingIntegrationInstance, |
| 40 | + afterAllSetup(client) { |
| 41 | + browserTracingIntegrationInstance.afterAllSetup(client); |
| 42 | + |
| 43 | + const initialWindowLocation = WINDOW.location; |
| 44 | + if (instrumentPageLoad && initialWindowLocation) { |
| 45 | + const matchedRoutes = castRouterInstance.matchRoutes( |
| 46 | + initialWindowLocation.pathname, |
| 47 | + castRouterInstance.options.parseSearch(initialWindowLocation.search), |
| 48 | + { preload: false, throwOnError: false }, |
| 49 | + ); |
| 50 | + |
| 51 | + const lastMatch = matchedRoutes[matchedRoutes.length - 1]; |
| 52 | + |
| 53 | + startBrowserTracingPageLoadSpan(client, { |
| 54 | + name: lastMatch ? lastMatch.routeId : initialWindowLocation.pathname, |
| 55 | + attributes: { |
| 56 | + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', |
| 57 | + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router', |
| 58 | + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: lastMatch ? 'route' : 'url', |
| 59 | + ...routeMatchToParamSpanAttributes(lastMatch), |
| 60 | + }, |
| 61 | + }); |
| 62 | + } |
| 63 | + |
| 64 | + if (instrumentNavigation) { |
| 65 | + // The onBeforeNavigate hook is called at the very beginning of a navigation and is only called once per navigation, even when the user is redirected |
| 66 | + castRouterInstance.subscribe('onBeforeNavigate', onBeforeNavigateArgs => { |
| 67 | + // onBeforeNavigate is called during pageloads. We can avoid creating navigation spans by comparing the states of the to and from arguments. |
| 68 | + if (onBeforeNavigateArgs.toLocation.state === onBeforeNavigateArgs.fromLocation?.state) { |
| 69 | + return; |
| 70 | + } |
| 71 | + |
| 72 | + const onResolvedMatchedRoutes = castRouterInstance.matchRoutes( |
| 73 | + onBeforeNavigateArgs.toLocation.pathname, |
| 74 | + onBeforeNavigateArgs.toLocation.search, |
| 75 | + { preload: false, throwOnError: false }, |
| 76 | + ); |
| 77 | + |
| 78 | + const onBeforeNavigateLastMatch = onResolvedMatchedRoutes[onResolvedMatchedRoutes.length - 1]; |
| 79 | + |
| 80 | + const navigationLocation = WINDOW.location; |
| 81 | + const navigationSpan = startBrowserTracingNavigationSpan(client, { |
| 82 | + name: onBeforeNavigateLastMatch ? onBeforeNavigateLastMatch.routeId : navigationLocation.pathname, |
| 83 | + attributes: { |
| 84 | + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', |
| 85 | + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.tanstack_router', |
| 86 | + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: onBeforeNavigateLastMatch ? 'route' : 'url', |
| 87 | + }, |
| 88 | + }); |
| 89 | + |
| 90 | + // In case the user is redirected during navigation we want to update the span with the right value. |
| 91 | + const unsubscribeOnResolved = castRouterInstance.subscribe('onResolved', onResolvedArgs => { |
| 92 | + unsubscribeOnResolved(); |
| 93 | + if (navigationSpan) { |
| 94 | + const onResolvedMatchedRoutes = castRouterInstance.matchRoutes( |
| 95 | + onResolvedArgs.toLocation.pathname, |
| 96 | + onResolvedArgs.toLocation.search, |
| 97 | + { preload: false, throwOnError: false }, |
| 98 | + ); |
| 99 | + |
| 100 | + const onResolvedLastMatch = onResolvedMatchedRoutes[onResolvedMatchedRoutes.length - 1]; |
| 101 | + |
| 102 | + if (onResolvedLastMatch) { |
| 103 | + navigationSpan.updateName(onResolvedLastMatch.routeId); |
| 104 | + navigationSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); |
| 105 | + navigationSpan.setAttributes(routeMatchToParamSpanAttributes(onResolvedLastMatch)); |
| 106 | + } |
| 107 | + } |
| 108 | + }); |
| 109 | + }); |
| 110 | + } |
| 111 | + }, |
| 112 | + }; |
| 113 | +} |
| 114 | + |
| 115 | +function routeMatchToParamSpanAttributes(match: VendoredTanstackRouterRouteMatch | undefined): Record<string, string> { |
| 116 | + if (!match) { |
| 117 | + return {}; |
| 118 | + } |
| 119 | + |
| 120 | + const paramAttributes: Record<string, string> = {}; |
| 121 | + Object.entries(match.params).forEach(([key, value]) => { |
| 122 | + paramAttributes[`url.path.params.${key}`] = value; // TODO(v11): remove attribute which does not adhere to Sentry's semantic convention |
| 123 | + paramAttributes[`url.path.parameter.${key}`] = value; |
| 124 | + paramAttributes[`params.${key}`] = value; // params.[key] is an alias |
| 125 | + }); |
| 126 | + |
| 127 | + return paramAttributes; |
| 128 | +} |
0 commit comments