Skip to content

feat(react-router): Add sentryHandleRequest #15787

New issue

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

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

Already on GitHub? # to your account

Merged
merged 13 commits into from
Apr 3, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { AppLoadContext, EntryContext } from 'react-router';
import { ServerRouter } from 'react-router';
const ABORT_DELAY = 5_000;

export default function handleRequest(
function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
Expand Down Expand Up @@ -60,6 +60,8 @@ export default function handleRequest(
});
}

export default Sentry.sentryHandleRequest(handleRequest);

import { type HandleErrorFunction } from 'react-router';

export const handleError: HandleErrorFunction = (error, { request }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
"@react-router/node": "^7.1.5",
"@react-router/serve": "^7.1.5",
"@sentry/react-router": "latest || *",
"@sentry-internal/feedback": "latest || *",
"@sentry-internal/replay-canvas": "latest || *",
"@sentry-internal/browser-utils": "latest || *",
"@sentry/browser": "latest || *",
"@sentry/core": "latest || *",
"@sentry/node": "latest || *",
"@sentry/opentelemetry": "latest || *",
"@sentry/react": "latest || *",
"@sentry-internal/replay": "latest || *",
"isbot": "^5.1.17"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { APP_NAME } from '../constants';
test.describe('servery - performance', () => {
test('should send server transaction on pageload', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
// todo: should be GET /performance
return transactionEvent.transaction === 'GET *';
return transactionEvent.transaction === 'GET /performance';
});

await page.goto(`/performance`);
Expand All @@ -30,8 +29,7 @@ test.describe('servery - performance', () => {
spans: expect.any(Array),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
// todo: should be GET /performance
transaction: 'GET *',
transaction: 'GET /performance',
type: 'transaction',
transaction_info: { source: 'route' },
platform: 'node',
Expand All @@ -58,8 +56,7 @@ test.describe('servery - performance', () => {

test('should send server transaction on parameterized route', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
// todo: should be GET /performance/with/:param
return transactionEvent.transaction === 'GET *';
return transactionEvent.transaction === 'GET /performance/with/:param';
});

await page.goto(`/performance/with/some-param`);
Expand All @@ -83,8 +80,7 @@ test.describe('servery - performance', () => {
spans: expect.any(Array),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
// todo: should be GET /performance/with/:param
transaction: 'GET *',
transaction: 'GET /performance/with/:param',
type: 'transaction',
transaction_info: { source: 'route' },
platform: 'node',
Expand Down
3 changes: 3 additions & 0 deletions packages/react-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"@sentry/core": "9.10.1",
"@sentry/node": "9.10.1",
"@sentry/vite-plugin": "^3.2.4",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@opentelemetry/core": "^1.30.1",
"@opentelemetry/api": "^1.9.0",
"glob": "11.0.1"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/react-router/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from '@sentry/node';

export { init } from './sdk';
export { sentryHandleRequest } from './sentryHandleRequest';
6 changes: 5 additions & 1 deletion packages/react-router/src/server/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { applySdkMetadata, setTag } from '@sentry/core';
import { applySdkMetadata, logger, setTag } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { init as initNodeSdk } from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';

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

DEBUG_BUILD && logger.log('Initializing SDK...');

applySdkMetadata(opts, 'react-router', ['react-router', 'node']);

const client = initNodeSdk(opts);

setTag('runtime', 'node');

DEBUG_BUILD && logger.log('SDK successfully initialized');
return client;
}
52 changes: 52 additions & 0 deletions packages/react-router/src/server/sentryHandleRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { context } from '@opentelemetry/api';
import { RPCType, getRPCMetadata } from '@opentelemetry/core';
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, getRootSpan } from '@sentry/core';
import type { AppLoadContext, EntryContext } from 'react-router';

type OriginalHandleRequest = (
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
loadContext: AppLoadContext,
) => Promise<unknown>;

/**
* Wraps the original handleRequest function to add Sentry instrumentation.
*
* @param originalHandle - The original handleRequest function to wrap
* @returns A wrapped version of the handle request function with Sentry instrumentation
*/
export function sentryHandleRequest(originalHandle: OriginalHandleRequest): OriginalHandleRequest {
return async function sentryInstrumentedHandleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
loadContext: AppLoadContext,
) {
const parameterizedPath =
routerContext?.staticHandlerContext?.matches?.[routerContext.staticHandlerContext.matches.length - 1]?.route.path;
if (parameterizedPath) {
const activeSpan = getActiveSpan();
if (activeSpan) {
const rootSpan = getRootSpan(activeSpan);
const routeName = `/${parameterizedPath}`;

// The express instrumentation writes on the rpcMetadata and that ends up stomping on the `http.route` attribute.
const rpcMetadata = getRPCMetadata(context.active());
if (rpcMetadata?.type === RPCType.HTTP) {
rpcMetadata.route = routeName;
}

// The span exporter picks up the `http.route` (ATTR_HTTP_ROUTE) attribute to set the transaction name
rootSpan.setAttributes({
[ATTR_HTTP_ROUTE]: routeName,
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
});
}
}
return originalHandle(request, responseStatusCode, responseHeaders, routerContext, loadContext);
};
}
Loading