Skip to content

Commit 58e80ed

Browse files
committed
feat: add support for TanStack Router Solid
1 parent d7538cd commit 58e80ed

File tree

3 files changed

+212
-0
lines changed

3 files changed

+212
-0
lines changed

packages/solid/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@
3838
"types": "./solidrouter.d.ts",
3939
"default": "./build/cjs/solidrouter.js"
4040
}
41+
},
42+
"./tanstack-router": {
43+
"import": {
44+
"types": "./tanstackrouter.d.ts",
45+
"default": "./build/esm/tanstackrouter.js"
46+
},
47+
"require": {
48+
"types": "./tanstackrouter.d.ts",
49+
"default": "./build/cjs/tanstackrouter.js"
50+
}
4151
}
4252
},
4353
"publishConfig": {
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2021-present Tanner Linsley
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
// The following types are vendored types from TanStack Router, so we don't have to depend on the actual package
28+
29+
export interface VendoredTanstackRouter {
30+
history: VendoredTanstackRouterHistory;
31+
state: VendoredTanstackRouterState;
32+
options: {
33+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
34+
parseSearch: (search: string) => Record<string, any>;
35+
};
36+
matchRoutes: (
37+
pathname: string,
38+
// eslint-disable-next-line @typescript-eslint/ban-types
39+
locationSearch: {},
40+
opts?: {
41+
preload?: boolean;
42+
throwOnError?: boolean;
43+
},
44+
) => Array<VendoredTanstackRouterRouteMatch>;
45+
subscribe(
46+
eventType: 'onResolved' | 'onBeforeNavigate',
47+
callback: (stateUpdate: {
48+
toLocation: VendoredTanstackRouterLocation;
49+
fromLocation?: VendoredTanstackRouterLocation;
50+
}) => void,
51+
): () => void;
52+
}
53+
54+
interface VendoredTanstackRouterLocation {
55+
pathname: string;
56+
// eslint-disable-next-line @typescript-eslint/ban-types
57+
search: {};
58+
state: string;
59+
}
60+
61+
interface VendoredTanstackRouterHistory {
62+
subscribe: (cb: () => void) => () => void;
63+
}
64+
65+
interface VendoredTanstackRouterState {
66+
matches: Array<VendoredTanstackRouterRouteMatch>;
67+
pendingMatches?: Array<VendoredTanstackRouterRouteMatch>;
68+
}
69+
70+
export interface VendoredTanstackRouterRouteMatch {
71+
routeId: string;
72+
pathname: string;
73+
params: { [key: string]: string };
74+
}

0 commit comments

Comments
 (0)