UNPKG

18.4 kBMarkdownView Raw
1---
2title: Instrumentation
3---
4
5# Instrumentation
6
7[MODES: framework, data]
8
9<br/>
10<br/>
11
12Instrumentation 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.
13
14## Overview
15
16With 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:
17
18- Monitor application performance
19- Add logging
20- Integrate with observability platforms (Sentry, DataDog, New Relic, etc.)
21- Implement OpenTelemetry tracing
22- Track user behavior and navigation patterns
23
24A 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.
25
26<docs-info>
27As 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.
28</docs-info>
29
30## Quick Start (Framework Mode)
31
32[modes: framework]
33
34### 1. Server-side Instrumentation
35
36Add instrumentations to your `entry.server.tsx`:
37
38```tsx filename=app/entry.server.tsx
39export const instrumentations = [
40 {
41 // Instrument the server handler
42 handler(handler) {
43 handler.instrument({
44 async request(handleRequest, { request }) {
45 let url = `${request.method} ${request.url}`;
46 console.log(`Request start: ${url}`);
47 let result = await handleRequest();
48 let pattern = result.meta?.pattern ?? "unknown";
49 console.log(
50 `Request end: ${url} (${result.statusCode} ${pattern})`,
51 );
52 },
53 });
54 },
55
56 // Instrument individual routes
57 route(route) {
58 // Skip instrumentation for specific routes if needed
59 if (route.id === "root") return;
60
61 route.instrument({
62 async loader(callLoader, { request }) {
63 let url = `${request.method} ${request.url}`;
64 console.log(`Loader start: ${url} - ${route.id}`);
65 await callLoader();
66 console.log(`Loader end: ${url} - ${route.id}`);
67 },
68 // Other available instrumentations:
69 // async action() { /* ... */ },
70 // async middleware() { /* ... */ },
71 // async lazy() { /* ... */ },
72 });
73 },
74 },
75];
76
77export default function handleRequest(/* ... */) {
78 // Your existing handleRequest implementation
79}
80```
81
82### 2. Client-side Instrumentation
83
84Add instrumentations to your `entry.client.tsx`:
85
86```tsx filename=app/entry.client.tsx
87import { startTransition, StrictMode } from "react";
88import { hydrateRoot } from "react-dom/client";
89import { HydratedRouter } from "react-router/dom";
90
91const instrumentations = [
92 {
93 // Instrument router operations
94 router(router) {
95 router.instrument({
96 // Instrument navigations
97 async navigate(callNavigate, { currentUrl, to }) {
98 let nav = `${currentUrl} -> ${to}`;
99 console.log(`Navigation start: ${nav}`);
100 let result = await callNavigate();
101 console.log(
102 `Navigation end: ${nav} (${result.meta?.pattern})`,
103 );
104 },
105 // Instrument fetcher calls
106 async fetch(
107 callFetch,
108 { href, currentUrl, fetcherKey },
109 ) {
110 let fetch = `${fetcherKey} -> ${href}`;
111 console.log(`Fetcher start: ${fetch}`);
112 let result = await callFetch();
113 console.log(
114 `Fetcher end: ${fetch} (${result.meta?.pattern})`,
115 );
116 },
117 });
118 },
119
120 // Instrument individual routes (same as server-side)
121 route(route) {
122 // Skip instrumentation for specific routes if needed
123 if (route.id === "root") return;
124
125 route.instrument({
126 async loader(callLoader, { request }) {
127 let url = `${request.method} ${request.url}`;
128 console.log(`Loader start: ${url} - ${route.id}`);
129 await callLoader();
130 console.log(`Loader end: ${url} - ${route.id}`);
131 },
132 // Other available instrumentations:
133 // async action() { /* ... */ },
134 // async middleware() { /* ... */ },
135 // async lazy() { /* ... */ },
136 });
137 },
138 },
139];
140
141startTransition(() => {
142 hydrateRoot(
143 document,
144 <StrictMode>
145 <HydratedRouter instrumentations={instrumentations} />
146 </StrictMode>,
147 );
148});
149```
150
151## Quick Start (Data Mode)
152
153[modes: data]
154
155In Data Mode, you add instrumentations when creating your router:
156
157```tsx
158import {
159 createBrowserRouter,
160 RouterProvider,
161} from "react-router";
162
163const instrumentations = [
164 {
165 // Instrument router operations
166 router(router) {
167 router.instrument({
168 // Instrument navigations
169 async navigate(callNavigate, { currentUrl, to }) {
170 let nav = `${currentUrl} -> ${to}`;
171 console.log(`Navigation start: ${nav}`);
172 let result = await callNavigate();
173 console.log(
174 `Navigation end: ${nav} (${result.meta?.pattern})`,
175 );
176 },
177 // Instrument fetcher calls
178 async fetch(
179 callFetch,
180 { href, currentUrl, fetcherKey },
181 ) {
182 let fetch = `${fetcherKey} -> ${href}`;
183 console.log(`Fetcher start: ${fetch}`);
184 let result = await callFetch();
185 console.log(
186 `Fetcher end: ${fetch} (${result.meta?.pattern})`,
187 );
188 },
189 });
190 },
191
192 // Instrument individual routes (same as server-side)
193 route(route) {
194 // Skip instrumentation for specific routes if needed
195 if (route.id === "root") return;
196
197 route.instrument({
198 async loader(callLoader, { request }) {
199 let url = `${request.method} ${request.url}`;
200 console.log(`Loader start: ${url} - ${route.id}`);
201 await callLoader();
202 console.log(`Loader end: ${url} - ${route.id}`);
203 },
204 // Other available instrumentations:
205 // async action() { /* ... */ },
206 // async middleware() { /* ... */ },
207 // async lazy() { /* ... */ },
208 });
209 },
210 },
211];
212
213const router = createBrowserRouter(routes, {
214 instrumentations,
215});
216
217function App() {
218 return <RouterProvider router={router} />;
219}
220```
221
222## Core Concepts
223
224### Instrumentation Levels
225
226There 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.
227
228#### 1. Handler Level (Server)
229
230[modes: framework]
231
232Instruments the top-level request handler that processes all requests to your server:
233
234```tsx filename=entry.server.tsx
235export const instrumentations = [
236 {
237 handler(handler) {
238 handler.instrument({
239 async request(handleRequest, { request, context }) {
240 // Runs around ALL requests to your app
241 let result = await handleRequest();
242 let statusCode = result.statusCode;
243 let routePattern = result.meta?.pattern;
244 },
245 });
246 },
247 },
248];
249```
250
251#### 2. Router Level (Client)
252
253[modes: framework,data]
254
255Instruments client-side router operations like navigations and fetcher calls:
256
257```tsx
258export const instrumentations = [
259 {
260 router(router) {
261 router.instrument({
262 async navigate(callNavigate, { to, currentUrl }) {
263 // Runs around navigation operations
264 let result = await callNavigate();
265 let routePattern = result.meta?.pattern;
266 },
267 async fetch(
268 callFetch,
269 { href, currentUrl, fetcherKey },
270 ) {
271 // Runs around fetcher operations
272 let result = await callFetch();
273 let routePattern = result.meta?.pattern;
274 },
275 });
276 },
277 },
278];
279
280// Framework Mode (entry.client.tsx)
281<HydratedRouter instrumentations={instrumentations} />;
282
283// Data Mode
284const router = createBrowserRouter(routes, {
285 instrumentations,
286});
287```
288
289#### 3. Route Level (Server + Client)
290
291[modes: framework,data]
292
293Instruments individual route handlers:
294
295```tsx
296const instrumentations = [
297 {
298 route(route) {
299 route.instrument({
300 async loader(
301 callLoader,
302 { params, request, context, pattern },
303 ) {
304 // Runs around loader execution
305 await callLoader();
306 },
307 async action(
308 callAction,
309 { params, request, context, pattern },
310 ) {
311 // Runs around action execution
312 await callAction();
313 },
314 async middleware(
315 callMiddleware,
316 { params, request, context, pattern },
317 ) {
318 // Runs around middleware execution
319 await callMiddleware();
320 },
321 async lazy(callLazy) {
322 // Runs around lazy route loading
323 await callLazy();
324 },
325 });
326 },
327 },
328];
329```
330
331### Read-only Design
332
333Instrumentations are designed to be **observational only**. You cannot:
334
335- Modify arguments passed to handlers
336- Change return values from handlers
337- Alter application behavior
338
339This ensures that instrumentation is safe to add to production applications and cannot introduce bugs in your route logic.
340
341### Error Handling
342
343To 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.
344
345First, 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 `{ status: "success", error: undefined } | { status: "error", error: Error }`. This ensures your entire instrumentation function runs without needing any try/catch/finally logic to handle application errors.
346
347```tsx
348export const instrumentations = [
349 {
350 route(route) {
351 route.instrument({
352 async loader(callLoader) {
353 let { status, error } = await callLoader();
354
355 if (status === "error") {
356 // error case - `error` is defined
357 } else {
358 // success case - `error` is undefined
359 }
360 },
361 });
362 },
363 },
364];
365```
366
367Second, 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:
368
369```tsx
370export const instrumentations = [
371 {
372 route(route) {
373 route.instrument({
374 // Throwing before calling the handler - RR will
375 // catch the error and still call the loader
376 async loader(callLoader) {
377 somethingThatThrows();
378 await callLoader();
379 },
380 // Throwing after calling the handler - RR will
381 // catch the error internally
382 async action(callAction) {
383 await callAction();
384 somethingThatThrows();
385 },
386 });
387 },
388 },
389];
390```
391
392### Result Metadata
393
394Some instrumented calls return additional information that is only available after React Router starts processing the request, navigation, or fetcher call.
395
396- Route-level instrumentations (`loader`/`action`/`middleware`) don't include `meta` because metadata is available on the `info` parameter
397- Client navigation/fetcher and Server request handler instrumentations return a meta field
398 - `meta` contains the same values passed to loaders and actions
399 - `url`: The normalized `URL` for the matched route request
400 - `pattern`: The matched route pattern, such as `/projects/:id`
401 - `params`: The matched route params
402 - `meta` may be `undefined` when React Router does not have route metadata for the instrumented call, such as server manifest requests or numeric POP navigations like `navigate(-1)`
403 - For client navigations that redirect, `meta` describes the original navigation target instead of the final redirected location.
404- Server request handler instrumentations also return the `statusCode` of the response
405
406```tsx
407// entry.server.tsx
408export const instrumentations = [
409 {
410 handler(handler) {
411 handler.instrument({
412 async request(handleRequest) {
413 let result = await handleRequest();
414
415 let statusCode = result.statusCode;
416 let routeUrl = result.meta?.url;
417 let routePattern = result.meta?.pattern;
418 let routeParams = result.meta?.params;
419 },
420 });
421 },
422 },
423];
424
425// entry.client.tsx
426const instrumentations = [
427 {
428 router(router) {
429 router.instrument({
430 async navigate(callNavigate) {
431 let result = await callNavigate();
432
433 let routeUrl = result.meta?.url;
434 let routePattern = result.meta?.pattern;
435 let routeParams = result.meta?.params;
436 },
437 async fetch(callFetch) {
438 let result = await callFetch();
439
440 let routeUrl = result.meta?.url;
441 let routePattern = result.meta?.pattern;
442 let routeParams = result.meta?.params;
443 },
444 });
445 },
446 },
447];
448```
449
450### Composition
451
452You can compose multiple instrumentations by providing an array:
453
454```tsx
455export const instrumentations = [
456 loggingInstrumentation,
457 performanceInstrumentation,
458 errorReportingInstrumentation,
459];
460```
461
462Each instrumentation wraps the previous one, creating a nested execution chain.
463
464### Conditional Instrumentation
465
466You can enable instrumentation conditionally based on environment or other factors:
467
468```tsx
469export const instrumentations =
470 process.env.NODE_ENV === "production"
471 ? [productionInstrumentation]
472 : [developmentInstrumentation];
473```
474
475```tsx
476// Or conditionally within an instrumentation
477export const instrumentations = [
478 {
479 route(route) {
480 // Only instrument specific routes
481 if (!route.id?.startsWith("routes/admin")) return;
482
483 // Or, only instrument if a query parameter is present
484 let sp = new URL(request.url).searchParams;
485 if (!sp.has("DEBUG")) return;
486
487 route.instrument({
488 async loader() {
489 /* ... */
490 },
491 });
492 },
493 },
494];
495```
496
497## Common Patterns
498
499### Request logging (server)
500
501```tsx
502const logging: ServerInstrumentation = {
503 handler({ instrument }) {
504 instrument({
505 async request(fn, { request }) {
506 let label = `request ${request.url}`;
507 let start = Date.now();
508 console.log(`-> ${label}`);
509 let result = await fn();
510 let pattern = result.meta?.pattern ?? "";
511 console.log(
512 `<- ${label} (${Date.now() - start}ms ${result.statusCode} ${pattern})`,
513 );
514 },
515 });
516 },
517 route({ instrument, id }) {
518 instrument({
519 middleware: (fn) => log(` middleware (${id})`, fn),
520 loader: (fn) => log(` loader (${id})`, fn),
521 action: (fn) => log(` action (${id})`, fn),
522 });
523 },
524};
525
526async function log(
527 label: string,
528 cb: () => Promise<InstrumentationHandlerResult>,
529) {
530 let start = Date.now();
531 console.log(`-> ${label}`);
532 await cb();
533 console.log(`<- ${label} (${Date.now() - start}ms)`);
534}
535
536export const instrumentations = [logging];
537```
538
539### OpenTelemetry Integration
540
541```tsx
542import { trace, SpanStatusCode } from "@opentelemetry/api";
543
544const tracer = trace.getTracer("my-app");
545
546const otel: ServerInstrumentation = {
547 handler({ instrument }) {
548 instrument({
549 request: (fn, { request }) =>
550 otelSpan(`request`, { url: request.url }, fn),
551 });
552 },
553 route({ instrument, id }) {
554 instrument({
555 middleware: (fn, { pattern }) =>
556 otelSpan(
557 "middleware",
558 { routeId: id, pattern: pattern },
559 fn,
560 ),
561 loader: (fn, { pattern }) =>
562 otelSpan(
563 "loader",
564 { routeId: id, pattern: pattern },
565 fn,
566 ),
567 action: (fn, { pattern }) =>
568 otelSpan(
569 "action",
570 { routeId: id, pattern: pattern },
571 fn,
572 ),
573 });
574 },
575};
576
577async function otelSpan(
578 label: string,
579 attributes: Record<string, string>,
580 cb: () => Promise<InstrumentationHandlerResult>,
581) {
582 return tracer.startActiveSpan(
583 label,
584 { attributes },
585 async (span) => {
586 let { error } = await cb();
587 if (error) {
588 span.recordException(error);
589 span.setStatus({
590 code: SpanStatusCode.ERROR,
591 });
592 }
593 span.end();
594 },
595 );
596}
597
598export const instrumentations = [otel];
599```
600
601### Client-side Performance Tracking
602
603```tsx
604const windowPerf: ClientInstrumentation = {
605 router({ instrument }) {
606 instrument({
607 async navigate(fn, { to, currentUrl }) {
608 let label = `navigation:${currentUrl}->${to}`;
609 performance.mark(`start:${label}`);
610 let result = await fn();
611 performance.mark(`end:${label}`);
612 performance.measure(
613 label,
614 `start:${label}`,
615 `end:${label}`,
616 );
617 console.log(
618 `navigation pattern: ${result.meta?.pattern}`,
619 );
620 },
621 async fetch(fn, { href }) {
622 let label = `fetcher:${href}`;
623 performance.mark(`start:${label}`);
624 let result = await fn();
625 performance.mark(`end:${label}`);
626 performance.measure(
627 label,
628 `start:${label}`,
629 `end:${label}`,
630 );
631 console.log(
632 `fetcher pattern: ${result.meta?.pattern}`,
633 );
634 },
635 });
636 },
637 route({ instrument, id }) {
638 instrument({
639 middleware: (fn) => measure(`middleware:${id}`, fn),
640 loader: (fn) => measure(`loader:${id}`, fn),
641 action: (fn) => measure(`action:${id}`, fn),
642 });
643 },
644};
645
646async function measure(
647 label: string,
648 cb: () => Promise<InstrumentationHandlerResult>,
649) {
650 performance.mark(`start:${label}`);
651 await cb();
652 performance.mark(`end:${label}`);
653 performance.measure(
654 label,
655 `start:${label}`,
656 `end:${label}`,
657 );
658}
659
660<HydratedRouter instrumentations={[windowPerf]} />;
661```