/** * Bug Report Email Notifications * * Sends email notifications using Resend when bug reports are analyzed. * - Reporter receives a thank-you email with summary * - Team members receive full analysis report */ import { v } from "convex/values"; import { internalAction, internalQuery } from "../_generated/server"; import { internal } from "../_generated/api"; import { Resend } from "resend"; import type { BugSeverity } from "../schema"; // Severity labels and colors const severityConfig: Record = { low: { label: "Low", color: "#22c55e" }, medium: { label: "Medium", color: "#f59e0b" }, high: { label: "High", color: "#ea580c" }, critical: { label: "Critical", color: "#ef4444" }, }; // Effort labels const effortLabels: Record = { low: "Low (< 1 week)", medium: "Medium (1-4 weeks)", high: "High (> 4 weeks)", }; // ============================================================================ // Internal Queries // ============================================================================ /** * Get full bug report data for email */ export const getBugReportForEmail = internalQuery({ args: { reportId: v.id("bugReports"), }, returns: v.union( v.object({ _id: v.id("bugReports"), title: v.string(), description: v.string(), severity: v.string(), status: v.string(), reporterName: v.string(), reporterEmail: v.string(), url: v.string(), consoleErrors: v.optional(v.string()), createdAt: v.number(), }), v.null() ), handler: async (ctx, args) => { const report = await ctx.db.get(args.reportId); if (!report) { return null; } return { _id: report._id, title: report.title, description: report.description, severity: report.severity, status: report.status, reporterName: report.reporterName, reporterEmail: report.reporterEmail, url: report.url, consoleErrors: report.consoleErrors, createdAt: report.createdAt, }; }, }); // ============================================================================ // Email Templates // ============================================================================ function generateReporterEmailHtml(report: { title: string; summary: string; reporterName: string; }): string { return ` Thank You for Your Bug Report

Thank You for Your Bug Report!

Hi ${report.reporterName},

Thank you for reporting this issue. We've received your bug report and our team is already investigating.

${report.title}

${report.summary}

We take all bug reports seriously and will work to resolve this issue as quickly as possible. You may receive updates as we make progress.

Best regards,
The Support Team

This is an automated message. Please do not reply directly to this email.

