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

crumbtrail-core

v0.31.0

Published

Capture the console, network, DOM and state around a bug and hand it to a coding agent as evidence. Zero-dependency browser SDK, with optional React and Tauri adapters on the crumbtrail-core/react and crumbtrail-core/tauri subpaths. See https://crumbtrail

Readme

crumbtrail-core

The capture engine behind Crumbtrail. It records what actually happened around a bug — the console, the network calls, the DOM interactions, the state — and hands it over as evidence a coding agent can act on, instead of a screenshot and a vague repro.

Framework-agnostic, with no dependencies.

Install

npm install crumbtrail-core

Or let the wizard install and wire it for you:

npx crumbtrail

Setup

Call Crumbtrail.init() once, at your app's entry point:

import { Crumbtrail, PRESET_PASSIVE } from "crumbtrail-core";

Crumbtrail.init({
  ...PRESET_PASSIVE,
  httpEndpoint: "https://api.crumbtrail.ai",
  httpAuthToken: process.env.CRUMBTRAIL_KEY,
});

That's the whole integration — capture runs in the background from there.

Catching the requests that beat init

init() usually runs from an async import, so the fetches that render the first screen can finish before the network patch exists. Those requests leave no net.req and, more importantly, no correlation header, so their backend and database events cannot be joined to the session later.

Add one side-effect import above everything else in your entry file:

import "crumbtrail-core/early";

It patches fetch and XMLHttpRequest synchronously, stamps the same correlation headers the SDK stamps on same origin requests, and parks bounded metadata (at most 50 requests, 2 MB of body text) until init() drains it through the normal redaction pipeline. init() adopts the session id it minted, so the early requests, the live session, and the backend events all match. If init() never runs within 60 seconds, the queue is dropped and recording stops.

Presets

| Preset | Behaviour | | ---------------- | ---------------------------------------------------------------------- | | PRESET_PASSIVE | Capture continuously and auto-flag on errors and signals. The default. | | PRESET_LIGHT | Leaner capture, less overhead. | | PRESET_FULL | Everything, for a heavy debugging session. |

Flagging a bug yourself

init() returns the instance, so you can mark moments explicitly:

const crumbtrail = Crumbtrail.init({ ...PRESET_PASSIVE, httpEndpoint });

crumbtrail.mark("checkout: submitted");

await crumbtrail.flagBug({
  note: "Payment failed with no error toast",
  tags: ["checkout"],
});

Also on the instance: addEvent, registerStateProvider, setEnv, createRequestHeaders, pause, resume, stop, getSessionId.

Redaction is on by default

Crumbtrail is meant to be pointed at real traffic, so scrubbing is not opt in. Tokens, cookies, storage values, page text, input values, and database row values are redacted before an event enters the browser buffer. See BROWSER_REDACTION_POLICY and the redact* helpers if you want to inspect or tighten the policy.

Keeping a field the classifier would otherwise drop

JSON request bodies go through a deny biased per value classifier: numbers and short enum like strings are kept, everything else is replaced by a shape placeholder. That is the right default and it is wrong for one specific case, which is when the text a user submitted is the defect. A stored XSS payload, a search term with a quote in it, an address line a validator wrongly rejects, a decimal comma amount: a hash of any of those tells an agent nothing.

redaction.keepFields names those fields. It is matched on the whole field name, never as a substring, and it applies to JSON keys, query string parameters and form input values alike:

Crumbtrail.init({
  redaction: { keepFields: ["body", "q", "postalCode"] },
});

What a keep does and does not do:

  • It overrides the built in name heuristics only. Those match by substring and have real false positives, so without an override an app whose schema trips one (auth matches author, pan matches panel) cannot capture the field at all.
  • It never disables value based detection. An email, a JWT, a card number, a token, or a high entropy secret sitting in a kept field is still redacted.
  • Your own denyFields entry still wins over your keep for the same name.
  • On a form input it is matched against the field's name, and a password, email or tel input is never kept whatever it is called.

The capture server takes the same list, so a name kept in a db.diff row is also kept in the request body and query string that produced it:

crumbtrail-server serve --keep-field body,q,postalCode

CRUMBTRAIL_KEEP_FIELDS sets it from the environment; flags add to it rather than replacing it. The active list is printed at boot, because it is the one setting that makes the server store more than it did before.

Production capture

Page text, keystrokes, clipboard content, DOM snapshots, and database row values are masked before they enter the local ring buffer. Add data-crumbtrail-unmask to one element when its text or value is safe to capture. Add data-crumbtrail-block to exclude an element and its descendants entirely.

