Skip to content

feat(solidjs): Add solid router instrumentation #12263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/solidjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,44 @@
# Official Sentry SDK for SolidJS

This SDK is work in progress, and should not be used before officially released.

# Solid Router

The Solid Router instrumentation uses the Solid Router library to create navigation spans to ensure you collect
meaningful performance data about the health of your page loads and associated requests.

Add `Sentry.solidRouterBrowserTracingIntegration` instead of the regular `Sentry.browserTracingIntegration` and provide
the hooks it needs to enable performance tracing:

`useBeforeLeave` from `@solidjs/router`
`useLocation` from `@solidjs/router`

Make sure `Sentry.solidRouterBrowserTracingIntegration` is initialized by your `Sentry.init` call, before you wrap
`Router`. Otherwise, the routing instrumentation may not work properly.

Wrap `Router`, `MemoryRouter` or `HashRouter` from `@solidjs/router` using `Sentry.withSentryRouterRouting`. This
creates a higher order component, which will enable Sentry to reach your router context.

```js
import * as Sentry from '@sentry/solidjs';
import { Route, Router, useBeforeLeave, useLocation } from '@solidjs/router';

Sentry.init({
dsn: '__PUBLIC_DSN__',
integrations: [Sentry.solidRouterBrowserTracingIntegration({ useBeforeLeave, useLocation })],
tracesSampleRate: 1.0, // Capture 100% of the transactions
debug: true,
});

const SentryRouter = Sentry.withSentryRouterRouting(Router);

render(
() => (
<SentryRouter>
<Route path="/" component={App} />
...
</SentryRouter>
),
document.getElementById('root'),
);
```
7 changes: 4 additions & 3 deletions packages/solidjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@
"dependencies": {
"@sentry/browser": "8.7.0",
"@sentry/core": "8.7.0",
"@sentry/types": "8.7.0"
"@sentry/types": "8.7.0",
"@sentry/utils": "8.7.0"
},
"peerDependencies": {
"solid-js": "1.8.x"
"solid-js": "^1.8.4"
},
"devDependencies": {
"@solidjs/testing-library": "0.8.5",
"solid-js": "1.8.11",
"solid-js": "^1.8.11",
"vite-plugin-solid": "^2.8.2"
},
"scripts": {
Expand Down
8 changes: 8 additions & 0 deletions packages/solidjs/src/debug-build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare const __DEBUG_BUILD__: boolean;

/**
* 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.
*
* ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
*/
export const DEBUG_BUILD = __DEBUG_BUILD__;
2 changes: 2 additions & 0 deletions packages/solidjs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from '@sentry/browser';

export { init } from './sdk';

export * from './solidrouter';
179 changes: 179 additions & 0 deletions packages/solidjs/src/solidrouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import {
browserTracingIntegration,
getActiveSpan,
getRootSpan,
spanToJSON,
startBrowserTracingNavigationSpan,
} from '@sentry/browser';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
getClient,
} from '@sentry/core';
import type { Client, Integration, Span } from '@sentry/types';
import { logger } from '@sentry/utils';
import { createEffect, mergeProps, splitProps } from 'solid-js';
import type { Component, JSX, ParentProps } from 'solid-js';
import { createComponent } from 'solid-js/web';
import { DEBUG_BUILD } from './debug-build';

// Vendored solid router types so that we don't need to depend on solid router.
// These are not exhaustive and loose on purpose.
interface Location {
pathname: string;
}

interface BeforeLeaveEventArgs {
from: Location;
to: string | number;
}

interface RouteSectionProps<T = unknown> {
location: Location;
data?: T;
children?: JSX.Element;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type RouteDefinition<S extends string | string[] = any, T = unknown> = {
path?: S;
children?: RouteDefinition | RouteDefinition[];
component?: Component<RouteSectionProps<T>>;
};

interface RouterProps {
base?: string;
root?: Component<RouteSectionProps>;
children?: JSX.Element | RouteDefinition | RouteDefinition[];
}

interface SolidRouterOptions {
useBeforeLeave: UserBeforeLeave;
useLocation: UseLocation;
}

type UserBeforeLeave = (listener: (e: BeforeLeaveEventArgs) => void) => void;
type UseLocation = () => Location;

const CLIENTS_WITH_INSTRUMENT_NAVIGATION = new WeakSet<Client>();

let _useBeforeLeave: UserBeforeLeave;
let _useLocation: UseLocation;

function handleNavigation(location: string): void {
const client = getClient();
if (!client || !CLIENTS_WITH_INSTRUMENT_NAVIGATION.has(client)) {
return;
}

startBrowserTracingNavigationSpan(client, {
name: location,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solidjs.solidrouter',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
},
});
}

function getActiveRootSpan(): Span | undefined {
const span = getActiveSpan();
return span ? getRootSpan(span) : undefined;
}

/** Pass-through component in case user didn't specify a root **/
function SentryDefaultRoot(props: ParentProps): JSX.Element {
return props.children;
}

