The widget
A single deferred script, a few kilobytes gzipped, with no dependencies. It renders a quiet tab on the left or right edge of the viewport in your accent colour, and it does nothing else until someone clicks it.
Everything it renders lives inside a Shadow DOM, so your stylesheet cannot reach into it and its styles cannot leak out. It is a labelled dialog with focus trapping, Escape-to-close, visible focus rings, a live region for status messages, and motion that respects prefers-reduced-motion.
On load it makes exactly one request: for its own configuration. If that request fails, the widget renders nothing and logs nothing to your visitors. A tool embedded on someone else’s site should fail silently or not at all.
The screenshot code is not part of that. It lives in a second file, fetched with a dynamic import the first time somebody presses “Attach a screenshot”, so a visitor who never presses it downloads the always-on bundle and nothing else. Both files carry their own gzip budget and the build fails if either exceeds it.
The form
Four inputs come as standard: what kind of feedback this is, how severe it is, a one-line summary, and what happened. Feedback is not only bugs — the type chips can offer any of bug, design, copy, content, performance, idea, question and other, and choosing design reveals a second question asking which aspect: colour, layout, spacing, typography, imagery, animation, responsiveness, or something else.
Severity is a five-point picker, 1 Cosmetic through 5 Blocking, worded from the reporter’s side rather than yours — “I can’t finish what I came to do”, not “P1”. It is optional by default, because a required field is a reason to abandon a form, and where it is set it prefixes the issue title as [Bug · S4] … and sorts your inbox. A steps-to-reproduce field is still available per project, off by default.
Two optional inputs sit alongside them: a microphone button that dictates into the description where you have enabled it, and an “Attach a screenshot” button that takes one still image of the page. Both are covered below.
On top of that you add your own fields: text, long text, dropdown, checkbox, email, URL, or number. Each can be required. Each is validated on the server against the definition you saved, so a submission cannot contain a key you never defined or a dropdown value you never offered.
The email field is optional and comes with an opt-in checkbox for “tell me when this is fixed”. Nobody is required to identify themselves to report a problem.
The screenshot
Pressing the button does five things in a fixed order: the widget hides its own panel, blanks everything private, renders the page to an image, restores the page exactly as it was, and shows the reporter a preview. Attach keeps it. Remove throws it away and the report goes without one. Nothing is captured on a page where nobody presses the button.
Blanking is not a setting. Every input and textarea on the page has its value replaced with bullets, and anything carrying data-bugloop-mask is not drawn at all. That happens on the page, before the pixels exist, so no unmasked copy is ever produced. A tool that ships to other people’s sites must not be able to exfiltrate a half-typed password because somebody pressed a button.
The image is produced by rendering your own DOM to a canvas. There is no screen-sharing prompt, so it structurally cannot see another tab, another window, or anything else on the reporter’s desktop. It is also an approximation: cross-origin images without CORS headers render blank, iframe contents and a tainted canvas cannot be read at all, and a few effects such as backdrop-filter are redrawn rather than copied. Treat it as a reference image, not a forensic record — the screenshots documentation lists every limit, and the two attributes you use to mark what must never be captured.
Images are stored in a private bucket with no client-side access of any kind. They are uploaded server-side after the same project key, origin and rate-limit checks a report passes, with the file type read from its leading bytes rather than believed. Reads are time-limited signed URLs. Deleting a report deletes its image; deleting a project deletes every image under it.
Dictation, and the one third-party request
The microphone button uses the browser’s own Web Speech API, which costs the bundle nothing and is simply absent where the browser does not support it. In Chrome, that API streams the audio to Google for transcription.
BugLoop’s pitch is that it makes no third-party requests, so this is the exception and it is treated as one: dictation is off unless you turn it on for a project, the widget shows a one-line notice the first time the button is pressed, and it is written down here, in the docs, and in the privacy pages rather than left for somebody to discover in a network tab. BugLoop never receives the audio — only the text the browser hands back, and a label recording that dictation was used.
The ingest path
A submission passes six checks before a row is written: schema validation, project lookup, origin allowlist, two independent rate-limit windows, bot heuristics, and custom-field validation against your saved definitions. The security model covers each in detail.
What gets stored is what the visitor wrote, plus the page URL, page title, viewport, and browser user agent. Their IP address is hashed with a server-side salt for rate limiting and the raw address is never persisted.
A screenshot travels separately. It is uploaded while the reporter is still looking at the preview and handed back as a one-shot token, which the submission redeems. The redemption is a single conditional update in Postgres, so a replayed token loses the race rather than attaching one image to two reports — and a token that turns out to be unknown or already used is ignored, because a missing picture must never cost you the report.
The issue
The title is the visitor’s summary with the feedback type as a prefix, and the severity too where they set one — so a bug arrives as [Bug · S4] Checkout button does nothing on Safari. The body carries the description, every custom field under its human label, and a context block with the page, viewport, browser, timestamp, and reporter email if given.
For GitHub that is a Markdown issue with your labels and assignees applied. For Jira it is converted to Atlassian Document Format so the ticket renders as formatted text rather than raw Markdown.
The screenshot arrives differently in each. GitHub’s REST API cannot take a binary when it creates an issue, so the body embeds a signed link and the pixel dimensions — signed for a year, because an issue outlives any session. Jira can take a real file, so the adapter creates the ticket and then uploads the image as an attachment. If the object cannot be read or the upload fails, the issue is still created: a ticket without its picture is worth immeasurably more than no ticket.
Exactly one issue is created per report per destination. GitHub offers no idempotency key on issue creation, so BugLoop derives a deterministic key per feedback-and-destination pair and puts a unique index behind it. Retries and concurrent workers cannot produce a duplicate.
When something fails
A delivery failure never becomes the visitor’s problem. They see the confirmation, the report is safely stored, the delivery is recorded as failed with the reason, the error surfaces on your integration settings page, and you can retry it from the report once the cause is fixed.
What it does not do
No session recording, no heatmaps, no click or scroll tracking, no cookies, no localStorage, no fingerprinting. Nothing at all is collected passively — there is no phase in which the widget is watching. These are design constraints rather than a roadmap gap: the whole argument for this product is that you can have the button without the recorder.
A screenshot is not a recording. It happens once, on a press, of the page the reporter is already looking at, masked before it is taken and shown to them before it is attached. Nothing runs between presses — no second frame, no cursor path, no timeline.
And the widget makes no third-party requests, with the single exception of dictation described above, which is off unless you enable it. Nothing else in the widget contacts anyone but BugLoop.