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:
type ScreenshotRenderer = (
root: HTMLElement,
options: { filter: (node: Node) => boolean; pixelRatio: number },
) => Promise<string>; // PNG data URLThe 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:
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:
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.