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 itemsconsoleSink({ 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.envis read behind a guard —LOGIT_LEVELand colour detection simply do nothing without it. A bundler that inlinesprocess.env.NODE_ENVmakeswithEnvironment()work.AbortControllerandcrypto.randomUUIDare used when present and skipped when not.LogContext.push()needsSymbol.disposeforusing; the handle'sdispose()method works everywhere.