UNPKG

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