UNPKG

14.8 kB Markdown View 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 await handleRequest();
48 console.log(`Request end: ${url}`);
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 await callNavigate();
98 console.log(`Navigation end: ${nav}`);
99 },
100 // Instrument fetcher calls
101 async fetch(
102 callFetch,
103 { href, currentUrl, fetcherKey },
104 ) {
105 let fetch = `${fetcherKey} → ${href}`;
106 console.log(`Fetcher start: ${fetch}`);
107 await callFetch();
108 console.log(`Fetcher end: ${fetch}`);
109 },
110 });
111 },
112
113 // Instrument individual routes (same as server-side)
114 route(route) {
115 // Skip instrumentation for specific routes if needed
116 if (route.id === "root") return;
117
118 route.instrument({
119 async loader(callLoader, { request }) {
120 let url = `${request.method} ${request.url}`;
121 console.log(`Loader start: ${url} - ${route.id}`);
122 await callLoader();
123 console.log(`Loader end: ${url} - ${route.id}`);
124 },
125 // Other available instrumentations:
126 // async action() { /* ... */ },
127 // async middleware() { /* ... */ },
128 // async lazy() { /* ... */ },
129 });
130 },
131 },
132];
133
134startTransition(() => {
135 hydrateRoot(
136 document,
137 <StrictMode>
138 <HydratedRouter instrumentations={instrumentations} />
139 </StrictMode>,
140 );
141});
142```
143
144## Quick Start (Data Mode)
145
146[modes: data]
147
148In Data Mode, you add instrumentations when creating your router:
149
150```tsx
151import {
152 createBrowserRouter,
153 RouterProvider,
154} from "react-router";
155
156const instrumentations = [
157 {
158 // Instrument router operations
159 router(router) {
160 router.instrument({
161 // Instrument navigations
162 async navigate(callNavigate, { currentUrl, to }) {
163 let nav = `${currentUrl} → ${to}`;
164 console.log(`Navigation start: ${nav}`);
165 await callNavigate();
166 console.log(`Navigation end: ${nav}`);
167 },
168 // Instrument fetcher calls
169 async fetch(
170 callFetch,
171 { href, currentUrl, fetcherKey },
172 ) {
173 let fetch = `${fetcherKey} → ${href}`;
174 console.log(`Fetcher start: ${fetch}`);
175 await callFetch();
176 console.log(`Fetcher end: ${fetch}`);
177 },
178 });
179 },
180
181 // Instrument individual routes (same as server-side)
182 route(route) {
183 // Skip instrumentation for specific routes if needed
184 if (route.id === "root") return;
185
186 route.instrument({
187 async loader(callLoader, { request }) {
188 let url = `${request.method} ${request.url}`;
189 console.log(`Loader start: ${url} - ${route.id}`);
190 await callLoader();
191 console.log(`Loader end: ${url} - ${route.id}`);
192 },
193 // Other available instrumentations:
194 // async action() { /* ... */ },
195 // async middleware() { /* ... */ },
196 // async lazy() { /* ... */ },
197 });
198 },
199 },
200];
201
202const router = createBrowserRouter(routes, {
203 instrumentations,
204});
205
206function App() {
207 return <RouterProvider router={router} />;
208}
209```
210
211## Core Concepts
212
213### Instrumentation Levels
214
215There 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.
216
217#### 1. Handler Level (Server)
218
219[modes: framework]
220
221Instruments the top-level request handler that processes all requests to your server:
222
223```tsx filename=entry.server.tsx
224export const instrumentations = [
225 {
226 handler(handler) {
227 handler.instrument({
228 async request(handleRequest, { request, context }) {
229 // Runs around ALL requests to your app
230 await handleRequest();
231 },
232 });
233 },
234 },
235];
236```
237
238#### 2. Router Level (Client)
239
240[modes: framework,data]
241
242Instruments client-side router operations like navigations and fetcher calls:
243
244```tsx
245export const instrumentations = [
246 {
247 router(router) {
248 router.instrument({
249 async navigate(callNavigate, { to, currentUrl }) {
250 // Runs around navigation operations
251 await callNavigate();
252 },
253 async fetch(
254 callFetch,
255 { href, currentUrl, fetcherKey },
256 ) {
257 // Runs around fetcher operations
258 await callFetch();
259 },
260 });
261 },
262 },
263];
264
265// Framework Mode (entry.client.tsx)
266<HydratedRouter instrumentations={instrumentations} />;
267
268// Data Mode
269const router = createBrowserRouter(routes, {
270 instrumentations,
271});
272```
273
274#### 3. Route Level (Server + Client)
275
276[modes: framework,data]
277
278Instruments individual route handlers:
279
280```tsx
281const instrumentations = [
282 {
283 route(route) {
284 route.instrument({
285 async loader(
286 callLoader,
287 { params, request, context, pattern },
288 ) {
289 // Runs around loader execution
290 await callLoader();
291 },
292 async action(
293 callAction,
294 { params, request, context, pattern },
295 ) {
296 // Runs around action execution
297 await callAction();
298 },
299 async middleware(
300 callMiddleware,
301 { params, request, context, pattern },
302 ) {
303 // Runs around middleware execution
304 await callMiddleware();
305 },
306 async lazy(callLazy) {
307 // Runs around lazy route loading
308 await callLazy();
309 },
310 });
311 },
312 },
313];
314```
315
316### Read-only Design
317
318Instrumentations are designed to be **observational only**. You cannot:
319
320- Modify arguments passed to handlers
321- Change return values from handlers
322- Alter application behavior
323
324This ensures that instrumentation is safe to add to production applications and cannot introduce bugs in your route logic.
325
326### Error Handling
327
328To 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.
329
330First, 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.
331
332```tsx
333export const instrumentations = [
334 {
335 route(route) {
336 route.instrument({
337 async loader(callLoader) {
338 let { status, error } = await callLoader();
339
340 if (status === "error") {
341 // error case - `error` is defined
342 } else {
343 // success case - `error` is undefined
344 }
345 },
346 });
347 },
348 },
349];
350```
351
352Second, 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:
353
354```tsx
355export const instrumentations = [
356 {
357 route(route) {
358 route.instrument({
359 // Throwing before calling the handler - RR will
360 // catch the error and still call the loader
361 async loader(callLoader) {
362 somethingThatThrows();
363 await callLoader();
364 },
365 // Throwing after calling the handler - RR will
366 // catch the error internally
367 async action(callAction) {
368 await callAction();
369 somethingThatThrows();
370 },
371 });
372 },
373 },
374];
375```
376
377### Composition
378
379You can compose multiple instrumentations by providing an array:
380
381```tsx
382export const instrumentations = [
383 loggingInstrumentation,
384 performanceInstrumentation,
385 errorReportingInstrumentation,
386];
387```
388
389Each instrumentation wraps the previous one, creating a nested execution chain.
390
391### Conditional Instrumentation
392
393You can enable instrumentation conditionally based on environment or other factors:
394
395```tsx
396export const instrumentations =
397 process.env.NODE_ENV === "production"
398 ? [productionInstrumentation]
399 : [developmentInstrumentation];
400```
401
402```tsx
403// Or conditionally within an instrumentation
404export const instrumentations = [
405 {
406 route(route) {
407 // Only instrument specific routes
408 if (!route.id?.startsWith("routes/admin")) return;
409
410 // Or, only instrument if a query parameter is present
411 let sp = new URL(request.url).searchParams;
412 if (!sp.has("DEBUG")) return;
413
414 route.instrument({
415 async loader() {
416 /* ... */
417 },
418 });
419 },
420 },
421];
422```
423
424## Common Patterns
425
426### Request logging (server)
427
428```tsx
429const logging: ServerInstrumentation = {
430 handler({ instrument }) {
431 instrument({
432 request: (fn, { request }) =>
433 log(`request ${request.url}`, fn),
434 });
435 },
436 route({ instrument, id }) {
437 instrument({
438 middleware: (fn) => log(` middleware (${id})`, fn),
439 loader: (fn) => log(` loader (${id})`, fn),
440 action: (fn) => log(` action (${id})`, fn),
441 });
442 },
443};
444
445async function log(
446 label: string,
447 cb: () => Promise<InstrumentationHandlerResult>,
448) {
449 let start = Date.now();
450 console.log(`➡️ ${label}`);
451 await cb();
452 console.log(`⬅️ ${label} (${Date.now() - start}ms)`);
453}
454
455export const instrumentations = [logging];
456```
457
458### OpenTelemetry Integration
459
460```tsx
461import { trace, SpanStatusCode } from "@opentelemetry/api";
462
463const tracer = trace.getTracer("my-app");
464
465const otel: ServerInstrumentation = {
466 handler({ instrument }) {
467 instrument({
468 request: (fn, { request }) =>
469 otelSpan(`request`, { url: request.url }, fn),
470 });
471 },
472 route({ instrument, id }) {
473 instrument({
474 middleware: (fn, { pattern }) =>
475 otelSpan(
476 "middleware",
477 { routeId: id, pattern: pattern },
478 fn,
479 ),
480 loader: (fn, { pattern }) =>
481 otelSpan(
482 "loader",
483 { routeId: id, pattern: pattern },
484 fn,
485 ),
486 action: (fn, { pattern }) =>
487 otelSpan(
488 "action",
489 { routeId: id, pattern: pattern },
490 fn,
491 ),
492 });
493 },
494};
495
496async function otelSpan(
497 label: string,
498 attributes: Record<string, string>,
499 cb: () => Promise<InstrumentationHandlerResult>,
500) {
501 return tracer.startActiveSpan(
502 label,
503 { attributes },
504 async (span) => {
505 let { error } = await cb();
506 if (error) {
507 span.recordException(error);
508 span.setStatus({
509 code: SpanStatusCode.ERROR,
510 });
511 }
512 span.end();
513 },
514 );
515}
516
517export const instrumentations = [otel];
518```
519
520### Client-side Performance Tracking
521
522```tsx
523const windowPerf: ClientInstrumentation = {
524 router({ instrument }) {
525 instrument({
526 navigate: (fn, { to, currentUrl }) =>
527 measure(`navigation:${currentUrl}->${to}`, fn),
528 fetch: (fn, { href }) =>
529 measure(`fetcher:${href}`, fn),
530 });
531 },
532 route({ instrument, id }) {
533 instrument({
534 middleware: (fn) => measure(`middleware:${id}`, fn),
535 loader: (fn) => measure(`loader:${id}`, fn),
536 action: (fn) => measure(`action:${id}`, fn),
537 });
538 },
539};
540
541async function measure(
542 label: string,
543 cb: () => Promise<InstrumentationHandlerResult>,
544) {
545 performance.mark(`start:${label}`);
546 await cb();
547 performance.mark(`end:${label}`);
548 performance.measure(
549 label,
550 `start:${label}`,
551 `end:${label}`,
552 );
553}
554
555<HydratedRouter instrumentations={[windowPerf]} />;
556```