Logging & Monitoring
Log requests with @mrzr/api-client, report errors to Sentry with onError, track auth events and add tracing headers, without ever logging a token.
Request logging
Logging is per request, so debug output doesn't leak into production:
await api.get("/users", { log: true });Each logged call produces one entry, on success and on failure. It goes to onLog, or to console.info("[api-client]", entry) if you didn't set one:
const api = createClient({
onLog: (e) => console.log(`${e.method} ${e.url} → ${e.statusCode} (${e.durationMs}ms)`),
});interface LogEntry {
url: string; // the final URL, params included
method: HttpMethod;
statusCode: number;
status: boolean;
message: string;
durationMs: number; // the whole call, including a refresh and a retry
timestamp: string; // ISO 8601
error?: unknown;
}durationMs covers the whole call. A slow entry may be two round trips (a 401, a refresh and a retry), not one slow server.
Finding slow endpoints
Group by route, with IDs replaced, or you get one bucket per user:
const stats = new Map<string, { calls: number; totalMs: number }>();
const api = createClient({
onLog: (e) => {
const route = `${e.method} ${new URL(e.url).pathname.replace(/\/\d+(?=\/|$)/g, "/:id")}`;
const s = stats.get(route) ?? { calls: 0, totalMs: 0 };
stats.set(route, { calls: s.calls + 1, totalMs: s.totalMs + e.durationMs });
},
});To log every request, set log: true in a small plugin:
const logAll: ApiPlugin = {
name: "log-all",
beforeRequest: (request) => ({ ...request, config: { ...request.config, log: true } }),
};Error reporting
onError fires for every failed request, whether or not it throws. It never fires for a canceled request, and you can silence it for one call with hideErrorMessage: true.
const api = createClient({
onError: (res) => {
if (res.statusCode === 0) return; // offline or CORS: not actionable
if (res.statusCode === 401) return; // the refresh flow handles it
Sentry.captureMessage(res.message, {
level: res.statusCode >= 500 ? "error" : "warning",
extra: { statusCode: res.statusCode, errors: res.errors },
});
},
});Use onError for cross-cutting things: reporting, toasts, an offline banner. Handling a specific failure still belongs in that call's catch. res.data is included, so scrub personal data before sending it anywhere.
Auth events
const api = createClient({
onAuthStateChanged: (s) => analytics.setUser(s.isAuthenticated ? (s.user as User)?.id : null),
onAuthFailure: () => analytics.track("session_expired"),
});Neither ever receives a token, so these are safe to forward to third-party analytics.
Tracing headers
Add a correlation ID per request, or one for the whole session:
await api.get("/users", { headers: { "X-Request-Id": crypto.randomUUID() } });
const api = createClient({ baseUrl, headers: { "X-Session-Id": crypto.randomUUID() } });For a header on every request that changes each time, like a W3C traceparent, use a beforeRequest plugin.
What never gets logged
Tokens, the Authorization header, and request and response bodies never reach a LogEntry. Tokens never appear in AuthState or in the messages between tabs either.
A debug setup
export const api = createClient({
baseUrl,
onLog: (e) => console.log("[api]", e),
onError: (r) => console.error("[api error]", r),
onAuthStateChanged: (s) => console.log("[auth]", s),
onAuthFailure: () => console.warn("[auth] session ended"),
});
console.log("worker:", api.isWorker);
console.log("state:", await api.getAuthState());Frameworks
Use @mrzr/api-client with TanStack Query, SWR, React, Next.js App Router and Vue. Short, copy-paste setups for each, including server rendering.
Recipes
Copy-paste patterns for @mrzr/api-client: retry with backoff, pagination, polling, deduplication, case conversion, Zod validation and testing.