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.
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.
{
"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.
{
"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:
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.