Input values answer to the redaction policy rather than to a blanket mask, so they behave the same way the same value does in a request body: numbers and short enum-like strings are recorded, and free prose, emails, JWTs, card numbers, IBANs and high-entropy strings are replaced by [REDACTED]. A password, email or tel input is redacted on its type before anything reads it. redaction.keepFields recovers free text in a field you name.

This is why the price a shopper typed and the price the request carried can both appear in one capture, which is the whole of a large class of filter and validation defects.

To record none of it, set redaction: { captureInputValues: false }. Every input becomes [REDACTED] whatever the field is called, and keepFields cannot bring one back. Operators can enforce the same thing at the capture server with crumbtrail-server serve --no-input-values or CRUMBTRAIL_CAPTURE_INPUT_VALUES=0, which overrides whatever the application asked for — the switch only ever removes.

Remote capture policy can only add masking. Clear text or values are captured only when data-crumbtrail-unmask is added to that individual safe element.

For consent managed applications, begin capture only after your consent manager grants permission:

const crumbtrail = Crumbtrail.init({
  consentMode: "required",
  configEndpoint: "https://capture.example.com/config",
  projectKey: "project_123",
});

crumbtrail.consent(true);
crumbtrail.identify({ accountId: "account_123", userId: "user_456" });
await crumbtrail.flag();

Global Privacy Control is respected by default. Email shaped identifiers are discarded by identify.

Set flightRecorder: true to buffer locally until an error, signal, widget action, or flag() triggers capture. The recorder adds the configured tail before finalizing the report. A cloud config response can disable capture with killSwitch: true; the SDK clears its buffer as soon as that response arrives.

Live probes

A capture config response can also name probes to run, under a probes field. A probe answers one question about the running application and rests its answer as a probe.result event, so an agent reading the session afterwards sees what the app actually looked like at that moment rather than what the code implies it looked like. Four probes exist and the server can name nothing else: runtime.env, storage.snapshot, network.inflight and flags.current.

The whole of what a server may say is a name. Probes take no selector, no URL, no path and no expression, so no value from a config response reaches probe code, and one entry shaped as an object rather than a string refuses the entire field rather than being salvaged. A name outside the four is dropped without normalization. At most four probes run per poll, repeats are collapsed, and a list longer than 64 entries is refused before it is read.

They run one at a time, only after the remote policy has been applied, and each gets its own deadline: 2 seconds, 200 rows and 32 KB of serialized rows by default. A probe that hangs is abandoned at the deadline. A kill switch, a stop() or a newer poll ends the run between probes. A probe never throws: a failure rests as a probe.result carrying ok: false and a short reason, because "this source was not available in production" is itself an answer. Values pass through the same redaction the rest of capture uses.

A probe is answered by whichever application instance is polling when the request goes out. That is not the session an agent is investigating, and by the time a bundle is being read that session has ended, so a probe reports on a bystander rather than on the person who hit the defect. Read every reading as "the app looks like this right now", never as "the failing session looked like this".

storage.snapshot is the reading where that distinction changes what is emitted, so its keys get a stricter treatment than the ordinary storage capture uses. It reports which keys exist, how many, what pattern each follows and how many bytes each holds. It does not report any stored value, which is replaced unconditionally, and it does not report the identifying part of a key: an email address, a user id, an order number, a phone number or any other span that could carry a value is replaced with *, so session:[email protected]:cart is reported as session:*:cart. A key from which no ordinary word survives is reported as [REDACTED_KEY]. A plain word in a key is kept, which is what makes two patterns tellable apart, so a key that spells out a person's name in ordinary letters is the one case this cannot catch.

Response body summaries (net.res)

net.res keeps carrying the redacted response body as text in d.body. It also carries d.bodyMeta, the size facts plus a bounded parsed view: { ct, bytes, truncated?, data?, arrayTotal? }. data is present for JSON responses of 32 KB or less, is derived from the already redacted body, and is capped at four levels of nesting, 20 array items, and 120 character strings. arrayTotal records the real length of each array that was cut, keyed by its path, so a truncated list is never mistaken for a short one. Anything else, including non JSON and oversized responses, carries the content type and byte size only.

Content Security Policy refusals (csp)

A policy refusal is the quietest way for a feature to stop existing. The browser refuses a script, a stylesheet, an image or a connection, and the page reports nothing: no JavaScript error, because the code never ran, and no failed request, because the request was never made. A capture built on errors and network traffic is blind to it by construction, while the user watches a button do nothing.

