You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: decisions/0015-observability.md
+74-12Lines changed: 74 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,7 +8,7 @@ Status: proposed
8
8
9
9
We want it to be easy to add observability to production React Router applications. This involves the ability to add logging, error reporting, and performance tracing to your application on both the server and the client.
10
10
11
-
We always had a good story for user-facing error handling via `ErrorBoundary`, but until recently we only had a server-side error-reporting solution via the `entry.server``handleError` export. In `7.8.2`, we shipped an `onError` client-side equivalent so it should now be possible to report on errors on the server and client pretty easily.
11
+
We always had a good story for user-facing error _display_ via `ErrorBoundary`, but until recently we only had a server-side error_reporting_solution via the `entry.server``handleError` export. In `7.8.2`, we shipped an `unstable_onError` client-side equivalent so it should now be possible to report on errors on the server and client pretty easily.
12
12
13
13
We have not historically had great recommendations for the other 2 facets of observability - logging and performance tracing. Middleware, shipped in `7.3.0` and stabilized in `7.9.0` gave us a way to "wrap" request handlers at any level of the tree, which provides a good solution for logging and _some_ high-level performance tracing. But it's too coarse-grained and does not allow folks to drill down into their applications.
14
14
@@ -24,31 +24,54 @@ Adopt instrumentation as a first class API and the recommended way to implement
24
24
25
25
There are 2 levels in which we want to instrument:
26
26
27
-
- "router" level - ability to track the start and end of a router operation
28
-
- requests on the server handler
29
-
- navigations and fetchers on the client router
27
+
- handler (server) and router (client) level
28
+
- instrument the request handler on the server
29
+
- instrument navigations and fetcher calls on the client
30
+
- singular instrumentation per operation
30
31
- route level
31
-
- loaders, actions, middlewares, lazy
32
+
- instrument loaders, actions, middlewares, lazy
33
+
- multiple instrumentations per operation - multiple routes, multiple middlewares etc.
32
34
33
-
On the server, if you are using a custom server, this is already possible by wrapping the react router handler and walking the `build.routes` tree and wrapping the route handlers.
35
+
On the server, if you are using a custom server, this is already possible by wrapping the react router request handler and walking the `build.routes` tree and wrapping the route handlers.
34
36
35
-
To provide the same functionality when using `@react-router/serve` we need to open up a new API. Currently, I am proposing a new `instrumentations` export from `entry.server`. This will be run on the server build in `createRequestHandler` and that way can work without a custom server. This will also allow custom-server users today to move some more code from their custom server into React Router by leveraging these new exports.
37
+
To provide the same functionality when using `@react-router/serve` we need to open up a new API. Currently, I am proposing a new `instrumentations` export from `entry.server`. This will be applied to the server build in `createRequestHandler` and that way can work without a custom server. This will also allow custom-server users today to move some more code from their custom server into React Router by leveraging these new exports.
38
+
39
+
A singular instrumentation function has the following shape:
40
+
41
+
```tsx
42
+
function intrumentationFunction(doTheActualThing, info) {
43
+
// Do some stuff before starting the thing
44
+
45
+
// Do the the thing
46
+
awaitdoTheActualThing();
47
+
48
+
// Do some stuff after the thing finishes
49
+
}
50
+
```
51
+
52
+
This API allows for a few things:
53
+
54
+
- Consistent API for instrumenting any async action - from a handler, to a navigation, to a loader, or a middleware
55
+
- By passing no arguments to `doTheActualThing()` and returning no data, this restricts the ability for instrumentation code to alter the actual runtime behavior of the app. I.e., you cannot modify arguments to loaders, nor change data returned from loaders. You can only report on the execution of loaders.
56
+
- The `info` parameter allows us to pass relevant read-only information, such as the `request`, `context`, `routeId`, etc.
57
+
- Nesting the call within a singular scope allows for contextual execution (i.e, `AsyncLocalStorage`) which enables things like nested OTEL traces to work properly
58
+
59
+
Here's an example of this API on the server:
36
60
37
61
```tsx
38
62
// entry.server.tsx
39
63
40
64
exportconst instrumentations = [
41
65
{
42
-
// Wrap incoming request handlers. Currently applies to _all_ requests handled
43
-
// by the RR handler, including:
44
-
// - manifest reqeusts
66
+
// Wrap the request handler - applies to _all_ requests handled by RR, including:
67
+
// - manifest requests
45
68
// - document requests
46
69
// - `.data` requests
47
70
// - resource route requests
48
71
handler({ instrument }) {
49
72
// Calling instrument performs the actual instrumentation
50
73
instrument({
51
-
// Provide the instrumentation implementation for the equest handler
74
+
// Provide the instrumentation implementation for the request handler
In both of these cases, we'll handle the instrumentation at the router creation level. And by passing `instrumentRoute` into the router, we can properly instrument future routes discovered via `route.lazy` or `patchRouteOnNavigation`
133
156
157
+
### Error Handling
158
+
159
+
It's important to note that the "handler" function will never throw. If the underlying loader/action throws, React Router will catch the error and return it out to you in case you need to perform some conditional logic in your instrumentation function - but your entire instrumentation function is thus guaranteed to run to completion even if the underlying application code errors.
160
+
161
+
```tsx
162
+
function intrumentationFunction(doTheActualThing, info) {
163
+
let { error } =awaitdoTheActualThing();
164
+
// error will be undefined if the underlying handler succeeded,
165
+
// or contain the error if it threw
166
+
167
+
if (error) {
168
+
// ...
169
+
} else {
170
+
// ...
171
+
}
172
+
}
173
+
```
174
+
175
+
You should not be using the instrumentation logic to report errors though, that's better served by `entry.server.tsx`'s `handleError` and `HydratedRouter`/`RouterProvider``unstable_onError` props.
176
+
177
+
If your throw from your instrumentation function, we do not want that to impact runtime application behavior so React Router will gracefully swallow that error with a console warning and continue running as if you had returned successfully.
178
+
179
+
In both of these examples, the handlers and all other instrumentation functions will still run:
180
+
181
+
```tsx
182
+
// Throwing before calling the handler - we will detect this and still call the
183
+
// handler internally
184
+
function intrumentationFunction(doTheActualThing, info) {
185
+
somethingThatThrows();
186
+
awaitdoTheActualThing();
187
+
}
188
+
189
+
// Throwing after calling the handler - error will be caught internally
190
+
function intrumentationFunction2(doTheActualThing, info) {
191
+
awaitdoTheActualThing();
192
+
somethingThatThrows();
193
+
}
194
+
```
195
+
134
196
### Composition
135
197
136
-
Instrumentations is an aray so that you can compose together multiple independent instrumentations easily:
198
+
Instrumentations is an array so that you can compose together multiple independent instrumentations easily:
0 commit comments