Evidence

Screenshots

captureScreenshot takes a renderer rather than importing one. A bundler resolves every import it can see, optional or not, so a built-in import of html-to-image would make it a hard dependency for every application — including the ones that never take a picture.

bugbottle/html-to-image is the ready-made renderer. Any function with the same shape works in its place:

ts
type ScreenshotRenderer = (
  root: HTMLElement,
  options: { filter: (node: Node) => boolean; pixelRatio: number },
) => Promise<string>; // PNG data URL

The capture renders from the DOM, not from the screen, so it can only ever show the page the reporter is on — never another tab, another window, or the desktop behind it. That is a deliberate limit rather than a missing feature.

What the reporter typed is hidden before the picture is taken; see Masking.

Marking the picture #

A screenshot of the whole page rarely says which part of it is wrong. The reporter can mark it before sending: a rectangle to point at something, an arrow to point from somewhere, and a blur.

The blur is the privacy tool as much as the marking one. It does not put a frosted rectangle over the region — it reads those pixels back out of the canvas, averages them in 12-pixel blocks and paints the averages on top, so the original pixels are gone from the exported PNG. Whoever receives the report cannot recover what was under it. That is the tool to reach for when a picture caught a customer name the masking rules did not know about. The region is rounded outwards to whole pixels before it is averaged, so a drag covers a fraction of a pixel too much rather than a fraction too little.

In the ready-made panel it is a button, "Edit picture", that appears once a picture is attached; it opens a toolbar and the canvas in place of the preview. The panel does not import the annotator — you hand it in, the way you hand in a screenshot renderer, a scrubber or a signer, so that a panel nobody marks a picture in does not ship a canvas editor:

ts
import { mountBugbottle } from "bugbottle/ui";
import { createAnnotator } from "bugbottle/annotate";

mountBugbottle({ endpoint: "/api/feedback", screenshot: htmlToImage, annotate: createAnnotator });

Leave annotate out (or pass false) and nothing on screen leads to an editor. The script tag is the build that carries everything, so it wires the annotator up for you and data-annotate="off" is how you switch it off there. Sending with the editor still open keeps the marks: the picture is folded back into the report either way, so a blur cannot be lost by skipping "Done". Escape in the editor leaves the editor rather than the panel — it is the way out that drops the marks nobody confirmed, puts the preview back and returns focus to "Edit picture"; while a mark is being drawn it still abandons that mark, and with the editor closed it still closes the panel.

With your own form, use the annotator directly. It is its own entry point, about 1.4 kB gzipped, and it draws on a canvas you supply:

ts
import { createAnnotator } from "bugbottle/annotate";
import { buildReport, sendReport, captureScreenshot } from "bugbottle";
import { htmlToImage } from "bugbottle/html-to-image";

const picture = await captureScreenshot(htmlToImage);
const annotator = createAnnotator(canvas, picture);
await annotator.ready; // the canvas is now the size of the picture

toolbar.onclick = (e) => annotator.setTool(e.target.value); // "rect" | "arrow" | "blur"
undoButton.onclick = () => annotator.undo();

// When the reporter is finished, the marked picture is the attachment.
const report = buildReport({
  type: "bug",
  message: message.value,
  screenshotDataUrl: annotator.toDataUrl(),
});
await sendReport("/api/feedback", report);

createAnnotator(canvas, dataUrl, options) takes tool, colour (defaults to the computed --bb-primary), lineWidth, blockSize and an onChange called with the number of marks. It returns ready, setTool, getTool, undo, clear, count, toDataUrl and destroy. Drawing is by pointer, so a mouse, a pen and a finger all work; Backspace or Delete undoes and Escape abandons the mark being drawn — with nothing being drawn the key is left to travel, so the form around the canvas can decide what it means. Nothing in it is a string the reporter reads, so it needs no locale of its own — the panel supplies the labels around it.

Edit this page on GitHub