Skip to content

Commit 61f5fd5

Browse files
authored
feat(react-router): Add basic package (#15289)
1 parent 36878d1 commit 61f5fd5

24 files changed

+528
-0
lines changed

Diff for: dev-packages/e2e-tests/verdaccio-config/config.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ packages:
122122
unpublish: $all
123123
# proxy: npmjs # Don't proxy for E2E tests!
124124

125+
'@sentry/react-router':
126+
access: $all
127+
publish: $all
128+
unpublish: $all
129+
# proxy: npmjs # Don't proxy for E2E tests!
130+
125131
'@sentry/remix':
126132
access: $all
127133
publish: $all

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"packages/opentelemetry",
7474
"packages/profiling-node",
7575
"packages/react",
76+
"packages/react-router",
7677
"packages/remix",
7778
"packages/replay-internal",
7879
"packages/replay-canvas",

Diff for: packages/react-router/.eslintrc.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = {
2+
env: {
3+
browser: true,
4+
node: true,
5+
},
6+
overrides: [
7+
{
8+
files: ['vite.config.ts'],
9+
parserOptions: {
10+
project: ['tsconfig.test.json'],
11+
},
12+
},
13+
],
14+
extends: ['../../.eslintrc.js'],
15+
};

Diff for: packages/react-router/LICENSE

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Functional Software, Inc. dba Sentry
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8+
persons to whom the Software is furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11+
Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Diff for: packages/react-router/README.md

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<p align="center">
2+
<a href="https://sentry.io/?utm_source=github&utm_medium=logo" target="_blank">
3+
<img src="https://sentry-brand.storage.googleapis.com/sentry-wordmark-dark-280x84.png" alt="Sentry" width="280" height="84">
4+
</a>
5+
</p>
6+
7+
# Official Sentry SDK for React Router (Framework) (EXPERIMENTAL)
8+
9+
[![npm version](https://img.shields.io/npm/v/@sentry/react-router.svg)](https://www.npmjs.com/package/@sentry/react-router)
10+
[![npm dm](https://img.shields.io/npm/dm/@sentry/react-router.svg)](https://www.npmjs.com/package/@sentry/react-router)
11+
[![npm dt](https://img.shields.io/npm/dt/@sentry/react-router.svg)](https://www.npmjs.com/package/@sentry/react-router)
12+
13+
> [!WARNING]
14+
> This SDK is considered ⚠️ **experimental and in an alpha state**. It may experience breaking changes. Please reach out
15+
> on [GitHub](https://github.com/getsentry/sentry-javascript/issues/) if you have any feedback or concerns. This
16+
> SDK is for [React Router (framework)](https://reactrouter.com/start/framework/installation). If you're using [React Router (library)](https://reactrouter.com/start/library/installation) see our
17+
> [React SDK here](https://docs.sentry.io/platforms/javascript/guides/react/features/react-router/v7/).
18+
19+
## Links
20+
21+
- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/react-router/)
22+
23+
## General
24+
25+
This package is a wrapper around `@sentry/node` for the server and `@sentry/browser` for the client side.
26+
27+
## Manual Setup
28+
29+
### Expose Hooks
30+
31+
React Router exposes two hooks in your `app` folder (`entry.client.tsx` and `entry.server.tsx`).
32+
If you do not see these two files, expose them with the following command:
33+
34+
```bash
35+
npx react-router reveal
36+
```
37+
38+
### Client-Side Setup
39+
40+
Initialize the SDK in your `entry.client.tsx` file:
41+
42+
```tsx
43+
import * as Sentry from '@sentry/react-router';
44+
import { startTransition, StrictMode } from 'react';
45+
import { hydrateRoot } from 'react-dom/client';
46+
import { HydratedRouter } from 'react-router/dom';
47+
48+
Sentry.init({
49+
dsn: '___PUBLIC_DSN___',
50+
integrations: [Sentry.browserTracingIntegration()],
51+
52+
tracesSampleRate: 1.0, // Capture 100% of the transactions
53+
54+
// Set `tracePropagationTargets` to declare which URL(s) should have trace propagation enabled
55+
tracePropagationTargets: [/^\//, /^https:\/\/yourserver\.io\/api/],
56+
});
57+
58+
startTransition(() => {
59+
hydrateRoot(
60+
document,
61+
<StrictMode>
62+
<HydratedRouter />
63+
</StrictMode>,
64+
);
65+
});
66+
```
67+
68+
Now, update your `app/root.tsx` file to report any unhandled errors from your global error boundary:
69+
70+
```tsx
71+
import * as Sentry from '@sentry/react-router';
72+
73+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
74+
let message = 'Oops!';
75+
let details = 'An unexpected error occurred.';
76+
let stack: string | undefined;
77+
78+
if (isRouteErrorResponse(error)) {
79+
message = error.status === 404 ? '404' : 'Error';
80+
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
81+
} else if (error && error instanceof Error) {
82+
// you only want to capture non 404-errors that reach the boundary
83+
Sentry.captureException(error);
84+
if (import.meta.env.DEV) {
85+
details = error.message;
86+
stack = error.stack;
87+
}
88+
}
89+
90+
return (
91+
<main>
92+
<h1>{message}</h1>
93+
<p>{details}</p>
94+
{stack && (
95+
<pre>
96+
<code>{stack}</code>
97+
</pre>
98+
)}
99+
</main>
100+
);
101+
}
102+
// ...
103+
```
104+
105+
### Server-Side Setup
106+
107+
Create an `instrument.server.mjs` file in the root of your app:
108+
109+
```js
110+
import * as Sentry from '@sentry/node';
111+
112+
Sentry.init({
113+
dsn: '___PUBLIC_DSN___',
114+
tracesSampleRate: 1.0, // Capture 100% of the transactions
115+
});
116+
```
117+
118+
In your `entry.server.tsx` file, export the `handleError` function:
119+
120+
```tsx
121+
import * as Sentry from '@sentry/node';
122+
import { type HandleErrorFunction } from 'react-router';
123+
124+
export const handleError: HandleErrorFunction = (error, { request }) => {
125+
// React Router may abort some interrupted requests, report those
126+
if (!request.signal.aborted) {
127+
Sentry.captureException(error);
128+
129+
// make sure to still log the error so you can see it
130+
console.error(error);
131+
}
132+
};
133+
// ... rest of your server entry
134+
```
135+
136+
### Update Scripts
137+
138+
Since React Router is running in ESM mode, you need to use the `--import` command line options to load our server-side instrumentation module before the application starts.
139+
Update the `start` and `dev` script to include the instrumentation file:
140+
141+
```json
142+
"scripts": {
143+
"dev": "NODE_OPTIONS='--import ./instrument.server.mjs' react-router dev",
144+
"start": "NODE_OPTIONS='--import ./instrument.server.mjs' react-router-serve ./build/server/index.js",
145+
}
146+
```

Diff for: packages/react-router/package.json

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"name": "@sentry/react-router",
3+
"version": "9.0.0-alpha.2",
4+
"description": "Official Sentry SDK for React Router (Framework)",
5+
"repository": "git://github.com/getsentry/sentry-javascript.git",
6+
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react-router",
7+
"author": "Sentry",
8+
"license": "MIT",
9+
"engines": {
10+
"node": ">=20"
11+
},
12+
"files": [
13+
"/build"
14+
],
15+
"main": "build/cjs/index.server.js",
16+
"module": "build/esm/index.server.js",
17+
"browser": "build/esm/index.client.js",
18+
"types": "build/types/index.types.d.ts",
19+
"exports": {
20+
"./package.json": "./package.json",
21+
".": {
22+
"types": "./build/types/index.types.d.ts",
23+
"browser": {
24+
"import": "./build/esm/index.client.js",
25+
"require": "./build/cjs/index.client.js"
26+
},
27+
"node": {
28+
"import": "./build/esm/index.server.js",
29+
"require": "./build/cjs/index.server.js"
30+
}
31+
}
32+
},
33+
"publishConfig": {
34+
"access": "public"
35+
},
36+
"dependencies": {
37+
"@sentry/core": "9.0.0-alpha.2",
38+
"@sentry/browser": "9.0.0-alpha.2",
39+
"@sentry/node": "9.0.0-alpha.2"
40+
},
41+
"devDependencies": {
42+
"@react-router/node": "^7.1.5",
43+
"react-router": "^7.1.5"
44+
},
45+
"peerDependencies": {
46+
"@react-router/node": "7.x",
47+
"react-router": "7.x",
48+
"react": ">=18"
49+
},
50+
"scripts": {
51+
"build": "run-p build:transpile build:types",
52+
"build:dev": "yarn build",
53+
"build:transpile": "rollup -c rollup.npm.config.mjs",
54+
"build:types": "run-s build:types:core",
55+
"build:types:core": "tsc -p tsconfig.types.json",
56+
"build:watch": "run-p build:transpile:watch build:types:watch",
57+
"build:dev:watch": "yarn build:watch",
58+
"build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
59+
"build:types:watch": "tsc -p tsconfig.types.json --watch",
60+
"build:tarball": "npm pack",
61+
"circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts",
62+
"clean": "rimraf build coverage sentry-react-router-*.tgz",
63+
"fix": "eslint . --format stylish --fix",
64+
"lint": "eslint . --format stylish",
65+
"test": "yarn test:unit",
66+
"test:unit": "vitest run",
67+
"test:watch": "vitest --watch",
68+
"yalc:publish": "yalc publish --push --sig"
69+
},
70+
"volta": {
71+
"extends": "../../package.json"
72+
}
73+
}

Diff for: packages/react-router/rollup.npm.config.mjs

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
2+
3+
export default [
4+
...makeNPMConfigVariants(
5+
makeBaseNPMConfig({
6+
entrypoints: ['src/index.server.ts', 'src/index.client.ts'],
7+
packageSpecificConfig: {
8+
external: ['react-router', 'react-router-dom', 'react', 'react/jsx-runtime'],
9+
output: {
10+
// make it so Rollup calms down about the fact that we're combining default and named exports
11+
exports: 'named',
12+
},
13+
},
14+
sucrase: {
15+
// React 19 emits a warning if we don't use the newer jsx transform: https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html
16+
// but this breaks react 17, so we keep it at `classic` for now
17+
jsxRuntime: 'classic',
18+
production: true, // This is needed so that sucrase uses the production jsx runtime (ie `import { jsx } from 'react/jsx-runtime'` instead of `import { jsxDEV as _jsxDEV } from 'react/jsx-dev-runtime'`)
19+
},
20+
}),
21+
),
22+
];

Diff for: packages/react-router/src/client/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from '@sentry/browser';
2+
3+
export { init } from './sdk';

Diff for: packages/react-router/src/client/sdk.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { BrowserOptions } from '@sentry/browser';
2+
import { init as browserInit } from '@sentry/browser';
3+
import type { Client } from '@sentry/core';
4+
import { applySdkMetadata, setTag } from '@sentry/core';
5+
6+
/**
7+
* Initializes the client side of the React Router SDK.
8+
*/
9+
export function init(options: BrowserOptions): Client | undefined {
10+
const opts = {
11+
...options,
12+
};
13+
14+
applySdkMetadata(opts, 'react-router', ['react-router', 'browser']);
15+
16+
const client = browserInit(opts);
17+
18+
setTag('runtime', 'browser');
19+
20+
return client;
21+
}

Diff for: packages/react-router/src/common/debug-build.ts

+8
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__;

Diff for: packages/react-router/src/index.client.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './client';

Diff for: packages/react-router/src/index.server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './server';

Diff for: packages/react-router/src/index.types.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// re-define colliding type exports below
2+
3+
export * from './client';
4+
export * from './server';
5+
6+
import type { Integration, Options, StackParser } from '@sentry/core';
7+
import type * as clientSdk from './client';
8+
import type * as serverSdk from './server';
9+
10+
/** Initializes Sentry React Router SDK */
11+
export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): void;
12+
13+
export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration;
14+
export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration;
15+
export declare const defaultStackParser: StackParser;
16+
export declare const getDefaultIntegrations: (options: Options) => Integration[];

Diff for: packages/react-router/src/server/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from '@sentry/node';
2+
3+
export { init } from './sdk';

Diff for: packages/react-router/src/server/sdk.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { applySdkMetadata, setTag } from '@sentry/core';
2+
import type { NodeClient, NodeOptions } from '@sentry/node';
3+
import { init as initNodeSdk } from '@sentry/node';
4+
5+
/**
6+
* Initializes the server side of the React Router SDK
7+
*/
8+
export function init(options: NodeOptions): NodeClient | undefined {
9+
const opts = {
10+
...options,
11+
};
12+
13+
applySdkMetadata(opts, 'react-router', ['react-router', 'node']);
14+
15+
const client = initNodeSdk(opts);
16+
17+
setTag('runtime', 'node');
18+
19+
return client;
20+
}

0 commit comments

Comments
 (0)