Guides

Browser

The root entry in the browser: the console sink, shipping over HTTP, the synchronous context.

The root entry, @unhingged/logit, has no Node imports and bundles for the browser with any bundler. The other entries do not — import them only on the server.

The console#

In a real browser — the sink checks for document — consoleSink() uses the console's own methods, console.debug / info / warn / error by level, and passes objects, not strings: the rendered message, then the properties outside the message as an object, then the error. The devtools keep them expandable and the stack clickable. (Off Node without a document — the Next.js edge runtime, a worker — the same sink writes JSON lines through console.log instead, since nobody is looking at a devtools panel there.)

import { createLogger, consoleSink } from '@unhingged/logit';

export const log = createLogger({ minimumLevel: 'debug', sinks: [consoleSink()] });
log.info('Cart has {Count} items', 3);
// ▸ Cart has 3 items

consoleSink({ format: 'json' }) prints the JSON line instead, for copy-paste into an issue.

Shipping to the server#

The HTTP sink runs in the browser: fetch with keepalive: true, batches, retries, a bounded queue, and a flush on pagehide so the last events of a session are sent when the tab closes or the user navigates away.

export const log = createLogger({
  minimumLevel: 'info',
  properties: { Page: location.pathname },
  sinks: [consoleSink(), httpSink({ url: '/api/logs', batchSize: 20, flushInterval: 5000, maxQueue: 500 })],
});

A matching route handler is in Next.js → Client components. Keep maxQueue small — it is memory in someone's tab — and the minimum at info.

Context is synchronous#

There is no AsyncLocalStorage in a browser, so LogContext falls back to a plain stack: a run() or push() is visible to synchronous code inside it and lost across an await. For per-user or per-page properties use forContext / properties, which are bound to the logger and need no context:

export const log = createLogger({ properties: { SessionId: sessionId }, sinks: [...] });
const checkoutLog = log.forContext({ CartId: cart.id });

Bundling notes#

  • The package is ESM-first with sideEffects: false; unused sinks tree-shake out.
  • process.env is read behind a guard — LOGIT_LEVEL and colour detection simply do nothing without it. A bundler that inlines process.env.NODE_ENV makes withEnvironment() work.
  • AbortController and crypto.randomUUID are used when present and skipped when not.
  • LogContext.push() needs Symbol.dispose for using; the handle's dispose() method works everywhere.