npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

audit-log-lib

v2.0.0

Published

Browser-based audit logging: IndexedDB storage, batching + offline retry queue, PII redaction, Excel/JSON export, and a real-time backend channel (SignalR) the UI can listen to.

Readme

audit-log-lib

Browser audit logging done right. Stores every event in IndexedDB, then batches them to your backend with an offline retry queue, redacts PII before anything leaves the browser, and opens a real-time channel so your backend can push commands the UI listens to (e.g. "send me your logs now").

  • 📦 IndexedDB storage — no server required to get started
  • 🚚 Batching + offline queue — logs survive reloads & network drops, retried with backoff
  • 🔒 PII redaction — passwords, tokens, emails, card numbers masked before store/send
  • 🔁 Backend → UI push — built-in SignalR transport; backend fires a command, your UI reacts
  • 🧯 Global captureconsole.* and uncaught errors logged automatically
  • 📤 Export — download logs as JSON, Excel (.xlsx), or both in a ZIP

Installation

npm install audit-log-lib

Two features have optional peer dependencies — install them only if you use them.

# only if you use SignalRTransport (real-time backend channel)
npm install @microsoft/signalr

# only if you export to Excel — downloadLogs('excel' | 'both')
npm install exceljs

downloadLogs('json') works with no extra packages. Calling 'excel'/'both' without exceljs installed throws a clear, catchable error.

Quick start

import { AuditLog, setupGlobalLogging } from 'audit-log-lib';

const audit = new AuditLog();

// Optional: mirror console.* and uncaught errors into the audit log
setupGlobalLogging(audit);

await audit.log('user.login', { userId: 123 });
await audit.log('payment.failed', { orderId: 456 }, 'error');

That's it — logs are persisted locally in IndexedDB. Add a backend below when you're ready.

Send logs to your backend

Option A — real-time channel (SignalR, recommended)

Two-way: the library uploads batches to your hub, and your backend can push commands back down to the browser.

import { AuditLog, SignalRTransport } from 'audit-log-lib';

const audit = new AuditLog({
  transport: new SignalRTransport({
    url: 'https://your-api.com/hubs/audit',
    accessTokenFactory: () => localStorage.getItem('token') ?? '',
  }),
  // tune batching (optional)
  shipper: { batchSize: 50, flushIntervalMs: 5000 },
});

await audit.log('user.login', { userId: 123 }); // queued, batched, shipped automatically

Your .NET hub receives UploadLogs and can push an AuditCommand:

public class AuditHub : Hub
{
    // Client calls this with a batch of entries
    public async Task UploadLogs(List<AuditLogEntry> entries)
    {
        // ...persist / forward to your sink...
    }
}

// From anywhere on the server — ask a connected client to flush right now:
await hub.Clients.User(userId).SendAsync("AuditCommand", new { type = "pull" });

Option B — simple HTTP sink (no extra deps)

If you just want to POST each entry yourself, use the onLog hook:

const audit = new AuditLog({
  onLog: async (entry) => {
    await fetch('https://your-api.com/api/logs', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(entry),
    });
  },
});

With a transport, delivery is automatic and batched (use this). onLog is a low-level escape hatch for one-off custom sinks and is ignored when a transport is set.

Backend → UI: listen for commands

The backend can fire a command and your UI reacts to it. Known commands are also handled by the library automatically:

| Command | Auto behavior | Use case | |---|---|---| | pull / flush | flushes the queue now | backend wants the latest logs | | clear | clears local logs | wipe a client after investigation | | pause / resume | pauses/resumes shipping | back off a noisy client | | setLevel (payload: LogLevel) | raises the min level | quiet a client remotely | | anything custom | — | you handle it in on('command', …) |

audit.on('command', (cmd) => {
  if (cmd.type === 'pull') showToast('Backend requested your logs');
  if (cmd.type === 'toast') showToast(String(cmd.payload)); // your own command type
});

PII redaction

Sensitive data is replaced before it is stored or sent — so nothing leaks to IndexedDB or the network. Opt in by passing redaction:

const audit = new AuditLog({
  redaction: {
    // defaults cover password, token, secret, apiKey, card, ssn, ... + email/card patterns
    strategy: 'mask',           // 'mask' | 'remove' | 'hash'
    replacement: '[REDACTED]',  // for 'mask'
  },
});

await audit.log('user.signup', { email: '[email protected]', password: 'hunter2' });
// stored & shipped as: { email: '[REDACTED]', password: '[REDACTED]' }

| Strategy | Result | |---|---| | mask | replaced with replacement (default [REDACTED]) | | remove | key dropped entirely | | hash | deterministic hash — lets you correlate without exposing the value |

You can override keys and patterns:

