/** * Support Issue Reporting Utility * Formats error information for reporting to support/GitHub issues */ import type { ApiError } from '@/schemas/apiSchemas'; import { parseApiError } from './apiErrorHandler'; import { getErrorCategory } from './apiErrorHandler'; export interface IssueReport { title: string; body: string; metadata: { requestId?: string; errorCode?: string | number; statusCode?: number; category: string; timestamp: string; userAgent: string; url: string; }; } /** * Formats an error into a structured issue report * @param error - The error to report (ApiError, Error, or unknown) * @param context - Additional context about where/why the error occurred * @returns Formatted issue report */ export function formatIssueReport( error: Error | ApiError | unknown, context?: { component?: string; action?: string; userId?: string; additionalInfo?: Record; }, ): IssueReport { const apiError = parseApiError(error); const category = getErrorCategory(apiError); // Get browser/environment info const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : 'Unknown'; const url = typeof window !== 'undefined' ? window.location.href : 'Unknown'; const timestamp = new Date().toISOString(); // Build title const title = `[${category.toUpperCase()}] ${apiError.message || 'Unknown error'}`; // Build body with structured information const sections: string[] = []; // Error details section sections.push('## 🐞 Error Details'); sections.push(''); sections.push(`**Message:** ${apiError.message || 'No message provided'}`); if (apiError.code !== undefined) { sections.push(`**Error Code:** ${apiError.code}`); } if (apiError.code !== undefined) { sections.push(`**HTTP Status:** ${apiError.code}`); } if (apiError.request_id) { sections.push(`**Request ID:** \`${apiError.request_id}\``); } sections.push(`**Category:** ${category}`); sections.push(`**Timestamp:** ${timestamp}`); sections.push(''); // Context section if (context) { sections.push('## 📍 Context'); sections.push(''); if (context.component) { sections.push(`**Component:** ${context.component}`); } if (context.action) { sections.push(`**Action:** ${context.action}`); } if (context.userId) { sections.push(`**User ID:** ${context.userId}`); } if ( context.additionalInfo && Object.keys(context.additionalInfo).length > 0 ) { sections.push('**Additional Info:**'); sections.push('```json'); sections.push(JSON.stringify(context.additionalInfo, null, 2)); sections.push('```'); } sections.push(''); } // Error details section if (apiError.details) { sections.push('## 🔍 Error Details'); sections.push(''); sections.push('```json'); sections.push(JSON.stringify(apiError.details, null, 2)); sections.push('```'); sections.push(''); } // Validation errors section if (apiError.details && apiError.details.length > 0) { sections.push('## ⚠️ Validation Errors'); sections.push(''); apiError.details.forEach((detail) => { sections.push( `- **${detail.field}:** ${detail.message}${detail.value ? ` (value: ${detail.value})` : ''}`, ); }); sections.push(''); } // Environment section sections.push('## 💻 Environment'); sections.push(''); sections.push(`**URL:** ${url}`); sections.push(`**User Agent:** ${userAgent}`); sections.push(`**Browser:** ${getBrowserName(userAgent)}`); sections.push(`**Platform:** ${getPlatformName(userAgent)}`); if (typeof window !== 'undefined') { sections.push(`**Screen:** ${window.screen.width}x${window.screen.height}`); sections.push(`**Viewport:** ${window.innerWidth}x${window.innerHeight}`); } sections.push(''); // Steps to reproduce (placeholder) sections.push('## 🔁 Steps to Reproduce'); sections.push(''); sections.push('1. [Describe step 1]'); sections.push('2. [Describe step 2]'); sections.push('3. [Describe step 3]'); sections.push(''); // Expected behavior sections.push('## ✅ Expected Behavior'); sections.push(''); sections.push('[Describe what should have happened]'); sections.push(''); const body = sections.join('\n'); return { title, body, metadata: { requestId: apiError.request_id, errorCode: apiError.code, statusCode: apiError.code, category, timestamp, userAgent, url, }, }; } /** * Copies issue report to clipboard * @param report - The issue report to copy * @returns Promise that resolves when copy is complete */ export async function copyIssueReportToClipboard( report: IssueReport, ): Promise { const text = `${report.title}\n\n${report.body}`; if ( typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText ) { try { await navigator.clipboard.writeText(text); return; } catch (err) { // Fallback to legacy method } } // Fallback: create temporary textarea const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); } finally { document.body.removeChild(textarea); } } /** * Opens GitHub issue creation page with pre-filled issue report * @param report - The issue report to use * @param repository - GitHub repository (default: uses current repo if available) */ export function openGitHubIssue( report: IssueReport, repository?: string, ): void { // Try to detect repository from package.json or env const repo = repository || getRepositoryUrl(); if (!repo) { // Fallback: just copy to clipboard copyIssueReportToClipboard(report); return; } // GitHub issue URL format: https://github.com/owner/repo/issues/new?title=...&body=... const params = new URLSearchParams({ title: report.title, body: report.body, }); const url = `${repo}/issues/new?${params.toString()}`; window.open(url, '_blank'); } /** * Gets repository URL from package.json or environment */ function getRepositoryUrl(): string | null { // Try to get from environment variable if (import.meta.env.VITE_GITHUB_REPO) { return `https://github.com/${import.meta.env.VITE_GITHUB_REPO}`; } // Try to parse from package.json repository field (would need to be passed or read) // For now, return null and fallback to clipboard return null; } /** * Extracts browser name from user agent */ function getBrowserName(userAgent: string): string { if (userAgent.includes('Chrome') && !userAgent.includes('Edg')) return 'Chrome'; if (userAgent.includes('Firefox')) return 'Firefox'; if (userAgent.includes('Safari') && !userAgent.includes('Chrome')) return 'Safari'; if (userAgent.includes('Edg')) return 'Edge'; if (userAgent.includes('Opera') || userAgent.includes('OPR')) return 'Opera'; return 'Unknown'; } /** * Extracts platform name from user agent */ function getPlatformName(userAgent: string): string { if (userAgent.includes('Windows')) return 'Windows'; if (userAgent.includes('Mac')) return 'macOS'; if (userAgent.includes('Linux')) return 'Linux'; if (userAgent.includes('Android')) return 'Android'; if ( userAgent.includes('iOS') || userAgent.includes('iPhone') || userAgent.includes('iPad') ) return 'iOS'; return 'Unknown'; }