Evidence

What happened before

bugbottle/breadcrumbs records the last few things the reporter did, so the report says what was happening when it broke — not only what broke. It is a separate entry point: an application that does not import it does not carry it.

ts
import { initBreadcrumbs } from "bugbottle/breadcrumbs";

initBreadcrumbs();

Four things are recorded and nothing else: clicks (a short selector and the element's visible text), navigation (path and query, including pushState and replaceState, which fire no event of their own), form submits (the selector only) and visibility changes. The last 30 are kept.

jsonc
[
  { "ts": "2026-09-07T08:12:29.100Z", "kind": "click", "target": "button#save-order", "text": "Save order" },
  { "ts": "2026-09-07T08:12:30.400Z", "kind": "navigation", "from": "/orders/1", "to": "/orders/2?tab=notes" },
  { "ts": "2026-09-07T08:12:31.000Z", "kind": "submit", "target": "form#checkout" },
  { "ts": "2026-09-07T08:12:34.700Z", "kind": "visibility", "to": "hidden" }
]

buildReport attaches them on its own while the buffer is recording, as breadcrumbs; pass includeBreadcrumbs: false to leave them out of one report. toMarkdown renders them as a "What happened before" list.

Deliberately absent: input values, keystrokes, and anything read out of a field. A breadcrumb says where someone clicked, never what they typed. On top of that, three controls are yours:

ts
initBreadcrumbs({
  maxEntries: 30,
  beforeBreadcrumb: (crumb) =>
    crumb.to?.startsWith("/admin") ? null : { ...crumb, to: crumb.to?.replace(/\/\d+/, "/:id") },
});

getBreadcrumbs() returns a copy of the timeline, and resetBreadcrumbs() empties it, removes the listeners and puts history back as it found it. initBreadcrumbs returns that same function as its stop(), so a caller that started the recorder can undo it without importing a second name.

Edit this page on GitHub