diff --git a/packages/solidjs/README.md b/packages/solidjs/README.md
index 3e37b30e7032..00d451763624 100644
--- a/packages/solidjs/README.md
+++ b/packages/solidjs/README.md
@@ -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(
+ () => (
+
+
+ ...
+
+ ),
+ document.getElementById('root'),
+);
+```
diff --git a/packages/solidjs/package.json b/packages/solidjs/package.json
index f3b91d7b706a..7b4317c79d60 100644
--- a/packages/solidjs/package.json
+++ b/packages/solidjs/package.json
@@ -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": {
diff --git a/packages/solidjs/src/debug-build.ts b/packages/solidjs/src/debug-build.ts
new file mode 100644
index 000000000000..60aa50940582
--- /dev/null
+++ b/packages/solidjs/src/debug-build.ts
@@ -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__;
diff --git a/packages/solidjs/src/index.ts b/packages/solidjs/src/index.ts
index 8e25b84c4a0c..77f17110f5e1 100644
--- a/packages/solidjs/src/index.ts
+++ b/packages/solidjs/src/index.ts
@@ -1,3 +1,5 @@
export * from '@sentry/browser';
export { init } from './sdk';
+
+export * from './solidrouter';
diff --git a/packages/solidjs/src/solidrouter.ts b/packages/solidjs/src/solidrouter.ts
new file mode 100644
index 000000000000..d38b21313570
--- /dev/null
+++ b/packages/solidjs/src/solidrouter.ts
@@ -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 {
+ location: Location;
+ data?: T;
+ children?: JSX.Element;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type RouteDefinition = {
+ path?: S;
+ children?: RouteDefinition | RouteDefinition[];
+ component?: Component>;
+};
+
+interface RouterProps {
+ base?: string;
+ root?: Component;
+ 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();
+
+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): Component {
+ 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[0] & SolidRouterOptions,
+): Integration {
+ const integration = browserTracingIntegration({
+ ...options,
+ instrumentNavigation: false,
+ });
+
+ const { instrumentNavigation = true, useBeforeLeave, useLocation } = options;
+
+ return {
+ ...integration,
+ setup() {
+ _useBeforeLeave = useBeforeLeave;
+ _useLocation = useLocation;
+ },
+ 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): Component {
+ 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;
+}
diff --git a/yarn.lock b/yarn.lock
index e0e07a2aefae..e054994642fc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8452,6 +8452,7 @@
"@types/unist" "*"
"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@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==
@@ -26109,6 +26110,7 @@ react-is@^18.0.0:
"@remix-run/router" "1.0.2"
"react-router-6@npm:react-router@6.3.0", react-router@6.3.0:
+ 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==
@@ -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==
@@ -27934,13 +27936,13 @@ socks@^2.6.2:
ip "^2.0.0"
smart-buffer "^4.2.0"
-solid-js@1.8.11:
- 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:
@@ -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==
@@ -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==