/** * Bug Report AI Agent * * Automatically analyzes bug report submissions and generates: * - Summary of the bug * - Root cause analysis * - Reproduction steps * - Suggested fix * - Estimated effort (low/medium/high) * - Suggested severity (low/medium/high/critical) * * Triggers email notifications to the reporter and assigned team. */ import { v } from "convex/values"; import { Agent, createThread } from "@convex-dev/agent"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { components, internal } from "../_generated/api"; import { action, internalAction, internalQuery } from "../_generated/server"; import type { BugSeverity, Effort } from "../schema"; // Helper to create OpenRouter provider dynamically // This is created dynamically because Convex components don't inherit parent env vars function createOpenRouterProvider(apiKey: string) { return createOpenAICompatible({ name: "openrouter", baseURL: "https://openrouter.ai/api/v1", headers: { Authorization: `Bearer ${apiKey}`, }, }); } // Agent name constant const AGENT_NAME = "Bug Report Analyst"; // Agent instructions (shared between dynamic agent instances) const BUG_REPORT_AGENT_INSTRUCTIONS = `You are the Bug Report Analyst, an AI assistant that analyzes bug report submissions. When given a bug report, you will: 1. **Summarize** the bug in 2-3 clear sentences 2. **Analyze Root Cause** - identify potential underlying causes based on the symptoms described 3. **Extract Reproduction Steps** - create clear, numbered steps to reproduce the bug 4. **Suggest Fix** - provide a recommended approach to fix the bug 5. **Estimate Effort** - rate as "low" (< 1 day), "medium" (1-3 days), or "high" (> 3 days) 6. **Suggest Severity** - rate as "low" (minor inconvenience), "medium" (affects workflow), "high" (blocks work), or "critical" (data loss/security) ## Response Format Always respond in the following JSON format: \`\`\`json { "summary": "A 2-3 sentence summary of the bug", "rootCauseAnalysis": "Analysis of potential root cause", "reproductionSteps": ["Step 1", "Step 2", "Step 3"], "suggestedFix": "Recommended approach to fix the bug", "estimatedEffort": "low" | "medium" | "high", "suggestedSeverity": "low" | "medium" | "high" | "critical", "reasoning": "Brief explanation for estimates" } \`\`\` ## Guidelines - Be precise and technical in your analysis - Consider edge cases and related issues - Reproduction steps should be specific and actionable - Suggested fix should be practical and implementable - Effort estimation should account for investigation, fix, testing, and deployment - Severity should reflect user impact and business criticality`; // ============================================================================ // Internal Queries // ============================================================================ /** * Get bug report by ID for analysis */ export const getBugReportForAnalysis = internalQuery({ args: { bugReportId: v.id("bugReports"), }, returns: v.union( v.object({ _id: v.id("bugReports"), title: v.string(), description: v.string(), severity: v.string(), reporterName: v.string(), reporterEmail: v.string(), url: v.string(), browserInfo: v.string(), consoleErrors: v.optional(v.string()), }), v.null() ), handler: async (ctx, args) => { const bugReport = await ctx.db.get(args.bugReportId); if (!bugReport) { return null; } return { _id: bugReport._id, title: bugReport.title, description: bugReport.description, severity: bugReport.severity, reporterName: bugReport.reporterName, reporterEmail: bugReport.reporterEmail, url: bugReport.url, browserInfo: bugReport.browserInfo, consoleErrors: bugReport.consoleErrors, }; }, }); // ============================================================================ // Main Processing Action // ============================================================================ /** * Analyze bug report using AI and send notifications. * This is the main entry point triggered after bug report creation. */ export const processBugReport = internalAction({ args: { bugReportId: v.id("bugReports"), // Optional API keys - passed from parent app since components don't inherit env vars openRouterApiKey: v.optional(v.string()), resendApiKey: v.optional(v.string()), resendFromEmail: v.optional(v.string()), }, returns: v.object({ success: v.boolean(), error: v.optional(v.string()), }), handler: async (ctx, args): Promise<{ success: boolean; error?: string }> => { // Check if OpenRouter API key is configured - prefer args (from parent app), fallback to env const apiKey = args.openRouterApiKey || process.env.OPENROUTER_API_KEY; if (!apiKey) { console.log("OPENROUTER_API_KEY not configured, skipping AI analysis"); // Still try to send notifications without AI analysis try { await ctx.runAction(internal.emails.bugReportEmails.sendBugReportNotifications, { reportId: args.bugReportId, analysis: null, resendApiKey: args.resendApiKey, resendFromEmail: args.resendFromEmail, }); } catch { console.log("Email notifications not configured"); } return { success: true }; } // Get the bug report const bugReport = await ctx.runQuery(internal.agents.bugReportAgent.getBugReportForAnalysis, { bugReportId: args.bugReportId, }); if (!bugReport) { return { success: false, error: "Bug report not found" }; } // Create a thread for this analysis const threadId = await createThread(ctx, components.agent, { title: `Bug Report Analysis: ${bugReport.title}`, }); // Create OpenRouter provider dynamically with the API key const openrouter = createOpenRouterProvider(apiKey); // Create agent dynamically to ensure API key is available at runtime const bugReportAgent = new Agent(components.agent, { name: AGENT_NAME, languageModel: openrouter.languageModel("anthropic/claude-sonnet-4"), instructions: BUG_REPORT_AGENT_INSTRUCTIONS, tools: {}, maxSteps: 3, }); // Build the prompt for analysis const consoleErrorsSection = bugReport.consoleErrors ? `**Console Errors:** ${bugReport.consoleErrors}` : ""; const analysisPrompt = `Please analyze the following bug report: **Title:** ${bugReport.title} **Description:** ${bugReport.description} **Reporter Severity:** ${bugReport.severity} **Submitted From:** ${bugReport.url} **Browser Info:** ${bugReport.browserInfo} ${consoleErrorsSection} Provide your analysis in the JSON format specified.`; // Generate analysis using the agent let result: { text: string }; try { result = await bugReportAgent.generateText( ctx, { threadId }, { prompt: analysisPrompt } ); } catch (error) { console.error("AI analysis failed:", error); return { success: false, error: `AI analysis failed: ${error instanceof Error ? error.message : String(error)}` }; } // Parse the AI response let analysis: { summary: string; rootCauseAnalysis: string; reproductionSteps: string[]; suggestedFix: string; estimatedEffort: Effort; suggestedSeverity: BugSeverity; }; try { // Extract JSON from the response (handle markdown code blocks) const jsonMatch = result.text.match(/```json\s*([\s\S]*?)\s*```/); const jsonStr = jsonMatch ? jsonMatch[1] : result.text; const parsed = JSON.parse(jsonStr); analysis = { summary: parsed.summary || "Unable to generate summary", rootCauseAnalysis: parsed.rootCauseAnalysis || "Unable to generate root cause analysis", reproductionSteps: Array.isArray(parsed.reproductionSteps) ? parsed.reproductionSteps : [], suggestedFix: parsed.suggestedFix || "Unable to generate suggested fix", estimatedEffort: ["low", "medium", "high"].includes(parsed.estimatedEffort) ? parsed.estimatedEffort : "medium", suggestedSeverity: ["low", "medium", "high", "critical"].includes(parsed.suggestedSeverity) ? parsed.suggestedSeverity : bugReport.severity as BugSeverity, }; } catch { // If parsing fails, create a basic analysis from the text analysis = { summary: result.text.substring(0, 500), rootCauseAnalysis: "AI analysis parsing failed. Please review manually.", reproductionSteps: ["Review bug report manually", "Contact reporter for clarification"], suggestedFix: "Manual investigation required.", estimatedEffort: "medium", suggestedSeverity: bugReport.severity as BugSeverity, }; } // Update bug report with analysis await ctx.runMutation(internal.bugReports.updateWithAnalysis, { bugReportId: args.bugReportId, aiSummary: analysis.summary, aiRootCauseAnalysis: analysis.rootCauseAnalysis, aiReproductionSteps: analysis.reproductionSteps, aiSuggestedFix: analysis.suggestedFix, aiEstimatedEffort: analysis.estimatedEffort, aiSuggestedSeverity: analysis.suggestedSeverity, aiThreadId: threadId, }); // Send email notifications try { await ctx.runAction(internal.emails.bugReportEmails.sendBugReportNotifications, { reportId: args.bugReportId, analysis: { summary: analysis.summary, rootCauseAnalysis: analysis.rootCauseAnalysis, reproductionSteps: analysis.reproductionSteps, suggestedFix: analysis.suggestedFix, estimatedEffort: analysis.estimatedEffort, }, resendApiKey: args.resendApiKey, resendFromEmail: args.resendFromEmail, }); // Mark notifications as sent await ctx.runMutation(internal.bugReports.markNotificationsSent, { bugReportId: args.bugReportId, }); } catch { console.log("Email notifications not configured or failed"); } return { success: true }; }, }); /** * Public wrapper for processBugReport that can be called from parent app. * Accepts optional API keys since Convex components don't inherit parent env vars. */ export const processBugReportPublic = action({ args: { bugReportId: v.id("bugReports"), openRouterApiKey: v.optional(v.string()), resendApiKey: v.optional(v.string()), resendFromEmail: v.optional(v.string()), }, returns: v.object({ success: v.boolean(), error: v.optional(v.string()), }), handler: async (ctx, args): Promise<{ success: boolean; error?: string }> => { // Delegate to the internal action return await ctx.runAction(internal.agents.bugReportAgent.processBugReport, { bugReportId: args.bugReportId, openRouterApiKey: args.openRouterApiKey, resendApiKey: args.resendApiKey, resendFromEmail: args.resendFromEmail, }); }, });