Skip to content

Commit 9c5baf7

Browse files
committed
feat(solidjs): Add solid router instrumentation
1 parent bbe7be5 commit 9c5baf7

File tree

6 files changed

+196
-2
lines changed

6 files changed

+196
-2
lines changed

packages/solidjs/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"dependencies": {
4545
"@sentry/browser": "8.5.0",
4646
"@sentry/core": "8.5.0",
47-
"@sentry/types": "8.5.0"
47+
"@sentry/types": "8.5.0",
48+
"@sentry/utils": "8.5.0"
4849
},
4950
"peerDependencies": {
5051
"solid-js": "1.8.x"
@@ -54,6 +55,9 @@
5455
"solid-js": "1.8.11",
5556
"vite-plugin-solid": "^2.8.2"
5657
},
58+
"optionalDependencies": {
59+
"@solidjs/router": "0.13.3"
60+
},
5761
"scripts": {
5862
"build": "run-p build:transpile build:types",
5963
"build:dev": "yarn build",
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
22

3-
export default makeNPMConfigVariants(makeBaseNPMConfig());
3+
export default makeNPMConfigVariants(
4+
makeBaseNPMConfig({
5+
packageSpecificConfig: {
6+
external: ['solid-js', 'solid-js/web', '@solidjs/router'],
7+
},
8+
}),
9+
);

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

yarn.lock

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7768,6 +7768,11 @@
77687768
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
77697769
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
77707770

7771+
"@solidjs/[email protected]":
7772+
version "0.13.3"
7773+
resolved "https://registry.yarnpkg.com/@solidjs/router/-/router-0.13.3.tgz#f520362d716a58c0416a33372ae9e5ed1a26be9a"
7774+
integrity sha512-p8zznlvnN3KySMXqT8irhubgDNTETNa/guaGHU/cZl7kuiPO3PmkWNYfoNCygtEpoxLmLpf62/ZKeyhFdZexsw==
7775+
77717776
"@solidjs/[email protected]":
77727777
version "0.8.5"
77737778
resolved "https://registry.yarnpkg.com/@solidjs/testing-library/-/testing-library-0.8.5.tgz#97061b2286d8641bd43bf474e624c3bb47e486a6"
@@ -8452,6 +8457,7 @@
84528457
"@types/unist" "*"
84538458

84548459
"@types/history-4@npm:@types/[email protected]", "@types/history-5@npm:@types/[email protected]", "@types/history@*":
8460+
name "@types/history-4"
84558461
version "4.7.8"
84568462
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934"
84578463
integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==
@@ -26109,6 +26115,7 @@ react-is@^18.0.0:
2610926115
"@remix-run/router" "1.0.2"
2611026116

2611126117
"react-router-6@npm:[email protected]", [email protected]:
26118+
name react-router-6
2611226119
version "6.3.0"
2611326120
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557"
2611426121
integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==
@@ -28442,6 +28449,7 @@ string-template@~0.2.1:
2844228449
integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=
2844328450

2844428451
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
28452+
name string-width-cjs
2844528453
version "4.2.3"
2844628454
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
2844728455
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -31190,6 +31198,7 @@ workerpool@^6.4.0:
3119031198
integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A==
3119131199

3119231200
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
31201+
name wrap-ansi-cjs
3119331202
version "7.0.0"
3119431203
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
3119531204
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==

0 commit comments

Comments
 (0)