`; } function generateTeamEmailHtml( report: { title: string; description: string; severity: string; reporterName: string; reporterEmail: string; url: string; consoleErrors?: string; createdAt: number; }, analysis: { summary: string; rootCauseAnalysis: string; reproductionSteps: string[]; suggestedFix: string; estimatedEffort: string; } ): string { const severityInfo = severityConfig[report.severity] || severityConfig.medium; const effortLabel = effortLabels[analysis.estimatedEffort] || analysis.estimatedEffort; const submittedDate = new Date(report.createdAt).toLocaleString(); const reproductionStepsHtml = analysis.reproductionSteps .map((step) => `
  • ${step}
  • `) .join(""); let consoleErrorsHtml = ""; if (report.consoleErrors) { try { const errors = JSON.parse(report.consoleErrors); if (Array.isArray(errors) && errors.length > 0) { const errorItems = errors .slice(0, 10) .map( (err: { message?: string } | string) => `
  • ${typeof err === 'string' ? err : err.message || JSON.stringify(err)}
  • ` ) .join(""); consoleErrorsHtml = `

    🔴 Console Errors

      ${errorItems}
    `; } } catch { // Invalid JSON, skip console errors section } } return ` New Bug Report Analysis

    New Bug Report Received

    AI-Analyzed Bug Report

    ${report.title}

    ${severityInfo.label} Severity ${effortLabel}

    📋 AI Summary

    ${analysis.summary}

    🔍 Root Cause Analysis

    ${analysis.rootCauseAnalysis}

    🔄 Reproduction Steps

      ${reproductionStepsHtml}

    💡 Suggested Fix

    ${analysis.suggestedFix}

    ${consoleErrorsHtml}

    📝 Original Bug Description

    ${report.description}

    Reporter: ${report.reporterName}
    Email: ${report.reporterEmail}
    Submitted: ${submittedDate}
    Page URL: ${report.url}

    This analysis was generated by AI. Please review and verify before taking action.

    `; } function generateSimpleTeamEmailHtml(report: { title: string; description: string; severity: string; reporterName: string; reporterEmail: string; url: string; createdAt: number; }): string { const severityInfo = severityConfig[report.severity] || severityConfig.medium; const submittedDate = new Date(report.createdAt).toLocaleString(); return ` New Bug Report

    New Bug Report Received

    ${report.title}

    ${severityInfo.label} Severity

    Description

    ${report.description}

    Reporter: ${report.reporterName} (${report.reporterEmail})
    Submitted: ${submittedDate}
    Page URL: ${report.url}
    `; } // ============================================================================ // Email Sending Action // ============================================================================ /** * Send bug report notifications to reporter and team. */ export const sendBugReportNotifications = internalAction({ args: { reportId: v.id("bugReports"), analysis: v.union( v.object({ summary: v.string(), rootCauseAnalysis: v.string(), reproductionSteps: v.array(v.string()), suggestedFix: v.string(), estimatedEffort: v.string(), }), v.null() ), // Optional API keys - passed from parent app since components don't inherit env vars resendApiKey: v.optional(v.string()), resendFromEmail: v.optional(v.string()), }, returns: v.object({ success: v.boolean(), reporterEmailSent: v.boolean(), teamEmailsSent: v.number(), error: v.optional(v.string()), }), handler: async ( ctx, args ): Promise<{ success: boolean; reporterEmailSent: boolean; teamEmailsSent: number; error?: string; }> => { // Check for Resend API key - prefer args (from parent app), fallback to env const resendApiKey = args.resendApiKey || process.env.RESEND_API_KEY; if (!resendApiKey) { console.warn("RESEND_API_KEY not configured. Skipping email notifications."); return { success: false, reporterEmailSent: false, teamEmailsSent: 0, error: "RESEND_API_KEY not configured", }; } const resend = new Resend(resendApiKey); const fromEmail = args.resendFromEmail || process.env.RESEND_FROM_EMAIL || "bugs@resend.dev"; // Get bug report data const report = await ctx.runQuery( internal.emails.bugReportEmails.getBugReportForEmail, { reportId: args.reportId, } ); if (!report) { return { success: false, reporterEmailSent: false, teamEmailsSent: 0, error: "Bug report not found", }; } let reporterEmailSent = false; let teamEmailsSent = 0; // Send email to reporter try { const summary = args.analysis?.summary || "We are investigating your report."; const reporterHtml = generateReporterEmailHtml({ title: report.title, summary, reporterName: report.reporterName, }); await resend.emails.send({ from: fromEmail, to: [report.reporterEmail], subject: `Thank you for your bug report: ${report.title}`, html: reporterHtml, }); reporterEmailSent = true; console.log(`Reporter email sent to ${report.reporterEmail}`); } catch (error) { console.error("Failed to send reporter email:", error); } // Get teams that handle this bug report severity const teams = await ctx.runQuery(internal.supportTeams.getTeamsForBugReportSeverity, { severity: report.severity as BugSeverity, }); // Generate team email HTML const teamHtml = args.analysis ? generateTeamEmailHtml( { title: report.title, description: report.description, severity: report.severity, reporterName: report.reporterName, reporterEmail: report.reporterEmail, url: report.url, consoleErrors: report.consoleErrors, createdAt: report.createdAt, }, args.analysis ) : generateSimpleTeamEmailHtml({ title: report.title, description: report.description, severity: report.severity, reporterName: report.reporterName, reporterEmail: report.reporterEmail, url: report.url, createdAt: report.createdAt, }); // Send email to all team members from all matching teams for (const team of teams) { if (team.isActive && team.memberEmails.length > 0) { for (const email of team.memberEmails) { try { await resend.emails.send({ from: fromEmail, to: [email], subject: `[Bug Report - ${severityConfig[report.severity]?.label || report.severity}] ${report.title}${args.analysis ? " - AI Analysis" : ""}`, html: teamHtml, }); teamEmailsSent++; console.log(`Team email sent to ${email}`); } catch (error) { console.error(`Failed to send team email to ${email}:`, error); } } } } return { success: true, reporterEmailSent, teamEmailsSent, }; }, });