Operations
This guide covers the parts of LoggerJS that most affect production behavior: privacy, browser buffering, crash paths, and remote delivery reliability.
Privacy
Automatic integrations are opt-in. Enable only the capture surfaces your product needs.
Recommended defaults:
- Use
redactProcessor()before any remote transport. - Allowlist HTTP headers in fetch/XHR integrations. Do not ship cookies, authorization headers, or full request bodies by default.
- Sanitize URLs when query strings may contain tokens or user data.
- Keep console capture to
warnanderrorunless debug collection is explicitly needed. - Prefer stable tags such as
service,env, andruntime; put high-cardinality values in event data, not tags.
Example:
import { captureFetchIntegration } from "@loggerjs/browser/integration-fetch";
import { redactProcessor } from "@loggerjs/processors";
const processors = [redactProcessor({ keys: ["password", "token", /secret/i] })];
const integrations = [
captureFetchIntegration({
captureRequestHeaders: ["content-type", "x-request-id"],
captureResponseHeaders: ["content-type", "x-request-id"],
sanitizeUrl: (url) => new URL(url, location.origin).origin,
}),
];redactProcessor() masks by key, exact dot path, regex, or custom matcher. Paths are relative to each redacted event field (user.password, not data.user.password). Use replacement (or the Pino-compatible censor alias) to mask values, and remove: true when the field should be omitted from the event. LoggerJS does not compile redaction paths with eval or new Function; wildcard-like regexes and deep traversal are safer than generated code but cost more than exact keys and paths.
privacyGuardProcessor() is broader: it scans selected fields for built-in and custom string patterns such as emails, bearer tokens, and card-like values. Use it as a safety net, not as a replacement for capture allowlists.
Browser Queue And Offline Replay
browserHttpTransport() batches records in memory and can persist failed payloads into an offline queue adapter. memoryBrowserHttpOfflineQueue() keeps payloads only while the page is alive; use the built-in indexedDbBrowserHttpOfflineQueue() when queued payloads must survive reloads, or pass your own adapter with the same interface.
import { browserHttpTransport, memoryBrowserHttpOfflineQueue } from "@loggerjs/browser";
const transport = browserHttpTransport({
url: "/api/logs",
maxBatchSize: 50,
maxQueueSize: 1000,
offlineQueue: memoryBrowserHttpOfflineQueue({ maxEntries: 500 }),
useBeaconOnPageHide: true,
beaconMaxBytes: 60 * 1024,
});maxBatchSize defaults to 50 and must be a positive safe integer. It both triggers flushing and limits the number of events in each new Fetch or Beacon request. Fetch batches drain serially, including the final partial batch, even with flushIntervalMs: 0. Concurrent flush() calls wait for the active drain; new logs produced during that drain are included until the queue is empty. A failed batch returns to the front of the queue and stops that drain unless an offline queue accepts its encoded payload. Successful earlier batches are not requeued.
The limit counts events, not bytes. Beacon requests also respect beaconMaxBytes; Fetch has no strict payload byte budget. Smaller batches can increase request counts and codec/transform invocations. Offline storage capacity (maxEntries) counts encoded requests, not individual events. Already stored payloads replay unchanged, including older batches above the current limit. Offline replay and live delivery do not guarantee global ordering or exactly-once delivery.
Pagehide, hidden visibility, and close() submit queued Beacon chunks synchronously, including while an earlier Fetch is pending. The in-flight Fetch batch is never also sent by Beacon; its completion is still awaited by close(). This best-effort exit path can reach the server out of order. Rejected Beacon chunks fall back to bounded Fetch batches. With transformPayload, lifecycle flushes use Fetch instead. A successful flush may mean acceptance by the offline queue or browser Beacon queue, rather than server acknowledgment.
The transport replays stored payloads with retry and backoff when the browser fires online, shortly after it is created (so payloads persisted by an earlier page load are sent; disable with offlineReplayOnStart: false), after a flush in which a live batch reached the collector, and on an explicit flush() with no live logs pending. A failed replay leaves the entries queued and is reported through onInternalError instead of failing the flush. Page lifecycle integration should be enabled when logs matter during tab close or navigation:
import { pageLifecycleIntegration } from "@loggerjs/browser";
const integrations = [pageLifecycleIntegration()];Browser storage and shutdown behavior are still best effort. sendBeacon can be size-limited or skipped during shutdown, in-memory queues disappear on reload, and IndexedDB can be unavailable, full, evicted, or blocked by an upgrade. For production browser delivery, combine:
browserHttpTransport()for normal remote delivery.indexedDbBrowserHttpOfflineQueue()orofflineFirstTransport()for reload-surviving replay.pageLifecycleIntegration()anduseBeaconOnPageHidefor last-chance flush.- Drop/queue metrics from logger meta so quota or backpressure is visible.
Node Crash Path
For process-level failures, combine captureProcessIntegration() with a transport that can flush synchronously when needed.
import { captureProcessIntegration, fileTransport, stdoutTransport } from "@loggerjs/node";
const transports = [
stdoutTransport(),
fileTransport({ path: "./logs/app.ndjson" }),
];
const integrations = [captureProcessIntegration({ exitOnUncaught: true })];Crash-path guidance:
- Keep at least one local transport for fatal process events.
- Prefer
flushSync()for final synchronous shutdown when the transport supports it; useawait flush()for normal drain-and-continue shutdowns. - Use
fileTransport({ sync: true })when every write must reach the filesystem before the log call returns. - Use HTTP/OTLP remote transports for normal delivery, not as the only fatal-path sink.
- Keep processor work synchronous and bounded; crash handlers should not perform slow enrichment.
For uncaughtException with exitOnUncaught: true, the sequence is:
- Capture a
fatalrecord withprocess.kind: "uncaughtException". - Call
flushSync()on sync-capable transports. - Run a bounded async
flush()race controlled byflushTimeoutMs(default250ms). - Exit with code
1.
Unhandled promise rejections follow the same sequence by default and exit with code 1, matching Node's default --unhandled-rejections=throw behavior that registering a listener would otherwise disable. When Node runs with --unhandled-rejections=warn, none, or warn-with-error-code, the rejection is only logged; set exitOnUnhandledRejection to override either way.
For signals with exitOnSignal: true, LoggerJS captures a fatal signal record, uses the same sync-plus-bounded-async flush sequence, then exits with the signal exit code when known (SIGTERM -> 143, SIGINT -> 130).
Remote Transport Reliability
Batch-based transports support the same core reliability options:
{
maxRecords: 100,
maxBytes: 64 * 1024,
maxWaitMs: 2000,
concurrency: 2,
maxRetries: 3,
retryBaseDelayMs: 250,
retryMaxDelayMs: 5000,
circuitBreakerFailureThreshold: 5,
circuitBreakerResetMs: 30000,
}Use byte limits when payload size matters more than event count. Use onDrop to surface queue drops into your own metrics pipeline.
Context And Trace Correlation
Use explicit child context for values known at construction time and ambient context for request-scoped values:
import { installAsyncLocalStorageContext } from "@loggerjs/node";
import { withContext } from "@loggerjs/core";
installAsyncLocalStorageContext();
await withContext({ requestId: "req_123" }, async () => {
logger.info("request started");
});Use openTelemetryTraceProcessor() to attach the active OpenTelemetry span context when an OpenTelemetry API object is available.
