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:
@@ -0,0 +1,95 @@
|
||||
import { createRouter } from '../../create-router';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { DATA_PATH } from '@@/data-path';
|
||||
|
||||
const DISCORD_WEBHOOK_URL = process.env.DISCORD_BUG_REPORT_WEBHOOK;
|
||||
|
||||
export const bugReportRouter = createRouter();
|
||||
|
||||
bugReportRouter.post('/', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const body = ctx.get('body') as Record<string, unknown>;
|
||||
|
||||
const description = body.description as string | undefined;
|
||||
const contextRaw = body.context as string | undefined;
|
||||
const screenshot = body.screenshot as File | undefined;
|
||||
|
||||
if (!description?.trim()) {
|
||||
return ctx.text('Description is required', 400);
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const uuid = crypto.randomUUID().slice(0, 8);
|
||||
const dirName = `${timestamp}-${uuid}`;
|
||||
const reportDir = join(DATA_PATH, 'bug-reports', dirName);
|
||||
|
||||
await mkdir(reportDir, { recursive: true });
|
||||
|
||||
const context = contextRaw ? JSON.parse(contextRaw) : null;
|
||||
const report = {
|
||||
description: description.trim(),
|
||||
context,
|
||||
reporter: { id: user.id, email: user.email, name: user.name },
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await Bun.write(join(reportDir, 'report.json'), JSON.stringify(report, null, 2));
|
||||
|
||||
let screenshotBuffer: Buffer | null = null;
|
||||
if (screenshot instanceof File) {
|
||||
screenshotBuffer = Buffer.from(await screenshot.arrayBuffer());
|
||||
await Bun.write(join(reportDir, 'screenshot.png'), screenshotBuffer);
|
||||
}
|
||||
|
||||
if (DISCORD_WEBHOOK_URL) {
|
||||
await sendToDiscord(report, screenshotBuffer);
|
||||
}
|
||||
|
||||
return ctx.json({ ok: true, id: dirName });
|
||||
});
|
||||
|
||||
type BugReport = {
|
||||
description: string;
|
||||
context: { url?: string; userAgent?: string; viewport?: { width: number; height: number }; apiError?: { status: number; message: string } | null } | null;
|
||||
reporter: { id: number; email: string; name: string };
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
async function sendToDiscord(report: BugReport, screenshot: Buffer | null) {
|
||||
const embed = {
|
||||
title: 'Bug Report',
|
||||
description: report.description,
|
||||
color: 0xed4245,
|
||||
fields: [
|
||||
{ name: 'Reporter', value: `${report.reporter.name} (${report.reporter.email})`, inline: true },
|
||||
{ name: 'URL', value: report.context?.url ?? 'N/A', inline: false },
|
||||
{ name: 'Viewport', value: report.context?.viewport ? `${report.context.viewport.width}x${report.context.viewport.height}` : 'N/A', inline: true },
|
||||
{ name: 'Browser', value: shortenUA(report.context?.userAgent), inline: true },
|
||||
],
|
||||
timestamp: report.createdAt,
|
||||
};
|
||||
|
||||
if (report.context?.apiError) {
|
||||
embed.fields.push({ name: 'Last API Error', value: `${report.context.apiError.status}: ${report.context.apiError.message}`, inline: false });
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.append('payload_json', JSON.stringify({ embeds: [embed] }));
|
||||
|
||||
if (screenshot) {
|
||||
form.append('files[0]', new Blob([screenshot], { type: 'image/png' }), 'screenshot.png');
|
||||
}
|
||||
|
||||
const res = await fetch(DISCORD_WEBHOOK_URL!, { method: 'POST', body: form });
|
||||
if (!res.ok) {
|
||||
console.error('[bug-report] Discord webhook failed:', res.status, await res.text());
|
||||
}
|
||||
}
|
||||
|
||||
function shortenUA(ua?: string): string {
|
||||
if (!ua) return 'N/A';
|
||||
const browser = ua.match(/(Chrome|Firefox|Safari|Edge|Brave|OPR)\/[\d.]+/)?.[0] ?? '';
|
||||
const os = ua.match(/\(([^)]+)\)/)?.[1]?.split(';')[0] ?? '';
|
||||
return [browser, os].filter(Boolean).join(' — ') || ua.slice(0, 80);
|
||||
}
|
||||
Reference in New Issue
Block a user