Overview
Drop-in bug reporting that initialises nothing on its own: the host app chooses where reports go (HTTP, Sentry, or any callback), whether screenshots are captured, and what context is included. Privacy is the default.
- Role
- Sole author and maintainer
- Timeframe
- Aug 13 to 15, 2026 (0.1.0 to 0.2.1)
- Status
- Published, MIT, pre-1.0 (
0.2.1) - In use
- Adopted in Diwalco's web app
Stack
- React 18.2 / 19
- TypeScript (strict)
- tsup (ESM + CJS)
- Vitest
- Testing Library
- axe-core
- Playwright
- Redocly (OpenAPI 3.1)
Context
Apps need an in-app “report a bug” flow that doesn't lock them into one vendor or silently collect data. I first hand-built a Sentry-only feedback dialog for Diwalco, then wrote this general, provider-neutral package and adopted it back into Diwalco's web app, replacing ~580 lines of hand-rolled feedback UI.
Try it
This is the published package, loaded only when you open it. Reports go to a local callback, so nothing leaves your browser; after sending, you can inspect exactly what a transport would have received.
Try anonymous mode, the details toggle, or a screenshot.
What it does
- Three components: a standalone form, a native
<dialog>modal, and a floating widget. - A report model with message, severity, steps, expected and actual behaviour, optional contact, opt-in technical context and one screenshot, in a versioned envelope with a client-generated ID.
- Anonymous mode guarantees contact details are omitted.
- Transports: HTTP multipart (timeouts,
AbortSignal, RFC 7807 problem details,Retry-After, typed retryable errors); a Sentry adapter that wraps the host's ownsendFeedbackwith no Sentry dependency bundled; or any callback. - Pluggable screenshot capture (DOM or
getDisplayMedia) with bounded canvas sizes. - A published server contract: an OpenAPI 3.1 spec for
POST /v1/bug-reports.
Deep dives
A privacy leak hidden in a vendor default
- In 0.2.1: with technical context off, Sentry still received the reporter's live URL, because its
sendFeedbackback-fillsurlwhen the key is absent. The fix always states the key (an explicitundefinedoverrides the default), with an internal type so the public type stays assignable to Sentry's. Regression-tested.
Keeping the report out of its own screenshot
- In 0.2.0: screen capture photographed the open dialog, and closing it first would lose the draft. Providers can now declare
requiresHiddenUi: the UI hides by visibility (nothing unmounts), waits for a bounded next paint (a 100 ms deadline for background tabs), captures, and returns. Shipped as a minor version because it changes default behaviour.
Accessibility beyond the basics
- A native modal with Escape; a backdrop that only closes when both pointer-down and pointer-up land on it, so a text-selection drag never discards a draft; a fully keyboard-operable severity listbox; linked validation errors; a polite live region with focus moved to the success heading;
prefers-reduced-motionand forced-colours support; and an axe-core scan in tests.
Cross-bundle error identity
- Error classes are registered on a global symbol, so
instanceofworks across separately bundled ESM and CJS entry points.
Numbers
- Runtime dependencies
- 0
- Entry points: a React-free
core, two transports, two capture providers, and the root - 6
- Unit tests (static count), plus 9 Playwright end-to-end tests run across 3 browsers
- 70+
- Lines of hand-rolled feedback UI it replaced in Diwalco's web app
- ~580
Limits and roadmap
- No CI yet; the quality gate is the publish script.
- No manual screen-reader pass recorded; keyboard and axe-core tested.
- Bundle size not yet tracked in the repo.