---
title: Instrumentation
unstable: true
---
# Instrumentation
[MODES: framework, data]
Instrumentation allows you to add logging, error reporting, and performance tracing to your React Router application without modifying your actual route handlers. This enables comprehensive observability solutions for production applications on both the server and client.
## Overview
With the React Router Instrumentation APIs, you provide "wrapper" functions that execute around your request handlers, router operations, route middlewares, and/or route handlers. This allows you to:
- Monitor application performance
- Add logging
- Integrate with observability platforms (Sentry, DataDog, New Relic, etc.)
- Implement OpenTelemetry tracing
- Track user behavior and navigation patterns
A key design principle is that instrumentation is **read-only** - you can observe what's happening but cannot modify runtime application behavior by modifying the arguments passed to, or data returned from your route handlers.
As with any instrumentation approach, adding additional code execution at runtime may alter the performance characteristics compared to an uninstrumented application. Keep this in mind and perform appropriate testing and/or leverage conditional instrumentation to avoid a negative UX impact in production.
## Quick Start (Framework Mode)
[modes: framework]
### 1. Server-side Instrumentation
Add instrumentations to your `entry.server.tsx`:
```tsx filename=app/entry.server.tsx
export const unstable_instrumentations = [
{
// Instrument the server handler
handler(handler) {
handler.instrument({
async request(handleRequest, { request }) {
let url = `${request.method} ${request.url}`;
console.log(`Request start: ${url}`);
await handleRequest();
console.log(`Request end: ${url}`);
},
});
},
// Instrument individual routes
route(route) {
// Skip instrumentation for specific routes if needed
if (route.id === "root") return;
route.instrument({
async loader(callLoader, { request }) {
let url = `${request.method} ${request.url}`;
console.log(`Loader start: ${url} - ${route.id}`);
await callLoader();
console.log(`Loader end: ${url} - ${route.id}`);
},
// Other available instrumentations:
// async action() { /* ... */ },
// async middleware() { /* ... */ },
// async lazy() { /* ... */ },
});
},
},
];
export default function handleRequest(/* ... */) {
// Your existing handleRequest implementation
}
```
### 2. Client-side Instrumentation
Add instrumentations to your `entry.client.tsx`:
```tsx filename=app/entry.client.tsx
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
const unstable_instrumentations = [
{
// Instrument router operations
router(router) {
router.instrument({
// Instrument navigations
async navigate(callNavigate, { currentUrl, to }) {
let nav = `${currentUrl} → ${to}`;
console.log(`Navigation start: ${nav}`);
await callNavigate();
console.log(`Navigation end: ${nav}`);
},
// Instrument fetcher calls
async fetch(
callFetch,
{ href, currentUrl, fetcherKey },
) {
let fetch = `${fetcherKey} → ${href}`;
console.log(`Fetcher start: ${fetch}`);
await callFetch();
console.log(`Fetcher end: ${fetch}`);
},
});
},
// Instrument individual routes (same as server-side)
route(route) {
// Skip instrumentation for specific routes if needed
if (route.id === "root") return;
route.instrument({
async loader(callLoader, { request }) {
let url = `${request.method} ${request.url}`;
console.log(`Loader start: ${url} - ${route.id}`);
await callLoader();
console.log(`Loader end: ${url} - ${route.id}`);
},
// Other available instrumentations:
// async action() { /* ... */ },
// async middleware() { /* ... */ },
// async lazy() { /* ... */ },
});
},
},
];
startTransition(() => {
hydrateRoot(
document,
,
);
});
```
## Quick Start (Data Mode)
[modes: data]
In Data Mode, you add instrumentations when creating your router:
```tsx
import {
createBrowserRouter,
RouterProvider,
} from "react-router";
const unstable_instrumentations = [
{
// Instrument router operations
router(router) {
router.instrument({
// Instrument navigations
async navigate(callNavigate, { currentUrl, to }) {
let nav = `${currentUrl} → ${to}`;
console.log(`Navigation start: ${nav}`);
await callNavigate();
console.log(`Navigation end: ${nav}`);
},
// Instrument fetcher calls
async fetch(
callFetch,
{ href, currentUrl, fetcherKey },
) {
let fetch = `${fetcherKey} → ${href}`;
console.log(`Fetcher start: ${fetch}`);
await callFetch();
console.log(`Fetcher end: ${fetch}`);
},
});
},
// Instrument individual routes (same as server-side)
route(route) {
// Skip instrumentation for specific routes if needed
if (route.id === "root") return;
route.instrument({
async loader(callLoader, { request }) {
let url = `${request.method} ${request.url}`;
console.log(`Loader start: ${url} - ${route.id}`);
await callLoader();
console.log(`Loader end: ${url} - ${route.id}`);
},
// Other available instrumentations:
// async action() { /* ... */ },
// async middleware() { /* ... */ },
// async lazy() { /* ... */ },
});
},
},
];
const router = createBrowserRouter(routes, {
unstable_instrumentations,
});
function App() {
return ;
}
```
## Core Concepts
### Instrumentation Levels
There are different levels at which you can instrument your application. Each instrumentation function receives a second "info" parameter containing relevant contextual information for the specific aspect being instrumented.
#### 1. Handler Level (Server)
[modes: framework]
Instruments the top-level request handler that processes all requests to your server:
```tsx filename=entry.server.tsx
export const unstable_instrumentations = [
{
handler(handler) {
handler.instrument({
async request(handleRequest, { request, context }) {
// Runs around ALL requests to your app
await handleRequest();
},
});
},
},
];
```
#### 2. Router Level (Client)
[modes: framework,data]
Instruments client-side router operations like navigations and fetcher calls:
```tsx
export const unstable_instrumentations = [
{
router(router) {
router.instrument({
async navigate(callNavigate, { to, currentUrl }) {
// Runs around navigation operations
await callNavigate();
},
async fetch(
callFetch,
{ href, currentUrl, fetcherKey },
) {
// Runs around fetcher operations
await callFetch();
},
});
},
},
];
// Framework Mode (entry.client.tsx)
;
// Data Mode
const router = createBrowserRouter(routes, {
unstable_instrumentations,
});
```
#### 3. Route Level (Server + Client)
[modes: framework,data]
Instruments individual route handlers:
```tsx
const unstable_instrumentations = [
{
route(route) {
route.instrument({
async loader(
callLoader,
{ params, request, context, unstable_pattern },
) {
// Runs around loader execution
await callLoader();
},
async action(
callAction,
{ params, request, context, unstable_pattern },
) {
// Runs around action execution
await callAction();
},
async middleware(
callMiddleware,
{ params, request, context, unstable_pattern },
) {
// Runs around middleware execution
await callMiddleware();
},
async lazy(callLazy) {
// Runs around lazy route loading
await callLazy();
},
});
},
},
];
```
### Read-only Design
Instrumentations are designed to be **observational only**. You cannot:
- Modify arguments passed to handlers
- Change return values from handlers
- Alter application behavior
This ensures that instrumentation is safe to add to production applications and cannot introduce bugs in your route logic.
### Error Handling
To ensure that instrumentation code doesn't impact the runtime application, errors are caught internally and prevented from propagating outward. This design choice shows up in 2 aspects.
First, if a "handler" function (loader, action, request handler, navigation, etc.) throws an error, that error will not bubble out of the `callHandler` function invoked from your instrumentation. Instead, the `callHandler` function returns a discriminated union result of type `{ type: "success", error: undefined } | { type: "error", error: unknown }`. This ensures your entire instrumentation function runs without needing any try/catch/finally logic to handle application errors.
```tsx
export const unstable_instrumentations = [
{
route(route) {
route.instrument({
async loader(callLoader) {
let { status, error } = await callLoader();
if (status === "error") {
// error case - `error` is defined
} else {
// success case - `error` is undefined
}
},
});
},
},
];
```
Second, if your instrumentation function throws an error, React Router will gracefully swallow that so that it does not bubble outward and impact other instrumentations or application behavior. In both of these examples, the handlers and all other instrumentation functions will still run:
```tsx
export const unstable_instrumentations = [
{
route(route) {
route.instrument({
// Throwing before calling the handler - RR will
// catch the error and still call the loader
async loader(callLoader) {
somethingThatThrows();
await callLoader();
},
// Throwing after calling the handler - RR will
// catch the error internally
async action(callAction) {
await callAction();
somethingThatThrows();
},
});
},
},
];
```
### Composition
You can compose multiple instrumentations by providing an array:
```tsx
export const unstable_instrumentations = [
loggingInstrumentation,
performanceInstrumentation,
errorReportingInstrumentation,
];
```
Each instrumentation wraps the previous one, creating a nested execution chain.
### Conditional Instrumentation
You can enable instrumentation conditionally based on environment or other factors:
```tsx
export const unstable_instrumentations =
process.env.NODE_ENV === "production"
? [productionInstrumentation]
: [developmentInstrumentation];
```
```tsx
// Or conditionally within an instrumentation
export const unstable_instrumentations = [
{
route(route) {
// Only instrument specific routes
if (!route.id?.startsWith("routes/admin")) return;
// Or, only instrument if a query parameter is present
let sp = new URL(request.url).searchParams;
if (!sp.has("DEBUG")) return;
route.instrument({
async loader() {
/* ... */
},
});
},
},
];
```
## Common Patterns
### Request logging (server)
```tsx
const logging: unstable_ServerInstrumentation = {
handler({ instrument }) {
instrument({
request: (fn, { request }) =>
log(`request ${request.url}`, fn),
});
},
route({ instrument, id }) {
instrument({
middleware: (fn) => log(` middleware (${id})`, fn),
loader: (fn) => log(` loader (${id})`, fn),
action: (fn) => log(` action (${id})`, fn),
});
},
};
async function log(
label: string,
cb: () => Promise,
) {
let start = Date.now();
console.log(`➡️ ${label}`);
await cb();
console.log(`⬅️ ${label} (${Date.now() - start}ms)`);
}
export const unstable_instrumentations = [logging];
```
### OpenTelemetry Integration
```tsx
import { trace, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("my-app");
const otel: unstable_ServerInstrumentation = {
handler({ instrument }) {
instrument({
request: (fn, { request }) =>
otelSpan(`request`, { url: request.url }, fn),
});
},
route({ instrument, id }) {
instrument({
middleware: (fn, { unstable_pattern }) =>
otelSpan(
"middleware",
{ routeId: id, pattern: unstable_pattern },
fn,
),
loader: (fn, { unstable_pattern }) =>
otelSpan(
"loader",
{ routeId: id, pattern: unstable_pattern },
fn,
),
action: (fn, { unstable_pattern }) =>
otelSpan(
"action",
{ routeId: id, pattern: unstable_pattern },
fn,
),
});
},
};
async function otelSpan(
label: string,
attributes: Record,
cb: () => Promise,
) {
return tracer.startActiveSpan(
label,
{ attributes },
async (span) => {
let { error } = await cb();
if (error) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
});
}
span.end();
},
);
}
export const unstable_instrumentations = [otel];
```
### Client-side Performance Tracking
```tsx
const windowPerf: unstable_ClientInstrumentation = {
router({ instrument }) {
instrument({
navigate: (fn, { to, currentUrl }) =>
measure(`navigation:${currentUrl}->${to}`, fn),
fetch: (fn, { href }) =>
measure(`fetcher:${href}`, fn),
});
},
route({ instrument, id }) {
instrument({
middleware: (fn) => measure(`middleware:${id}`, fn),
loader: (fn) => measure(`loader:${id}`, fn),
action: (fn) => measure(`action:${id}`, fn),
});
},
};
async function measure(
label: string,
cb: () => Promise,
) {
performance.mark(`start:${label}`);
await cb();
performance.mark(`end:${label}`);
performance.measure(
label,
`start:${label}`,
`end:${label}`,
);
}
;
```