Request tracing
Give each user action a trace ID, and send the ID with each request that the action starts.
The goal
A user action, for example a click on "Save", starts many requests. A trace ID for each action connects the requests in the logs of the server. With the library, the code that sends a request reads the trace ID from the context. It is not necessary to give the ID to each function.
Make the storage
// trace.ts
import { AsyncLocalStorage } from "async-browser-context";
export type Trace = { traceId : string; action : string };
export const traceStorage = new AsyncLocalStorage<Trace>({ name : "trace" });
/** Starts `action` in a new trace. */
export function traced<R>(action : string, fn : () => Promise<R>) : Promise<R> {
return traceStorage.run({ traceId : crypto.randomUUID(), action }, fn);
}Add the trace ID to each request
// api.ts
import { traceStorage } from "./trace";
export async function api<T>(path : string, init : RequestInit = {}) : Promise<T> {
const headers = new Headers(init.headers);
const trace = traceStorage.getStore();
if (trace) headers.set("x-trace-id", trace.traceId);
const response = await fetch(path, { ...init, headers });
return await response.json() as T;
}Start a trace for each action
saveButton.addEventListener("click", () => traced("save", async () => {
const draft = await api<Draft>("/api/draft");
await api("/api/documents", { method : "POST", body : JSON.stringify(draft) });
await refreshList(); // its requests also get the trace ID
}));Each api() use in the action reads the same trace ID: after await, in then callbacks and in timers. Two actions at the same time keep different IDs.
Note
A dependency without the transform does not read the trace after its own native await. Use the Vite plugin, which transforms the dependencies, or bind the callbacks that you give to the dependency. The boundaries page tells how.