diff --git a/docs/how-to/instrumentation.md b/docs/how-to/instrumentation.md index a57a47c8a4..3aeb3ca071 100644 --- a/docs/how-to/instrumentation.md +++ b/docs/how-to/instrumentation.md @@ -395,120 +395,6 @@ export const unstable_instrumentations = [ Each instrumentation wraps the previous one, creating a nested execution chain. -## Common Patterns - -### Performance Monitoring - -```tsx -export const unstable_instrumentations = [ - { - handler(handler) { - handler.instrument({ - async request(handleRequest, info) { - const start = Date.now(); - await handleRequest(); - const duration = Date.now() - start; - reportPerf(info.request, duration); - }, - }); - }, - - route(route) { - route.instrument({ - async loader(callLoader, info) { - const start = Date.now(); - let { error } = await callLoader(); - const duration = Date.now() - start; - reportPerf(info.request, { - routePattern: info.unstable_pattern, - routeId: route.id, - duration, - error, - }); - }, - }); - }, - }, -]; -``` - -### OpenTelemetry Integration - -```tsx -import { trace, SpanStatusCode } from "@opentelemetry/api"; - -const tracer = trace.getTracer("my-app"); - -export const unstable_instrumentations = [ - { - handler(handler) { - handler.instrument({ - async request(handleRequest, { request }) { - return tracer.startActiveSpan( - "request handler", - async (span) => { - let { error } = await handleRequest(); - if (error) { - span.recordException(error); - span.setStatus({ - code: SpanStatusCode.ERROR, - }); - } - span.end(); - }, - ); - }, - }); - }, - - route(route) { - route.instrument({ - async loader(callLoader, { routeId }) { - return tracer.startActiveSpan( - "route loader", - { attributes: { routeId: route.id } }, - async (span) => { - let { error } = await callLoader(); - if (error) { - span.recordException(error); - span.setStatus({ - code: SpanStatusCode.ERROR, - }); - } - span.end(); - }, - ); - }, - }); - }, - }, -]; -``` - -### Client-side Performance Tracking - -```tsx -const unstable_instrumentations = [ - { - router(router) { - router.instrument({ - async navigate(callNavigate, { to, currentUrl }) { - let label = `${currentUrl}->${to}`; - performance.mark(`start:${label}`); - await callNavigate(); - performance.mark(`end:${label}`); - performance.measure( - `navigation:${label}`, - `start:${label}`, - `end:${label}`, - ); - }, - }); - }, - }, -]; -``` - ### Conditional Instrumentation You can enable instrumentation conditionally based on environment or other factors: @@ -541,3 +427,137 @@ export const unstable_instrumentations = [ }, ]; ``` + +## 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}`, + ); +} + +; +``` diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 8483a1cb1b..dc5c81704a 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -67,6 +67,7 @@ export type { unstable_InstrumentRequestHandlerFunction, unstable_InstrumentRouterFunction, unstable_InstrumentRouteFunction, + unstable_InstrumentationHandlerResult, } from "./lib/router/instrumentation"; export { IDLE_NAVIGATION, diff --git a/packages/react-router/lib/router/instrumentation.ts b/packages/react-router/lib/router/instrumentation.ts index 76a780324b..630f485b83 100644 --- a/packages/react-router/lib/router/instrumentation.ts +++ b/packages/react-router/lib/router/instrumentation.ts @@ -39,13 +39,13 @@ export type unstable_InstrumentRouteFunction = ( route: InstrumentableRoute, ) => void; -// Shared -type InstrumentResult = +export type unstable_InstrumentationHandlerResult = | { status: "success"; error: undefined } - | { status: "error"; error: unknown }; + | { status: "error"; error: Error }; +// Shared type InstrumentFunction = ( - handler: () => Promise, + handler: () => Promise, info: T, ) => Promise; @@ -402,19 +402,20 @@ async function recurseRight( // If they forget to call the handler, or if they throw before calling the // handler, we need to ensure the handlers still gets called let handlerPromise: ReturnType | undefined = undefined; - let callHandler = async (): Promise => { - if (handlerPromise) { - console.error("You cannot call instrumented handlers more than once"); - } else { - handlerPromise = recurseRight(impls, info, handler, index - 1); - } - result = await handlerPromise; - invariant(result, "Expected a result"); - if (result.type === "error" && result.value instanceof Error) { - return { status: "error", error: result.value }; - } - return { status: "success", error: undefined }; - }; + let callHandler = + async (): Promise => { + if (handlerPromise) { + console.error("You cannot call instrumented handlers more than once"); + } else { + handlerPromise = recurseRight(impls, info, handler, index - 1); + } + result = await handlerPromise; + invariant(result, "Expected a result"); + if (result.type === "error" && result.value instanceof Error) { + return { status: "error", error: result.value }; + } + return { status: "success", error: undefined }; + }; try { await impl(callHandler, info); diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 9d3fbcd864..4c816a548f 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -2023,5 +2023,5 @@ export function isRouteErrorResponse(error: any): error is ErrorResponse { } export function getRoutePattern(paths: (string | undefined)[]) { - return paths.filter(Boolean).join("/").replace(/\/\/*/g, "/"); + return paths.filter(Boolean).join("/").replace(/\/\/*/g, "/") || "/"; }