Evidence

Performance and storage

Two questions a report almost never answers and almost always needs to: was it slow, and what state was the browser in? bugbottle/perf answers both without bundling web-vitals. It is a separate entry point and opt-in, and it should be called as early as your app can manage — ideally in the same module that mounts the panel.

ts
import { initPerf } from "bugbottle/perf";

const stop = initPerf();

The observers are created with buffered: true, so the LCP that was painted while your application was still booting is delivered anyway; the browser's buffer is finite, which is why "as early as you can" is not a formality.

report.perf carries the Web Vitals the browser has already measured, the milestones from the navigation entry, the long tasks and — on Chromium only — the JS heap. Every field is optional, because every field is a measurement that may not have happened, and a figure that was never measured is left out rather than sent as a zero.

jsonc
{
  "lcp": 3412,          // milliseconds, the last candidate the browser reported
  "cls": 0.081,         // cumulative layout shift, three decimals
  "inp": 210,           // the worst interaction, in milliseconds
  "ttfb": 128,
  "domContentLoaded": 641,
  "load": 1200,
  "longTasks": { "count": 3, "totalMs": 480 },
  "memory": { "usedMB": 32, "limitMB": 2048 }
}

Two simplifications, said plainly because a number in a bug report is only worth what its definition is. CLS here is the sum of every shift that did not follow a recent input, where the Web Vitals definition takes the worst session window instead: on a page that shifts repeatedly this reads high rather than low, which is the safe direction for evidence. INP here is the worst interaction, where the real metric is roughly the 98th percentile: on the handful of interactions a session usually has these are the same number, and on a long session this over-reports rather than hides. first-input is observed too, so a browser without the event type still contributes its FID.

report.storage says what was in the browser's stores at the moment the report was written — not when initPerf ran, because what matters is the state the reporter was actually in.

jsonc
{
  "local": [{ "key": "theme", "length": 4 }, { "key": "authToken", "length": 132 }],
  "session": [{ "key": "cart", "length": 7 }],
  "cookies": ["session", "consent"],
  "values": { "tenant": "acme" }
}

Key names and value lengths, never values, and cookie names without cookie values. That a key called authToken is present and 132 characters long is usually the whole answer to "why was I logged out"; its contents are the session itself, and a bug report is not a place to put one. The one exception is values, and it is opt-in per key:

ts
initPerf({
  allowValues: ["tenant", "featureFlags"],  // nothing travels unless it is named here
  maxKeys: 50,                              // keys listed per store; 50 is also the ceiling
  storage: false,                           // measure the timings only
  vitals: false,                            // snapshot the storage only
});

allowValues looks each name up in localStorage first and then sessionStorage, and clips what it finds to 200 characters. A cookie value is never included, whatever the allow-list says. Run scrubReport over the report as well if the allow-listed keys can hold anything written by a person: the scrubber redacts storage.values and the cookie names, and leaves the key names and lengths alone, since those are the shape of the store and the point of the snapshot.

buildReport attaches both blocks on its own while initPerf is measuring, as perf and storage; pass includePerf: false to leave them out of one report. toMarkdown renders a "Performance" table and a collapsed "Storage" block. initPerf returns the stop() that disconnects the observers and unregisters both — the same thing resetPerf() does. The ready-made panel takes it the same way the network log is taken: perf: initPerf on mountBugbottle starts it on mount and stops it on destroy(), and { on: initPerf, storage: false } tunes it. In the one-script-tag build it is data-perf on the script tag, which is the same switch.

Edit this page on GitHub