# In-App Bug Reporting - Reference Implementation This document provides a reference implementation for adding **in-app bug reporting** to your project. The BMAD bug-tracking workflow works without this feature (using manual `bugs.md` input), but in-app reporting provides a better user experience. ## Overview The in-app bug reporting feature allows users to submit bug reports directly from your application. Reports are stored in your database and then synced to `bugs.md` by the triage workflow. ``` User -> UI Modal -> API -> Database -> Triage Workflow -> bugs.md/bugs.yaml ``` ## Components Required | Component | Purpose | Stack-Specific | |-----------|---------|----------------| | Database table | Store pending bug reports | Yes | | API: Create report | Accept user submissions | Yes | | API: Get pending | Fetch unsynced reports | Yes | | API: Mark synced | Update status after sync | Yes | | UI Modal | Bug report form | Yes | | Validation schemas | Input validation | Partially | ## 1. Database Schema ### Drizzle ORM (PostgreSQL) ```typescript // src/lib/server/db/schema.ts import { pgTable, uuid, text, timestamp, index } from 'drizzle-orm/pg-core'; export const bugReports = pgTable( 'bug_reports', { id: uuid('id').primaryKey().defaultRandom(), organizationId: uuid('organization_id').notNull(), // For multi-tenant apps reporterType: text('reporter_type').notNull(), // 'staff' | 'member' | 'user' reporterId: uuid('reporter_id').notNull(), title: text('title').notNull(), description: text('description').notNull(), userAgent: text('user_agent'), pageUrl: text('page_url'), platform: text('platform'), // 'Windows', 'macOS', 'iOS', etc. browser: text('browser'), // 'Chrome', 'Safari', 'Firefox' screenshotUrl: text('screenshot_url'), // Optional: cloud storage URL status: text('status').notNull().default('new'), // 'new' | 'synced' | 'dismissed' createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), syncedAt: timestamp('synced_at', { withTimezone: true }) }, (table) => [ index('bug_reports_organization_id_idx').on(table.organizationId), index('bug_reports_status_idx').on(table.status), index('bug_reports_created_at_idx').on(table.createdAt) ] ); export const BUG_REPORT_STATUS = { NEW: 'new', SYNCED: 'synced', DISMISSED: 'dismissed' } as const; export const REPORTER_TYPE = { STAFF: 'staff', MEMBER: 'member', USER: 'user' } as const; ``` ### Prisma Schema ```prisma model BugReport { id String @id @default(uuid()) organizationId String @map("organization_id") reporterType String @map("reporter_type") reporterId String @map("reporter_id") title String description String userAgent String? @map("user_agent") pageUrl String? @map("page_url") platform String? browser String? screenshotUrl String? @map("screenshot_url") status String @default("new") createdAt DateTime @default(now()) @map("created_at") syncedAt DateTime? @map("synced_at") @@index([organizationId]) @@index([status]) @@index([createdAt]) @@map("bug_reports") } ``` ## 2. Validation Schemas ### Zod (TypeScript) ```typescript // src/lib/schemas/bug-report.ts import { z } from 'zod'; export const createBugReportSchema = z.object({ title: z .string() .trim() .min(5, 'Title must be at least 5 characters') .max(200, 'Title must be 200 characters or less'), description: z .string() .trim() .min(10, 'Description must be at least 10 characters') .max(5000, 'Description must be 5000 characters or less'), pageUrl: z.string().url().optional(), userAgent: z.string().max(1000).optional(), platform: z.string().max(50).optional(), browser: z.string().max(50).optional() }); export const markSyncedSchema = z.object({ ids: z.array(z.string().uuid()).min(1, 'At least one ID is required') }); export const SCREENSHOT_CONFIG = { maxSizeBytes: 5 * 1024 * 1024, // 5MB maxSizeMB: 5, allowedTypes: ['image/jpeg', 'image/png', 'image/webp'] as const } as const; ``` ## 3. API Endpoints ### POST /api/bug-reports - Create Report ```typescript // SvelteKit: src/routes/api/bug-reports/+server.ts import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { db } from '$lib/server/db'; import { bugReports } from '$lib/server/db/schema'; import { createBugReportSchema } from '$lib/schemas/bug-report'; export const POST: RequestHandler = async ({ request, locals }) => { // Determine reporter from auth context if (!locals.user) { return json({ error: { code: 'UNAUTHORIZED' } }, { status: 401 }); } const body = await request.json(); const result = createBugReportSchema.safeParse(body); if (!result.success) { return json({ error: { code: 'VALIDATION_ERROR', message: result.error.issues[0]?.message } }, { status: 400 }); } const { title, description, pageUrl, userAgent, platform, browser } = result.data; const [newReport] = await db .insert(bugReports) .values({ organizationId: locals.user.organizationId, reporterType: 'staff', reporterId: locals.user.id, title, description, pageUrl, userAgent, platform, browser }) .returning(); return json({ data: { bugReport: { id: newReport.id, title: newReport.title, createdAt: newReport.createdAt.toISOString() } } }, { status: 201 }); }; ``` ### GET /api/bug-reports/pending - Fetch for Triage ```typescript // SvelteKit: src/routes/api/bug-reports/pending/+server.ts import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { db } from '$lib/server/db'; import { bugReports, BUG_REPORT_STATUS } from '$lib/server/db/schema'; import { eq } from 'drizzle-orm'; export const GET: RequestHandler = async () => { const reports = await db .select() .from(bugReports) .where(eq(bugReports.status, BUG_REPORT_STATUS.NEW)) .orderBy(bugReports.createdAt); // Map to workflow-expected format const formatted = reports.map((r) => ({ id: r.id, title: r.title, description: r.description, reporterType: r.reporterType, reporterName: 'Unknown', // Join with users table for real name platform: r.platform, browser: r.browser, pageUrl: r.pageUrl, screenshotUrl: r.screenshotUrl, createdAt: r.createdAt.toISOString() })); return json({ data: { reports: formatted, count: formatted.length } }); }; ``` ### POST /api/bug-reports/mark-synced - Update After Sync ```typescript // SvelteKit: src/routes/api/bug-reports/mark-synced/+server.ts import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { db } from '$lib/server/db'; import { bugReports, BUG_REPORT_STATUS } from '$lib/server/db/schema'; import { inArray } from 'drizzle-orm'; import { markSyncedSchema } from '$lib/schemas/bug-report'; export const POST: RequestHandler = async ({ request }) => { const body = await request.json(); const result = markSyncedSchema.safeParse(body); if (!result.success) { return json({ error: { code: 'VALIDATION_ERROR', message: result.error.issues[0]?.message } }, { status: 400 }); } const updated = await db .update(bugReports) .set({ status: BUG_REPORT_STATUS.SYNCED, syncedAt: new Date() }) .where(inArray(bugReports.id, result.data.ids)) .returning({ id: bugReports.id }); return json({ data: { updatedCount: updated.length, updatedIds: updated.map((r) => r.id) } }); }; ``` ## 4. UI Component ### Svelte 5 (with shadcn-svelte) ```svelte !o && onClose()}> Report a Bug
{ e.preventDefault(); handleSubmit(); }} class="space-y-4">