Skip to content

Commit a6e9651

Browse files
chargomelforst
andauthored
feat(react-router): Add sentryHandleRequest (#15787)
Co-authored-by: Luca Forstner <[email protected]>
1 parent 438e8be commit a6e9651

File tree

7 files changed

+77
-10
lines changed

7 files changed

+77
-10
lines changed

dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.server.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { AppLoadContext, EntryContext } from 'react-router';
99
import { ServerRouter } from 'react-router';
1010
const ABORT_DELAY = 5_000;
1111

12-
export default function handleRequest(
12+
function handleRequest(
1313
request: Request,
1414
responseStatusCode: number,
1515
responseHeaders: Headers,
@@ -60,6 +60,8 @@ export default function handleRequest(
6060
});
6161
}
6262

63+
export default Sentry.sentryHandleRequest(handleRequest);
64+
6365
import { type HandleErrorFunction } from 'react-router';
6466

6567
export const handleError: HandleErrorFunction = (error, { request }) => {

dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json

+9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
"@react-router/node": "^7.1.5",
1111
"@react-router/serve": "^7.1.5",
1212
"@sentry/react-router": "latest || *",
13+
"@sentry-internal/feedback": "latest || *",
14+
"@sentry-internal/replay-canvas": "latest || *",
15+
"@sentry-internal/browser-utils": "latest || *",
16+
"@sentry/browser": "latest || *",
17+
"@sentry/core": "latest || *",
18+
"@sentry/node": "latest || *",
19+
"@sentry/opentelemetry": "latest || *",
20+
"@sentry/react": "latest || *",
21+
"@sentry-internal/replay": "latest || *",
1322
"isbot": "^5.1.17"
1423
},
1524
"devDependencies": {

dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { APP_NAME } from '../constants';
55
test.describe('servery - performance', () => {
66
test('should send server transaction on pageload', async ({ page }) => {
77
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
8-
// todo: should be GET /performance
9-
return transactionEvent.transaction === 'GET *';
8+
return transactionEvent.transaction === 'GET /performance';
109
});
1110

1211
await page.goto(`/performance`);
@@ -30,8 +29,7 @@ test.describe('servery - performance', () => {
3029
spans: expect.any(Array),
3130
start_timestamp: expect.any(Number),
3231
timestamp: expect.any(Number),
33-
// todo: should be GET /performance
34-
transaction: 'GET *',
32+
transaction: 'GET /performance',
3533
type: 'transaction',
3634
transaction_info: { source: 'route' },
3735
platform: 'node',
@@ -58,8 +56,7 @@ test.describe('servery - performance', () => {
5856

5957
test('should send server transaction on parameterized route', async ({ page }) => {
6058
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
61-
// todo: should be GET /performance/with/:param
62-
return transactionEvent.transaction === 'GET *';
59+
return transactionEvent.transaction === 'GET /performance/with/:param';
6360
});
6461

6562
await page.goto(`/performance/with/some-param`);
@@ -83,8 +80,7 @@ test.describe('servery - performance', () => {
8380
spans: expect.any(Array),
8481
start_timestamp: expect.any(Number),
8582
timestamp: expect.any(Number),
86-
// todo: should be GET /performance/with/:param
87-
transaction: 'GET *',
83+
transaction: 'GET /performance/with/:param',
8884
type: 'transaction',
8985
transaction_info: { source: 'route' },
9086
platform: 'node',

packages/react-router/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
"@sentry/core": "9.10.1",
4040
"@sentry/node": "9.10.1",
4141
"@sentry/vite-plugin": "^3.2.4",
42+
"@opentelemetry/semantic-conventions": "^1.30.0",
43+
"@opentelemetry/core": "^1.30.1",
44+
"@opentelemetry/api": "^1.9.0",
4245
"glob": "11.0.1"
4346
},
4447
"devDependencies": {
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from '@sentry/node';
22

33
export { init } from './sdk';
4+
export { sentryHandleRequest } from './sentryHandleRequest';

packages/react-router/src/server/sdk.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { applySdkMetadata, setTag } from '@sentry/core';
1+
import { applySdkMetadata, logger, setTag } from '@sentry/core';
22
import type { NodeClient, NodeOptions } from '@sentry/node';
33
import { init as initNodeSdk } from '@sentry/node';
4+
import { DEBUG_BUILD } from '../common/debug-build';
45

56
/**
67
* Initializes the server side of the React Router SDK
@@ -10,11 +11,14 @@ export function init(options: NodeOptions): NodeClient | undefined {
1011
...options,
1112
};
1213

14+
DEBUG_BUILD && logger.log('Initializing SDK...');
15+
1316
applySdkMetadata(opts, 'react-router', ['react-router', 'node']);
1417

1518
const client = initNodeSdk(opts);
1619

1720
setTag('runtime', 'node');
1821

22+
DEBUG_BUILD && logger.log('SDK successfully initialized');
1923
return client;
2024
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { context } from '@opentelemetry/api';
2+
import { RPCType, getRPCMetadata } from '@opentelemetry/core';
3+
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
4+
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, getRootSpan } from '@sentry/core';
5+
import type { AppLoadContext, EntryContext } from 'react-router';
6+
7+
type OriginalHandleRequest = (
8+
request: Request,
9+
responseStatusCode: number,
10+
responseHeaders: Headers,
11+
routerContext: EntryContext,
12+
loadContext: AppLoadContext,
13+
) => Promise<unknown>;
14+
15+
/**
16+
* Wraps the original handleRequest function to add Sentry instrumentation.
17+
*
18+
* @param originalHandle - The original handleRequest function to wrap
19+
* @returns A wrapped version of the handle request function with Sentry instrumentation
20+
*/
21+
export function sentryHandleRequest(originalHandle: OriginalHandleRequest): OriginalHandleRequest {
22+
return async function sentryInstrumentedHandleRequest(
23+
request: Request,
24+
responseStatusCode: number,
25+
responseHeaders: Headers,
26+
routerContext: EntryContext,
27+
loadContext: AppLoadContext,
28+
) {
29+
const parameterizedPath =
30+
routerContext?.staticHandlerContext?.matches?.[routerContext.staticHandlerContext.matches.length - 1]?.route.path;
31+
if (parameterizedPath) {
32+
const activeSpan = getActiveSpan();
33+
if (activeSpan) {
34+
const rootSpan = getRootSpan(activeSpan);
35+
const routeName = `/${parameterizedPath}`;
36+
37+
// The express instrumentation writes on the rpcMetadata and that ends up stomping on the `http.route` attribute.
38+
const rpcMetadata = getRPCMetadata(context.active());
39+
if (rpcMetadata?.type === RPCType.HTTP) {
40+
rpcMetadata.route = routeName;
41+
}
42+
43+
// The span exporter picks up the `http.route` (ATTR_HTTP_ROUTE) attribute to set the transaction name
44+
rootSpan.setAttributes({
45+
[ATTR_HTTP_ROUTE]: routeName,
46+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
47+
});
48+
}
49+
}
50+
return originalHandle(request, responseStatusCode, responseHeaders, routerContext, loadContext);
51+
};
52+
}

0 commit comments

Comments
 (0)