The errors collector now listens for securitypolicyviolation and emits { directive, disposition, blockedUri, file, line, st }. disposition keeps a report-only policy distinguishable from an enforced one, since a report-only policy blocks nothing. The sample a browser may attach is a fragment of the page's own script or style text and is never read.

Streaming responses

Response.text() resolves when the stream closes, which for a streaming response may be never. The collector used to await that before emitting net.res, so a streamed request - progress updates, model tokens, a log tail, a large export - was recorded as a request that never came back.

The response body is now read under a budget: 2 seconds, or networkMaxBodySize, whichever comes first. Whatever arrived inside the budget is captured, and net.res carries streaming: true when the body had not finished. The event is timestamped when the response ARRIVED rather than when its body ended, so a long stream cannot push its own event past the effects it caused.

Worker traffic (worker.msg)

The workers collector is on by default. A worker is a second program with its own global scope: nothing this SDK patches exists inside it, so a fetch made there is not recorded and an error thrown there never reaches the page's handlers. Applications put parsing, pricing, sync and offline queues in workers, so a capture that says nothing about them can be silent about the whole computation that produced a wrong answer.

What is observable from the window is the conversation. The collector wraps the Worker constructor and emits { id, script, op } for start, for error (with the message the page never saw), and for each message as op: "post" and op: "recv" with a redacted body and a seq number. Messages answer to the same structured redaction as request bodies. A payload that is not text-shaped - a transferred buffer, a port - reports opaque: true rather than an invented summary.

Bounded like socket frames: 2 KB per message, 40 messages per worker, 200 across the session.

Form-shaped request bodies

fetch(url, { body: new URLSearchParams(form) }) and body: new FormData(form) are how a form submission is normally written, and both used to be discarded whole as non-text. Every field the user filled in went missing from the capture because of the container it arrived in.

Both are now read without consuming them, and the same body redaction runs over the result. A FormData is rendered as a JSON object keyed by field name, so a repeated field becomes an array. File parts are described, never read: the form field name survives as the key and the part reports { file: true, bytes }. The file's own name and MIME type are free text and answer to the same value rules as any other string in a body.

GraphQL operation identity (net.req d.gql)

Every GraphQL request in an application goes to the same URL, so a record keyed on method and path reports one endpoint doing everything. When a request body parses as GraphQL, net.req carries d.gql with op (query, mutation, subscription, or unknown for a persisted query that sent no document), name when the document or the request names one, and batch when the request carried several operations.

Only the operation name and type are read. Both are code, identical for every user. Variables are never inspected here: they carry user input, and the body redaction that runs over them is the only thing entitled to decide what survives.

Server-sent events (net.sse)

The eventSource collector wraps the EventSource constructor and emits { url, op } for open, error, and close, with count (messages received so far) on error and close, and reopen: true when a stream to the same URL is recreated within 30 seconds of the last failure. That makes a stream which quietly drops and reconnects visible while the page shows stale data. Message payloads are never read.

WebSocket traffic (net.ws)

The webSocket collector is on by default. It wraps the WebSocket constructor and emits { id, url, op } for open, error, and close, with received and sent counts plus the close code and clean flag, and reopen: true when a socket to the same URL is recreated within 30 seconds of the last failure.

It also carries the frames themselves, in both directions, as op: "send" and op: "msg" with a redacted body and a seq number. Frames answer to the same structured redaction policy as request and response bodies, including your own denyFields and keepFields, so a socket publishes no class of value your HTTP traffic does not. Binary frames report binary: true and a byte count only, never content.

Three bounds keep a chatty socket from filling a session: 2 KB per frame, 40 frames per socket, and 200 frames across the session. Past a cap the socket keeps counting and stops quoting, so the close event still reports the true totals.

Listener accounting (ui.listeners)

The listeners collector is on by default and disabled by PRESET_LIGHT. It patches addEventListener and removeEventListener to keep a running count per event type, and emits { total, byType, url } on every navigation and whenever the total grows by 25 since the last reading. That makes a view which re-subscribes on every render and never unsubscribes visible before it starts firing handlers twice. It stores counts only, never a target or a listener, and only sees registrations made after init().

On-screen numbers (ui.num)

The uiNumbers collector is on by default. It scans the visible DOM for labeled numeric tokens — a number paired with its nearby text label, e.g. Subtotal: $84.00 — and emits compact ui.num snapshots of {label, value, unit}. These power the display-arithmetic detector (subtotal + tax vs total) and the UI-vs-API divergence detector. It never captures raw DOM or HTML, only the short label and the parsed number.

