Bug reports from inside your app, with the evidence attached.
"The save button does nothing" is not something you can act on. BugBottle collects what was actually going on when someone noticed — the page, the viewport, recent console errors, the element they point at, optionally a screenshot — and sends it as JSON to a route you already own. You render the form; it handles the capture.
MIT-licensed, zero runtime dependencies, no hosted service.npm install bugbottle
What it does
Error trackers catch what throws. They cannot catch what merely looks wrong, and they rarely tell you what the person was doing at the time. BugBottle sits on the other side: a person says "this is wrong", and the report arrives with enough context to reproduce it.
- Headless
- You own the markup. The library owns state, capture and submit — as a React hook, or as three plain functions that work with any framework or none.
- Bring your own backend
- A report is a JSON body on a
fetch. The receiving end is a route you write; validation helpers for it ship in the same package. - Small
- Core about 0.8 kB gzipped. With element picker and React hook 3.9 kB. The ready-made panel 6.9 kB. The everything-in-one script tag 11.6 kB.
- Forwards reports
- Server-side helpers send a report on to email via Resend, Slack, Discord, a plain webhook or a GitHub issue. Keys stay on the server.
- Your language, your brand
- Eight bundled locales, every string overridable, and the optional panel is themed with a handful of CSS variables.
- Defensive on the server
- Every field a browser sends is trimmed, clipped and checked before it reaches storage — including the PNG signature and size of a screenshot.
What it is not: not a dashboard, not session replay, not a hosted service. If you want annotated issues filed in Jira by a vendor, look at Marker.io or Jam. If you want to record everything a user does, look at rrweb. BugBottle is the smallest thing that turns "it's broken" into a reproducible payload.
Who it is for
Teams shipping a web app who want a "report a problem" button that produces useful reports, without adding a third-party service, a script from another domain or another monthly bill. It fits internal tools, SaaS products and client projects where the data has to stay on your own infrastructure.
Install and use
npm install bugbottle
npm install html-to-image # optional, only if you want screenshots
Without npm: npm install github:mahope/bugbottle#v0.4.0, or import the built files from jsDelivr — dist/ is committed for that purpose.
1. Record console errors, then build a form (React)
import { initConsoleBuffer } from "bugbottle";
import { useBugReport } from "bugbottle/react";
import { htmlToImage } from "bugbottle/html-to-image"; // optional
initConsoleBuffer(); // once, as early as your app can manage
function ReportForm() {
const report = useBugReport({
endpoint: "/api/feedback",
screenshot: htmlToImage, // leave out to disable screenshots
});
return (
<form data-bugbottle onSubmit={(e) => { e.preventDefault(); void report.submit(); }}>
{report.types.map((t) => (
<button key={t} type="button" onClick={() => report.setType(t)}>{t}</button>
))}
<textarea value={report.message} onChange={(e) => report.setMessage(e.target.value)} />
<button type="submit" disabled={report.status === "sending"}>Send</button>
</form>
);
}
2. Anywhere else: three functions
import { captureScreenshot, pickElement, buildReport, sendReport } from "bugbottle";
import { htmlToImage } from "bugbottle/html-to-image";
const screenshot = await captureScreenshot(htmlToImage); // PNG data URL
const element = await pickElement(); // null if they pressed Escape
const report = buildReport({ type: "bug", message, screenshotDataUrl: screenshot, elements: element ? [element] : [] });
const { id } = await sendReport("/api/feedback", report);
Would rather not build a form? bugbottle/ui mounts a floating button and a small dialog in a shadow root, and dist/bugbottle.js does the same from a single <script> tag with data-endpoint, data-locale and data-brand attributes.
3. Receive it on the server
import {
decodeScreenshotDataUrl, InvalidScreenshotError, isReportType,
normaliseConsole, normaliseContext, normaliseElements, normaliseMessage,
} from "bugbottle/server";
export async function POST(req: Request) {
const payload = await req.json();
const message = normaliseMessage(payload.message);
if (!message) return Response.json({ error: "Write a message first" }, { status: 400 });
const type = isReportType(payload.type) ? payload.type : "other";
const context = normaliseContext(payload.context);
const console = normaliseConsole(payload.console);
const elements = normaliseElements(payload.elements);
let screenshot: Uint8Array | null = null;
try {
if (payload.screenshotDataUrl) screenshot = decodeScreenshotDataUrl(payload.screenshotDataUrl);
} catch (err) {
if (!(err instanceof InvalidScreenshotError)) throw err; // a bad picture must not fail the report
}
const { id } = await save({ type, message, context, console, elements, screenshot });
return Response.json({ id }, { status: 201 });
}
Works unchanged in a Next.js route handler, Hono, Cloudflare Workers, Bun, Deno or anything else built on the web Request. The helpers keep the most recent 50 console entries and verify the real PNG signature before a screenshot reaches your storage. Full API in the README.
Bundle sizes
- Core (buildReport, sendReport, console buffer)~0.8 kB gzip
- Core + element picker + React hook3.9 kB gzip
- Ready-made panel (
bugbottle/ui)6.9 kB gzip - Single script tag (
dist/bugbottle.js)11.6 kB gzip
Price
Free. MIT license, no hosted component, nothing to subscribe to. If you need help integrating it into a product, that is the kind of work mahoje.dk does.
Questions
Where do the reports go?
Wherever your endpoint puts them. BugBottle never talks to any server of its own. The example above saves to your database; the server helpers can also forward to Slack, Discord, email or a GitHub issue.
Do screenshots work on every page?
Screenshots use html-to-image, which renders the DOM to a canvas. Cross-origin images without CORS headers, some CSS features and very large pages can produce incomplete pictures. That is why the screenshot is optional and why a bad screenshot never fails the report.
What about personal data?
The report contains what you decide to collect: the message, the URL, viewport, user agent, recent console lines, the picked element's selector and, if enabled, a screenshot. Console lines and screenshots can contain personal data, so treat the endpoint like any other place where user input lands — the normalise helpers clip sizes but do not redact content.
Does it work without React?
Yes. The React hook is a thin layer over buildReport and sendReport. Vue, Svelte, plain JavaScript and the script-tag version all use the same core.
Is it stable?
Current release is 0.4.x. It is used in production, but the public API may still change before 1.0. Changes are listed in the GitHub releases.
Who makes this
BugBottle is built by Mads Holst Jensen, an independent developer in Odense, Denmark, working as mahoje.dk. It was extracted from a client project where "can you add a report-a-bug button" turned into the question of what a bug report should contain. Issues and pull requests on GitHub.
Related reading: add a bug report form to any website, bug reports in your CI pipeline.