Skip to content

Commit 396112c

Browse files
authored
feat(solidjs): Add solid router instrumentation (#12263)
To use this integration: ```javascript import * as Sentry from "@sentry/solidjs"; import {Route, Router, useBeforeLeave, useLocation} from "@solidjs/router" Sentry.init({ dsn: <dsn> integrations: [ Sentry.solidRouterBrowserTracingIntegration({ useBeforeLeave, useLocation }), ], tracesSampleRate: 1.0, // Capture 100% of the transactions debug: true, }); const SentryRouter = Sentry.withSentryRouterRouting(Router) render(() => ( <SentryRouter> // ... routes here // <Route .../> </SentryRouter> ), root!); ```
1 parent 730c794 commit 396112c

File tree

6 files changed

+244
-9
lines changed

6 files changed

+244
-9
lines changed

packages/solidjs/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,44 @@
77
# Official Sentry SDK for SolidJS
88

99
This SDK is work in progress, and should not be used before officially released.
10+
11+
# Solid Router
12+
13+
The Solid Router instrumentation uses the Solid Router library to create navigation spans to ensure you collect
14+
meaningful performance data about the health of your page loads and associated requests.
15+
16+
Add `Sentry.solidRouterBrowserTracingIntegration` instead of the regular `Sentry.browserTracingIntegration` and provide
17+
the hooks it needs to enable performance tracing:
18+
19+
`useBeforeLeave` from `@solidjs/router`
20+
`useLocation` from `@solidjs/router`
21+
22+
Make sure `Sentry.solidRouterBrowserTracingIntegration` is initialized by your `Sentry.init` call, before you wrap
23+
`Router`. Otherwise, the routing instrumentation may not work properly.
24+
25+
Wrap `Router`, `MemoryRouter` or `HashRouter` from `@solidjs/router` using `Sentry.withSentryRouterRouting`. This
26+
creates a higher order component, which will enable Sentry to reach your router context.
27+
28+
```js
29+
import * as Sentry from '@sentry/solidjs';
30+
import { Route, Router, useBeforeLeave, useLocation } from '@solidjs/router';
31+
32+
Sentry.init({
33+
dsn: '__PUBLIC_DSN__',
34+
integrations: [Sentry.solidRouterBrowserTracingIntegration({ useBeforeLeave, useLocation })],
35+
tracesSampleRate: 1.0, // Capture 100% of the transactions
36+
debug: true,
37+
});
38+
39+
const SentryRouter = Sentry.withSentryRouterRouting(Router);
40+
41+
render(
42+
() => (
43+
<SentryRouter>
44+
<Route path="/" component={App} />
45+
...
46+
</SentryRouter>
47+
),
48+
document.getElementById('root'),
49+
);
50+
```

packages/solidjs/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@
4444
"dependencies": {
4545
"@sentry/browser": "8.7.0",
4646
"@sentry/core": "8.7.0",
47-
"@sentry/types": "8.7.0"
47+
"@sentry/types": "8.7.0",
48+
"@sentry/utils": "8.7.0"
4849
},
4950
"peerDependencies": {
50-
"solid-js": "1.8.x"
51+
"solid-js": "^1.8.4"
5152
},
5253
"devDependencies": {
5354
"@solidjs/testing-library": "0.8.5",
54-
"solid-js": "1.8.11",
55+
"solid-js": "^1.8.11",
5556
"vite-plugin-solid": "^2.8.2"
5657
},
5758
"scripts": {

packages/solidjs/src/debug-build.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
declare const __DEBUG_BUILD__: boolean;
2+
3+
/**
4+
* This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.
5+
*
6+
* ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
7+
*/
8+
export const DEBUG_BUILD = __DEBUG_BUILD__;

packages/solidjs/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from '@sentry/browser';
22

33
export { init } from './sdk';
4+
5+
export * from './solidrouter';

packages/solidjs/src/solidrouter.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import {
2+
browserTracingIntegration,
3+
getActiveSpan,
4+
getRootSpan,
5+
spanToJSON,
6+
startBrowserTracingNavigationSpan,
7+
} from '@sentry/browser';
8+
import {
9+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
10+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
11+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
12+
getClient,
13+
} from '@sentry/core';
14+
import type { Client, Integration, Span } from '@sentry/types';
15+
import { logger } from '@sentry/utils';
16+
import { createEffect, mergeProps, splitProps } from 'solid-js';
17+
import type { Component, JSX, ParentProps } from 'solid-js';
18+
import { createComponent } from 'solid-js/web';
19+
import { DEBUG_BUILD } from './debug-build';
20+
21+
// Vendored solid router types so that we don't need to depend on solid router.
22+
// These are not exhaustive and loose on purpose.
23+
interface Location {
24+
pathname: string;
25+
}
26+
27+
interface BeforeLeaveEventArgs {
28+
from: Location;
29+
to: string | number;
30+
}
31+
32+
interface RouteSectionProps<T = unknown> {
33+
location: Location;
34+
data?: T;
35+
children?: JSX.Element;
36+
}
37+
38+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
39+
type RouteDefinition<S extends string | string[] = any, T = unknown> = {
40+
path?: S;
41+
children?: RouteDefinition | RouteDefinition[];
42+
component?: Component<RouteSectionProps<T>>;
43+
};
44+
45+
interface RouterProps {
46+
base?: string;
47+
root?: Component<RouteSectionProps>;
48+
children?: JSX.Element | RouteDefinition | RouteDefinition[];
49+
}
50+
51+
interface SolidRouterOptions {
52+
useBeforeLeave: UserBeforeLeave;
53+
useLocation: UseLocation;
54+
}
55+
56+
type UserBeforeLeave = (listener: (e: BeforeLeaveEventArgs) => void) => void;
57+
type UseLocation = () => Location;
58+
59+
const CLIENTS_WITH_INSTRUMENT_NAVIGATION = new WeakSet<Client>();
60+
61+
let _useBeforeLeave: UserBeforeLeave;
62+
let _useLocation: UseLocation;
63+
64+
function handleNavigation(location: string): void {
65+
const client = getClient();
66+
if (!client || !CLIENTS_WITH_INSTRUMENT_NAVIGATION.has(client)) {
67+
return;
68+
}
69+
70+
startBrowserTracingNavigationSpan(client, {
71+
name: location,
72+
attributes: {
73+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
74+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solidjs.solidrouter',
75+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
76+
},
77+
});
78+
}
79+
80+
function getActiveRootSpan(): Span | undefined {
81+
const span = getActiveSpan();
82+
return span ? getRootSpan(span) : undefined;
83+
}
84+
85+
/** Pass-through component in case user didn't specify a root **/
86+
function SentryDefaultRoot(props: ParentProps): JSX.Element {
87+
return props.children;
88+
}
89+
90+
/**
91+
* Unfortunately, we cannot use router hooks directly in the Router, so we
92+
* need to wrap the `root` prop to instrument navigation.
93+
*/
94+
function withSentryRouterRoot(Root: Component<RouteSectionProps>): Component<RouteSectionProps> {
95+
const SentryRouterRoot = (props: RouteSectionProps): JSX.Element => {
96+
// TODO: This is a rudimentary first version of handling navigation spans
97+
// It does not
98+
// - use query params
99+
// - parameterize the route
100+
101+
_useBeforeLeave(({ to }: BeforeLeaveEventArgs) => {
102+
// `to` could be `-1` if the browser back-button was used
103+
handleNavigation(to.toString());
104+
});
105+
106+
const location = _useLocation();
107+
createEffect(() => {
108+
const name = location.pathname;
109+
const rootSpan = getActiveRootSpan();
110+
111+
if (rootSpan) {
112+
const { op, description } = spanToJSON(rootSpan);
113+
114+
// We only need to update navigation spans that have been created by
115+
// a browser back-button navigation (stored as `-1` by solid router)
116+
// everything else was already instrumented correctly in `useBeforeLeave`
117+
if (op === 'navigation' && description === '-1') {
118+
rootSpan.updateName(name);
119+
}
120+
}
121+
});
122+
123+
return createComponent(Root, props);
124+
};
125+
126+
return SentryRouterRoot;
127+
}
128+
129+
/**
130+
* A browser tracing integration that uses Solid Router to instrument navigations.
131+
*/
132+
export function solidRouterBrowserTracingIntegration(
133+
options: Parameters<typeof browserTracingIntegration>[0] & SolidRouterOptions,
134+
): Integration {
135+
const integration = browserTracingIntegration({
136+
...options,
137+
instrumentNavigation: false,
138+
});
139+
140+
const { instrumentNavigation = true, useBeforeLeave, useLocation } = options;
141+
142+
return {
143+
...integration,
144+
setup() {
145+
_useBeforeLeave = useBeforeLeave;
146+
_useLocation = useLocation;
147+
},
148+
afterAllSetup(client) {
149+
integration.afterAllSetup(client);
150+
151+
if (instrumentNavigation) {
152+
CLIENTS_WITH_INSTRUMENT_NAVIGATION.add(client);
153+
}
154+
},
155+
};
156+
}
157+
158+
/**
159+
* A higher-order component to instrument Solid Router to create navigation spans.
160+
*/
161+
export function withSentryRouterRouting(Router: Component<RouterProps>): Component<RouterProps> {
162+
if (!_useBeforeLeave || !_useLocation) {
163+
DEBUG_BUILD &&
164+
logger.warn(`solidRouterBrowserTracingIntegration was unable to wrap Solid Router because of one or more missing hooks.
165+
useBeforeLeave: ${_useBeforeLeave}. useLocation: ${_useLocation}.`);
166+
167+
return Router;
168+
}
169+
170+
const SentryRouter = (props: RouterProps): JSX.Element => {
171+
const [local, others] = splitProps(props, ['root']);
172+
// We need to wrap root here in case the user passed in their own root
173+
const Root = withSentryRouterRoot(local.root ? local.root : SentryDefaultRoot);
174+
175+
return createComponent(Router, mergeProps({ root: Root }, others));
176+
};
177+
178+
return SentryRouter;
179+
}

yarn.lock

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8452,6 +8452,7 @@
84528452
"@types/unist" "*"
84538453

84548454
"@types/history-4@npm:@types/[email protected]", "@types/history-5@npm:@types/[email protected]", "@types/history@*":
8455+
name "@types/history-4"
84558456
version "4.7.8"
84568457
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934"
84578458
integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==
@@ -26109,6 +26110,7 @@ react-is@^18.0.0:
2610926110
"@remix-run/router" "1.0.2"
2611026111

2611126112
"react-router-6@npm:[email protected]", [email protected]:
26113+
name react-router-6
2611226114
version "6.3.0"
2611326115
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557"
2611426116
integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==
@@ -27492,7 +27494,7 @@ seroval-plugins@^1.0.3:
2749227494
resolved "https://registry.yarnpkg.com/seroval-plugins/-/seroval-plugins-1.0.7.tgz#c02511a1807e9bc8f68a91fbec13474fa9cea670"
2749327495
integrity sha512-GO7TkWvodGp6buMEX9p7tNyIkbwlyuAWbI6G9Ec5bhcm7mQdu3JOK1IXbEUwb3FVzSc363GraG/wLW23NSavIw==
2749427496

27495-
seroval@^1.0.3:
27497+
seroval@^1.0.4:
2749627498
version "1.0.7"
2749727499
resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.0.7.tgz#ee48ad8ba69f1595bdd5c55d1a0d1da29dee7455"
2749827500
integrity sha512-n6ZMQX5q0Vn19Zq7CIKNIo7E75gPkGCFUEqDpa8jgwpYr/vScjqnQ6H09t1uIiZ0ZSK0ypEGvrYK2bhBGWsGdw==
@@ -27934,13 +27936,13 @@ socks@^2.6.2:
2793427936
ip "^2.0.0"
2793527937
smart-buffer "^4.2.0"
2793627938

27937-
27938-
version "1.8.11"
27939-
resolved "https://registry.yarnpkg.com/solid-js/-/solid-js-1.8.11.tgz#0e7496a9834720b10fe739eaac250221d3f72cd5"
27940-
integrity sha512-WdwmER+TwBJiN4rVQTVBxocg+9pKlOs41KzPYntrC86xO5sek8TzBYozPEZPL1IRWDouf2lMrvSbIs3CanlPvQ==
27939+
solid-js@^1.8.11:
27940+
version "1.8.17"
27941+
resolved "https://registry.yarnpkg.com/solid-js/-/solid-js-1.8.17.tgz#780ed6f0fd8633009d1b3c29d56bf6b6bb33bd50"
27942+
integrity sha512-E0FkUgv9sG/gEBWkHr/2XkBluHb1fkrHywUgA6o6XolPDCJ4g1HaLmQufcBBhiF36ee40q+HpG/vCZu7fLpI3Q==
2794127943
dependencies:
2794227944
csstype "^3.1.0"
27943-
seroval "^1.0.3"
27945+
seroval "^1.0.4"
2794427946
seroval-plugins "^1.0.3"
2794527947

2794627948
solid-refresh@^0.6.3:
@@ -28442,6 +28444,7 @@ string-template@~0.2.1:
2844228444
integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=
2844328445

2844428446
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
28447+
name string-width-cjs
2844528448
version "4.2.3"
2844628449
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
2844728450
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -31190,6 +31193,7 @@ workerpool@^6.4.0:
3119031193
integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A==
3119131194

3119231195
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
31196+
name wrap-ansi-cjs
3119331197
version "7.0.0"
3119431198
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
3119531199
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==

0 commit comments

Comments
 (0)