redaction: {
  keys: ['password', 'cardNumber', 'authToken'],
  patterns: [/\b\d{3}-\d{2}-\d{4}\b/g],
  strategy: 'hash',
}

API

new AuditLog(options?)

| Option | Type | Default | Description | |---|---|---|---| | dbName | string | 'audit-log-db' | IndexedDB database name | | storeName | string | 'logs' | IndexedDB object store name | | maxDays | number | 30 | Prune entries older than this (runs on startup + daily) | | maxEntries | number | undefined | Cap stored entries; oldest synced ones trimmed past it (never drops un-sent logs) | | minLevel | LogLevel | 'debug' | Drop entries below this level | | redaction | RedactionOptions | undefined | Redact PII before store/send | | transport | RemoteTransport | undefined | Backend channel (e.g. SignalRTransport); enables batched delivery | | shipper | ShipperOptions | see below | Batching/retry tuning | | getContext | () => object | undefined | Injected into every entry's context (userId, version, …) | | onLog | (entry) => void \| Promise | undefined | One-off sink; ignored when transport is set |

ShipperOptions: batchSize (50), flushIntervalMs (5000), retryBaseMs (2000), retryMaxMs (60000).

Methods

await audit.log(action, payload?, level?, context?); // level: 'debug'|'info'|'warn'|'error'
await audit.flush();          // force-send queued entries now
await audit.getLogs();        // all stored entries, oldest→newest
await audit.downloadLogs('json' | 'excel' | 'both');
await audit.clearLogs();
audit.getSessionId();         // auto-attached to every entry for correlation
audit.destroy();              // stop timers + close transport (call on unmount)

Events — audit.on(event, handler) → returns an unsubscribe fn

| Event | Payload | Fired when | |---|---|---| | command | AuditCommand | backend pushes a command | | log | AuditLogEntry | an entry is written locally | | shipped | AuditLogEntry[] | a batch is delivered | | shipError | Error | a send attempt fails (will retry) | | state | TransportState | connection state changes |

const off = audit.on('shipped', (batch) => console.log('sent', batch.length));
off(); // unsubscribe

setupGlobalLogging(audit, options?)

Mirrors console.log/warn/error and window errors into the log. Returns a teardown function.

const stop = setupGlobalLogging(audit, { console: true, errors: true });
// later: stop();

Custom transports

SignalRTransport is just one implementation of RemoteTransport. Bring your own (SSE, WebSocket, fetch) by implementing the interface:

interface RemoteTransport {
  start(): Promise<void>;
  stop(): Promise<void>;
  send(entries: AuditLogEntry[]): Promise<void>; // MUST throw on failure so it's retried
  onCommand(handler: (cmd: AuditCommand) => void): void;
  onStateChange?(handler: (state: TransportState) => void): void;
}

Types

type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface AuditLogEntry {
  id?: number;
  action: string;
  payload?: unknown;
  timestamp: number;
  level: LogLevel;
  context?: Record<string, unknown>;
  sessionId?: string;
  synced?: 0 | 1;
}

interface AuditCommand {
  type: 'flush' | 'pull' | 'clear' | 'pause' | 'resume' | 'setLevel' | (string & {});
  payload?: unknown;
  id?: string;
}

Migrating from v1

v2 is a major release. Because of semver, npm will not upgrade existing ^1 installs automatically — you stay on v1 until you opt in with npm install audit-log-lib@2. When you do upgrade, note these changes:

  • IndexedDB is migrated automatically. v1 stored entries without an id key; v2 uses keyPath: 'id'. On first run, v2 reads your existing logs, rebuilds the store with ids, and marks the old entries as already-sent so they are not re-shipped to your backend. No action needed, no data loss.
  • onStorageFull was removed. It cleared all logs when maxEntries was hit. v2 replaces it with safer trimming: past maxEntries, only the oldest already-synced entries are dropped, so un-sent logs are never lost. To flush to a backend, use a transport (auto-batched) or audit.flush().
  • onLog is now an escape hatch. It still works when you have no transport, but it is ignored once a transport is set (the shipper handles delivery). It now also receives a complete entry — including timestamp, which v1 omitted.
  • AuditLogEntry is stricter. timestamp and level are now required (they were optional). Only relevant if you build entries by hand in TypeScript.

Everything else — log(), getLogs(), downloadLogs(), clearLogs(), destroy(), setupGlobalLogging() — is unchanged.

Browser support

Requires IndexedDB — all modern browsers (Chrome, Firefox, Edge, Safari).

License

Apache 2.0 — see LICENSE.

Runtime dependencies are file-saver (MIT) and jszip (used under its MIT option). @microsoft/signalr (MIT) and exceljs (MIT) are optional peer dependencies, pulled in only when you use the SignalR channel or Excel export.