-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathtanstackrouter.ts
More file actions
138 lines (121 loc) · 5.66 KB
/
tanstackrouter.ts
File metadata and controls
138 lines (121 loc) · 5.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import {
browserTracingIntegration as originalBrowserTracingIntegration,
startBrowserTracingNavigationSpan,
startBrowserTracingPageLoadSpan,
WINDOW,
} from '@sentry/browser';
import type { Integration } from '@sentry/core';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/core';
import type { AnyRouter } from '@tanstack/solid-router';
type RouteMatch = ReturnType<AnyRouter['matchRoutes']>[number];
/**
* A custom browser tracing integration for TanStack Router.
*
* The minimum compatible version of `@tanstack/solid-router` is `1.64.0
*
* @param router A TanStack Router `Router` instance that should be used for routing instrumentation.
* @param options Sentry browser tracing configuration.
*/
export function tanstackRouterBrowserTracingIntegration<R extends AnyRouter>(
router: R,
options: Parameters<typeof originalBrowserTracingIntegration>[0] = {},
): Integration {
const browserTracingIntegrationInstance = originalBrowserTracingIntegration({
...options,
instrumentNavigation: false,
instrumentPageLoad: false,
});
const { instrumentPageLoad = true, instrumentNavigation = true } = options;
return {
...browserTracingIntegrationInstance,
afterAllSetup(client) {
browserTracingIntegrationInstance.afterAllSetup(client);
const initialWindowLocation = WINDOW.location;
if (instrumentPageLoad && initialWindowLocation) {
const matchedRoutes = router.matchRoutes(
initialWindowLocation.pathname,
router.options.parseSearch(initialWindowLocation.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.
const routeMatch = lastMatch?.routeId !== '__root__' ? lastMatch : undefined;
startBrowserTracingPageLoadSpan(client, {
name: routeMatch ? routeMatch.routeId : initialWindowLocation.pathname,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeMatch ? 'route' : 'url',
...routeMatchToParamSpanAttributes(routeMatch),
},
});
}
if (instrumentNavigation) {
// 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
router.subscribe('onBeforeNavigate', onBeforeNavigateArgs => {
// onBeforeNavigate is called during pageloads. We can avoid creating navigation spans by:
// 1. Checking if there's no fromLocation (initial pageload)
// 2. Comparing the states of the to and from arguments
if (
!onBeforeNavigateArgs.fromLocation ||
onBeforeNavigateArgs.toLocation.state === onBeforeNavigateArgs.fromLocation.state
) {
return;
}
const matchedRoutesOnBeforeNavigate = router.matchRoutes(
onBeforeNavigateArgs.toLocation.pathname,
onBeforeNavigateArgs.toLocation.search,
{ preload: false, throwOnError: false },
);
const onBeforeNavigateLastMatch = matchedRoutesOnBeforeNavigate[matchedRoutesOnBeforeNavigate.length - 1];
const onBeforeNavigateRouteMatch =
onBeforeNavigateLastMatch?.routeId !== '__root__' ? onBeforeNavigateLastMatch : undefined;
const navigationLocation = WINDOW.location;
const navigationSpan = startBrowserTracingNavigationSpan(client, {
name: onBeforeNavigateRouteMatch ? onBeforeNavigateRouteMatch.routeId : navigationLocation.pathname,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.tanstack_router',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: onBeforeNavigateRouteMatch ? 'route' : 'url',
},
});
// In case the user is redirected during navigation we want to update the span with the right value.
const unsubscribeOnResolved = router.subscribe('onResolved', onResolvedArgs => {
unsubscribeOnResolved();
if (navigationSpan) {
const matchedRoutesOnResolved = router.matchRoutes(
onResolvedArgs.toLocation.pathname,
onResolvedArgs.toLocation.search,
{ preload: false, throwOnError: false },
);
const onResolvedLastMatch = matchedRoutesOnResolved[matchedRoutesOnResolved.length - 1];
const onResolvedRouteMatch =
onResolvedLastMatch?.routeId !== '__root__' ? onResolvedLastMatch : undefined;
if (onResolvedRouteMatch) {
navigationSpan.updateName(onResolvedRouteMatch.routeId);
navigationSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
navigationSpan.setAttributes(routeMatchToParamSpanAttributes(onResolvedRouteMatch));
}
}
});
});
}
},
};
}
function routeMatchToParamSpanAttributes(match: RouteMatch | undefined): Record<string, string> {
if (!match) {
return {};
}
const paramAttributes: Record<string, string> = {};
Object.entries(match.params as Record<string, string>).forEach(([key, value]) => {
paramAttributes[`url.path.parameter.${key}`] = value;
paramAttributes[`params.${key}`] = value; // params.[key] is an alias
});
return paramAttributes;
}