Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt
Use this file to discover all available pages before exploring further.
Instrumentation API
The instrumentation API allows you to wrap route handlers and operations for logging, performance tracing, and error reporting.
Overview
Instrumentation provides:
- Request-level tracing: Measure entire request duration
- Route-level instrumentation: Track individual loaders/actions
- Non-invasive observation: Cannot modify runtime behavior
- Flexible composition: Combine multiple instrumentations
Basic Server Instrumentation
// entry.server.tsx
export const instrumentations = [
{
// Instrument the request handler
handler({ instrument }) {
instrument({
async request(handleRequest, { request }) {
const start = Date.now();
console.log(`Request: ${request.method} ${request.url}`);
await handleRequest();
const duration = Date.now() - start;
console.log(`Completed in ${duration}ms`);
},
});
},
},
];
Route Instrumentation
Instrument individual route handlers:
export const instrumentations = [
{
route({ instrument, id }) {
// Skip instrumentation for specific routes
if (id === "routes/_index") return;
instrument({
async loader(callLoader, { request }) {
const start = performance.now();
await callLoader();
const duration = performance.now() - start;
console.log(`Loader ${id}: ${duration}ms`);
},
async action(callAction, { request }) {
const start = performance.now();
await callAction();
const duration = performance.now() - start;
console.log(`Action ${id}: ${duration}ms`);
},
});
},
},
];
Client Instrumentation
// entry.client.tsx
export const instrumentations = [
{
// Instrument router operations
router({ instrument }) {
instrument({
async initialize(callInitialize) {
const start = performance.now();
await callInitialize();
const duration = performance.now() - start;
console.log(`Router initialized in ${duration}ms`);
},
async navigate(callNavigate, { location }) {
const start = performance.now();
await callNavigate();
const duration = performance.now() - start;
console.log(`Navigation to ${location}: ${duration}ms`);
},
});
},
// Instrument routes
route({ instrument, id }) {
instrument({
async loader(callLoader) {
const start = performance.now();
await callLoader();
const duration = performance.now() - start;
console.log(`Client loader ${id}: ${duration}ms`);
},
});
},
},
];
Error Handling
Instrumentation handlers never throw - they return error status:
export const instrumentations = [
{
route({ instrument }) {
instrument({
async loader(callLoader) {
const { status, error } = await callLoader();
if (status === "error") {
console.error("Loader failed:", error);
// Log to error tracking service
Sentry.captureException(error);
}
},
});
},
},
];
OpenTelemetry Integration
Integrate with OpenTelemetry for distributed tracing:
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("react-router");
export const instrumentations = [
{
handler({ instrument }) {
instrument({
async request(handleRequest, { request }) {
const span = tracer.startSpan("http.request", {
attributes: {
"http.method": request.method,
"http.url": request.url,
},
});
try {
await handleRequest();
span.setStatus({ code: 0 }); // OK
} catch (error) {
span.setStatus({ code: 2 }); // ERROR
span.recordException(error);
} finally {
span.end();
}
},
});
},
route({ instrument, id }) {
instrument({
async loader(callLoader) {
return tracer.startActiveSpan(`loader.${id}`, async (span) => {
try {
return await callLoader();
} finally {
span.end();
}
});
},
});
},
},
];
Track performance metrics:
export const instrumentations = [
{
route({ instrument, id }) {
const metrics = {
loaderCalls: 0,
loaderDuration: 0,
actionCalls: 0,
actionDuration: 0,
};
instrument({
async loader(callLoader) {
const start = performance.now();
metrics.loaderCalls++;
await callLoader();
metrics.loaderDuration += performance.now() - start;
// Send to analytics
analytics.track("loader", {
route: id,
duration: performance.now() - start,
avgDuration: metrics.loaderDuration / metrics.loaderCalls,
});
},
});
},
},
];
Custom Middleware
Implement middleware-like patterns:
export const instrumentations = [
{
route({ instrument, id }) {
instrument({
async loader(callLoader, { request }) {
// Before loader
const auth = await checkAuth(request);
if (!auth) {
return { status: "error", error: new Error("Unauthorized") };
}
// Call loader
const result = await callLoader();
// After loader
await logAccess(id, auth.user);
return result;
},
});
},
},
];
Composition
Combine multiple instrumentations:
const loggingInstrumentation = {
route({ instrument }) {
instrument({
async loader(callLoader, { request }) {
console.log("Loading...");
return await callLoader();
},
});
},
};
const tracingInstrumentation = {
route({ instrument }) {
instrument({
async loader(callLoader) {
const span = startSpan();
const result = await callLoader();
span.end();
return result;
},
});
},
};
export const instrumentations = [
loggingInstrumentation,
tracingInstrumentation,
];
Conditional Instrumentation
Enable instrumentation conditionally:
// Client-side
const enableInstrumentation =
new URLSearchParams(window.location.search).has("debug");
export const instrumentations = enableInstrumentation
? [debugInstrumentation]
: [];
// Server-side (requires custom server)
import { createRequestHandler } from "@react-router/express";
app.all("*", (req, res, next) => {
const shouldInstrument = req.query.debug === "true";
const build = await import("./build/server");
const handler = createRequestHandler({
build: shouldInstrument ? build : {
...build,
entry: {
...build.entry,
module: {
...build.entry.module,
instrumentations: undefined,
},
},
},
});
handler(req, res, next);
});
Data Mode
Instrument data mode applications:
import { createBrowserRouter } from "react-router";
const instrumentations = [{
route({ instrument, id }) {
instrument({
async loader(callLoader) {
const start = performance.now();
const result = await callLoader();
console.log(`${id}: ${performance.now() - start}ms`);
return result;
},
});
},
}];
const router = createBrowserRouter(routes, {
instrumentations,
});
Available Handlers
Server
handler.request: Entire request lifecycle
route.loader: Route loader execution
route.action: Route action execution
route.middleware: Middleware execution
route.lazy: Lazy route loading
Client
router.initialize: Router initialization
router.navigate: Navigation operations
router.fetch: Fetcher operations
route.loader: Route loader execution
route.action: Route action execution
route.middleware: Client middleware execution
route.lazy: Lazy route loading
Best Practices
- Don’t modify behavior: Instrumentation is read-only
- Handle errors gracefully: Instrumentation errors are swallowed
- Keep it lightweight: Avoid heavy computation
- Use appropriate scope: Route vs request level
- Compose instrumentations: Separate concerns
Limitations
- Cannot modify request/response
- Cannot access handler arguments
- Cannot change data returned
- Errors are caught and logged
- Handler still runs if instrumentation throws