bug report tool — screenshot, console logs, and context sent to discord

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 18:09:51 +00:00
co-authored by Claude Opus 4.6
parent 30f06e91e4
commit 74ace56120
10 changed files with 315 additions and 0 deletions
@@ -0,0 +1,27 @@
import { Bug } from 'lucide-react';
import { useBugReport } from './use-bug-report';
import { BugReportDialog } from './BugReportDialog';
export const BugReportButton = () => {
const bugReport = useBugReport();
return (
<>
<button
onClick={bugReport.handleOpen}
className="fixed bottom-20 right-4 z-[90] flex h-10 w-10 items-center justify-center rounded-full bg-destructive text-destructive-foreground shadow-lg transition-transform hover:scale-110"
aria-label="Report a bug"
>
<Bug size={20} />
</button>
<BugReportDialog
open={bugReport.open}
capturing={bugReport.capturing}
submitting={bugReport.submitting}
screenshot={bugReport.screenshot}
onClose={bugReport.handleClose}
onSubmit={bugReport.handleSubmit}
/>
</>
);
};
@@ -0,0 +1,75 @@
import { useState, useMemo } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Loader2 } from 'lucide-react';
type BugReportDialogProps = {
open: boolean;
capturing: boolean;
submitting: boolean;
screenshot: Blob | null;
onClose: () => void;
onSubmit: (description: string) => void;
};
export const BugReportDialog = ({ open, capturing, submitting, screenshot, onClose, onSubmit }: BugReportDialogProps) => {
const [description, setDescription] = useState('');
const previewUrl = useMemo(() => (screenshot ? URL.createObjectURL(screenshot) : null), [screenshot]);
const handleSubmit = () => {
onSubmit(description);
setDescription('');
};
const canSubmit = description.trim().length > 0 && !capturing && !submitting;
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Report a Bug</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="overflow-hidden rounded-md border bg-muted">
{capturing ? (
<div className="flex h-40 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : previewUrl ? (
<img src={previewUrl} alt="Screenshot" className="max-h-48 w-full object-contain" />
) : (
<div className="flex h-40 items-center justify-center text-sm text-muted-foreground">
No screenshot captured
</div>
)}
</div>
<Textarea
placeholder="Describe what went wrong..."
value={description}
onChange={(ev) => setDescription(ev.target.value)}
rows={4}
autoFocus
/>
<p className="text-xs text-muted-foreground">
Console logs, browser info, and recent API errors are collected automatically.
</p>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={submitting}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!canSubmit}>
{submitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Submit
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,72 @@
import { useState, useCallback } from 'react';
import { toBlob } from 'html-to-image';
import { useClient } from 'hooks/useClient';
import { useAuth } from 'hooks/useAuth';
import { useGlobal } from 'hooks/useGlobal';
import type { ErrorDetails } from 'hooks/useClient';
import { getConsoleBuffer } from '@/lib/console-buffer';
import { toast } from 'sonner';
export const useBugReport = () => {
const [open, setOpen] = useState(false);
const [screenshot, setScreenshot] = useState<Blob | null>(null);
const [capturing, setCapturing] = useState(false);
const [submitting, setSubmitting] = useState(false);
const client = useClient();
const { user } = useAuth();
const [apiError] = useGlobal<ErrorDetails | null>('API_ERROR', null);
const handleOpen = useCallback(async () => {
setCapturing(true);
try {
const blob = await toBlob(document.body, { pixelRatio: 0.75 });
setScreenshot(blob);
} catch (err) {
console.error('Screenshot capture failed:', err);
} finally {
setCapturing(false);
setOpen(true);
}
}, []);
const handleClose = useCallback(() => {
setOpen(false);
setScreenshot(null);
setCapturing(false);
}, []);
const handleSubmit = useCallback(
async (description: string) => {
setSubmitting(true);
try {
const context = {
url: window.location.href,
userAgent: navigator.userAgent,
viewport: { width: window.innerWidth, height: window.innerHeight },
timestamp: new Date().toISOString(),
consoleLogs: getConsoleBuffer(),
apiError,
user: user ? { id: user.id, email: user.email, name: user.name } : null,
};
const formData = new FormData();
formData.append('description', description);
formData.append('context', JSON.stringify(context));
if (screenshot) {
formData.append('screenshot', screenshot, 'screenshot.png');
}
await client.post('/bug-report', formData);
toast.success('Bug report submitted');
handleClose();
} catch {
toast.error('Failed to submit bug report');
} finally {
setSubmitting(false);
}
},
[screenshot, client, user, apiError, handleClose],
);
return { open, capturing, submitting, screenshot, handleOpen, handleClose, handleSubmit };
};