Skip to content

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 warn and error unless debug collection is explicitly needed.
  • Prefer stable tags such as service, env, and runtime; put high-cardinality values in event data, not tags.

Example:

ts
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.

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

ts
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() or offlineFirstTransport() for reload-surviving replay.
  • pageLifecycleIntegration() and useBeaconOnPageHide for 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.

ts
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; use await 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:

  1. Capture a fatal record with process.kind: "uncaughtException".
  2. Call flushSync() on sync-capable transports.
  3. Run a bounded async flush() race controlled by flushTimeoutMs (default 250 ms).
  4. 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:

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

ts
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.

Released under the MIT License.