This means numeric amounts shown on screen are captured together with their labels. Labels run through the redaction classifier, so PII-shaped labels (emails, tokens) and card-number-shaped values are dropped entirely. The honest residual: a label that is itself a human name reads as ordinary free text and can survive capture, so the case to think about is numbers rendered next to names — payroll, CRM rows, admin tables (Jane Doe $84,000).

Opting out, narrowest first:

  • Add the sensitive label to redaction.denyFields to drop just those items.
  • Use PRESET_LIGHT, which disables the collector.
  • Disable it entirely with uiNumbers: false in Crumbtrail.init({...}).

Hidden elements (hidden, aria-hidden="true", display:none, visibility:hidden) are already skipped.

Each ui.num snapshot carries the document lang and dir, and the same collector emits one ui.layout event per navigation with { dir, lang, scrollW, clientW, overflowX, url }. Together they make a locale-vs-rendered-number contradiction, and horizontal overflow from a long translated label, joinable without capturing any DOM.

React

The React bindings live on the crumbtrail-core/react subpath. Nothing extra to install: React is an optional peer dependency, so a project that never imports this subpath never has to have React at all.

Wrap a subtree so a render error is flagged as a bug with the surrounding session already captured:

import { Crumbtrail, PRESET_PASSIVE } from "crumbtrail-core";
import { CrumbtrailErrorBoundary } from "crumbtrail-core/react";

const crumbtrail = Crumbtrail.init({
  ...PRESET_PASSIVE,
  httpEndpoint: "https://api.crumbtrail.ai",
  httpAuthToken: process.env.CRUMBTRAIL_KEY,
});

export function App() {
  return (
    <CrumbtrailErrorBoundary logger={crumbtrail} fallback={<p>Something broke.</p>}>
      <Checkout />
    </CrumbtrailErrorBoundary>
  );
}

| Prop | Type | Description | | --- | --- | --- | | logger | Crumbtrail | The instance returned by Crumbtrail.init(). | | children | ReactNode | The subtree to guard. | | fallback | ReactNode | Optional UI to render after an error. |

useBugState registers a value so it is attached to any bug flagged while the component is mounted:

import { useBugState } from "crumbtrail-core/react";

function Checkout({ crumbtrail }) {
  const [cart, setCart] = useState([]);
  const [step, setStep] = useState("address");

  useBugState(crumbtrail, "cart", cart);
  useBugState(crumbtrail, "step", step);

  // ...
}

Values are redacted by default using the same policy as the rest of the SDK, so a state field called token or password never leaves the browser in the clear. Pass { captureRawState: true } as the fourth argument only when you are certain the value is safe.

React 18 or newer. For React Native and Expo, use crumbtrail-react-native instead: its peer dependencies are native and must not reach a web bundle.

Tauri

The Tauri v2 transport lives on the crumbtrail-core/tauri subpath. It replaces the HTTP transport with native IPC, so a desktop app needs no server process. @tauri-apps/api is an optional peer dependency, which every Tauri v2 frontend already has.

import { Crumbtrail, PRESET_PASSIVE } from "crumbtrail-core";
import { TauriTransport } from "crumbtrail-core/tauri";

const logger = Crumbtrail.init({
  ...PRESET_PASSIVE,
  transportInstance: new TauriTransport(),
});

TauriTransport sends events over Tauri's invoke() IPC to the Rust side, which owns session directories, NDJSON writing, blob storage and post-processing. That Rust half is the tauri-plugin-crumbtrail crate, and it must be registered separately — see packages/tauri/README.md for the Cargo dependency, the .plugin(...) call and the capability permission. Without those three steps every Crumbtrail invoke fails.

Related packages

| Package | Use it for | | ---------------------------------------------------------------------------------- | ---------------------------------------------------------- | | crumbtrail | The npx crumbtrail setup wizard | | crumbtrail-node | Self-hosted server, Express middleware, MCP evidence tools | | crumbtrail-react-native | React Native and Expo bindings |

The React error boundary and state-capture hook, and the Tauri desktop transport, are subpaths of this package rather than packages of their own — see React and Tauri above.

Links

  • Website — https://crumbtrail.ai
  • Docs — https://crumbtrail.ai/docs
  • How it works — https://crumbtrail.ai/how-it-works
  • Source — https://github.com/CrumbtrailDev/crumbtrail-cli
  • Issues — https://github.com/CrumbtrailDev/crumbtrail-cli/issues

License

MIT