/**
* Unfortunately, we cannot use router hooks directly in the Router, so we
* need to wrap the `root` prop to instrument navigation.
*/
function withSentryRouterRoot(Root: Component<RouteSectionProps>): Component<RouteSectionProps> {
const SentryRouterRoot = (props: RouteSectionProps): JSX.Element => {
// TODO: This is a rudimentary first version of handling navigation spans
// It does not
// - use query params
// - parameterize the route

_useBeforeLeave(({ to }: BeforeLeaveEventArgs) => {
// `to` could be `-1` if the browser back-button was used
handleNavigation(to.toString());
});

const location = _useLocation();
createEffect(() => {
const name = location.pathname;
const rootSpan = getActiveRootSpan();

if (rootSpan) {
const { op, description } = spanToJSON(rootSpan);

// We only need to update navigation spans that have been created by
// a browser back-button navigation (stored as `-1` by solid router)
// everything else was already instrumented correctly in `useBeforeLeave`
if (op === 'navigation' && description === '-1') {
rootSpan.updateName(name);
}
}
});

return createComponent(Root, props);
};

return SentryRouterRoot;
}

/**
* A browser tracing integration that uses Solid Router to instrument navigations.
*/
export function solidRouterBrowserTracingIntegration(
options: Parameters<typeof browserTracingIntegration>[0] & SolidRouterOptions,
): Integration {
const integration = browserTracingIntegration({
...options,
instrumentNavigation: false,
});

const { instrumentNavigation = true, useBeforeLeave, useLocation } = options;

return {
...integration,
setup() {
_useBeforeLeave = useBeforeLeave;
_useLocation = useLocation;
Comment on lines +145 to +146
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we directly import and use useLocation and useBeforeLeave from solidjs/router instead?

I hope we don't have to await import it to make the optionalDependencies work properly 😬

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No we can't. I spent the entire day trying to figure this out with the help of @lforst - but basically the compiler detects that these hooks are used outside of the router and the invariant that detects this is baked into the bundle.

The router context is different :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dang that sucks :(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, was hoping it would work. Maybe we can talk to the maintainer to find some workaround.

},
afterAllSetup(client) {
integration.afterAllSetup(client);

if (instrumentNavigation) {
CLIENTS_WITH_INSTRUMENT_NAVIGATION.add(client);
}
},
};
}

/**
* A higher-order component to instrument Solid Router to create navigation spans.
*/
export function withSentryRouterRouting(Router: Component<RouterProps>): Component<RouterProps> {
if (!_useBeforeLeave || !_useLocation) {
DEBUG_BUILD &&
logger.warn(`solidRouterBrowserTracingIntegration was unable to wrap Solid Router because of one or more missing hooks.
useBeforeLeave: ${_useBeforeLeave}. useLocation: ${_useLocation}.`);

return Router;
}

const SentryRouter = (props: RouterProps): JSX.Element => {
const [local, others] = splitProps(props, ['root']);
// We need to wrap root here in case the user passed in their own root
const Root = withSentryRouterRoot(local.root ? local.root : SentryDefaultRoot);

return createComponent(Router, mergeProps({ root: Root }, others));
};

return SentryRouter;
}
16 changes: 10 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8452,6 +8452,7 @@
"@types/unist" "*"

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

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

seroval@^1.0.3:
seroval@^1.0.4:
version "1.0.7"
resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.0.7.tgz#ee48ad8ba69f1595bdd5c55d1a0d1da29dee7455"
integrity sha512-n6ZMQX5q0Vn19Zq7CIKNIo7E75gPkGCFUEqDpa8jgwpYr/vScjqnQ6H09t1uIiZ0ZSK0ypEGvrYK2bhBGWsGdw==
Expand Down Expand Up @@ -27934,13 +27936,13 @@ socks@^2.6.2:
ip "^2.0.0"
smart-buffer "^4.2.0"

[email protected]:
version "1.8.11"
resolved "https://registry.yarnpkg.com/solid-js/-/solid-js-1.8.11.tgz#0e7496a9834720b10fe739eaac250221d3f72cd5"
integrity sha512-WdwmER+TwBJiN4rVQTVBxocg+9pKlOs41KzPYntrC86xO5sek8TzBYozPEZPL1IRWDouf2lMrvSbIs3CanlPvQ==
solid-js@^1.8.11:
version "1.8.17"
resolved "https://registry.yarnpkg.com/solid-js/-/solid-js-1.8.17.tgz#780ed6f0fd8633009d1b3c29d56bf6b6bb33bd50"
integrity sha512-E0FkUgv9sG/gEBWkHr/2XkBluHb1fkrHywUgA6o6XolPDCJ4g1HaLmQufcBBhiF36ee40q+HpG/vCZu7fLpI3Q==
dependencies:
csstype "^3.1.0"
seroval "^1.0.3"
seroval "^1.0.4"
seroval-plugins "^1.0.3"

solid-refresh@^0.6.3:
Expand Down Expand Up @@ -28442,6 +28444,7 @@ string-template@~0.2.1:
integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=

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

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