A floating bug-report button for React / Next.js apps.
- Captures a full-page screenshot (via
html-to-image, handles modern CSS colour functions) - Records console logs, warnings, errors, uncaught exceptions and unhandled promise rejections
- Compresses the screenshot to under 2 MB automatically
- Rate-limited on both client (localStorage) and server (in-memory per IP)
- Two consent checkboxes before submission
- Ships a ready-made Next.js App Router API handler that sends the report by email via Resend
- Pure Tailwind CSS — no shadcn/ui dependency
npm install bug-report-widget
# peer dependencies (skip any you already have)
npm install lucide-react html-to-image
# optional — only needed for the built-in email handler
npm install resend// app/layout.tsx (or _app.tsx in Pages Router)
import { BugReportButton } from "bug-report-widget";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<BugReportButton
endpoint="/api/report"
authToken={session?.user?.id} // optional — sent as Bearer token
reporter={{ name: session?.user?.name, email: session?.user?.email }}
enabled={true}
/>
</body>
</html>
);
}Option A — zero config (uses environment variables)
// app/api/report/route.ts
export { POST } from "bug-report-widget/nextjs";Option B — custom configuration
// app/api/report/route.ts
import { createNextjsBugReportHandler } from "bug-report-widget/nextjs";
export const POST = createNextjsBugReportHandler({
resendApiKey: process.env.RESEND_API_KEY!,
emailTo: process.env.BUG_REPORT_EMAIL!,
emailFrom: "Bug Reporter <noreply@yourapp.com>",
// Optional: check a feature flag in your database before accepting reports
isEnabled: async () => {
const settings = await db.settings.findUnique({ where: { id: "singleton" } });
return settings?.bugReportEnabled !== false;
},
// Optional: persist the report to your own database
onReport: async (report, clientIp) => {
await db.bugReports.create({ data: { ...report, clientIp } });
},
});| Variable | Required | Description |
|---|---|---|
RESEND_API_KEY |
Yes (for email) | Your Resend API key |
BUG_REPORT_EMAIL |
Yes (for email) | Recipient address for bug reports |
EMAIL_FROM |
No | Sender address (default: Bug Reporter <noreply@example.com>) |
| Prop | Type | Default | Description |
|---|---|---|---|
endpoint |
string |
"/api/report" |
POST endpoint URL |
authToken |
string |
— | Sent as Authorization: Bearer <token> |
enabled |
boolean |
true |
Hide the button entirely when false |
reporter |
{ name, email } |
— | Included in the email report |
storageKey |
string |
"bug_report_last_ts" |
localStorage key for client-side rate limiting |
Use handleBugReport directly if you are using a different framework:
import { handleBugReport } from "bug-report-widget";
// Express example
app.post("/api/report", async (req, res) => {
const result = await handleBugReport(req.body, req.ip, {
onReport: async (report) => {
// your email / Slack / webhook logic
console.log("Bug report received:", report);
},
isEnabled: async () => true,
});
res.status(result.status).json(result.body);
});interface ReportPayload {
screenshot?: string; // base64 JPEG data URL
comment: string; // max 1000 chars
consoleErrors: string[]; // up to 50 captured log lines
url: string;
userAgent: string;
timestamp: string; // ISO 8601
reporter?: { name?: string | null; email?: string } | null;
}- User clicks the floating camera button (bottom-right corner, 40% opacity).
- The component hides any open dialogs from the capture, then calls
html-to-imageto render the page as a JPEG, cropping cross-origin images to avoid CORS errors. - The image is compressed iteratively until it is under 2 MB.
- The user optionally adds a comment and ticks two consent checkboxes.
- The payload is
POSTed to your endpoint along with the accumulated console log buffer. - On the server, IP rate limiting and payload validation run before
onReportis called. - Resend sends an HTML email with the screenshot, console logs and metadata.
MIT