# Logging & Monitoring (/docs/logging)



## Request logging [#request-logging]

Logging is per request, so debug output doesn't leak into production:

```ts
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:

```ts
const api = createClient({
  onLog: (e) => console.log(`${e.method} ${e.url} → ${e.statusCode} (${e.durationMs}ms)`),
});
```

```ts
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 [#finding-slow-endpoints]

Group by route, with IDs replaced, or you get one bucket per user:

```ts
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](/docs/plugins):

```ts
const logAll: ApiPlugin = {
  name: "log-all",
  beforeRequest: (request) => ({ ...request, config: { ...request.config, log: true } }),
};
```

## Error reporting [#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`.

```ts
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 [#auth-events]

```ts
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 [#tracing-headers]

Add a correlation ID per request, or one for the whole session:

```ts
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](/docs/plugins#writing-a-plugin).

## What never gets logged [#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 [#a-debug-setup]

```ts
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());
```
