2026-01-11 16:27:45 +00:00
|
|
|
/**
|
|
|
|
|
* Support Issue Reporting Utility
|
|
|
|
|
* Formats error information for reporting to support/GitHub issues
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-15 16:03:35 +00:00
|
|
|
import type { ApiError } from '@/schemas/apiSchemas';
|
2026-01-11 16:27:45 +00:00
|
|
|
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<string, any>;
|
2026-01-13 18:47:57 +00:00
|
|
|
},
|
2026-01-11 16:27:45 +00:00
|
|
|
): IssueReport {
|
|
|
|
|
const apiError = parseApiError(error);
|
|
|
|
|
const category = getErrorCategory(apiError);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
// Get browser/environment info
|
2026-01-13 18:47:57 +00:00
|
|
|
const userAgent =
|
|
|
|
|
typeof navigator !== 'undefined' ? navigator.userAgent : 'Unknown';
|
2026-01-11 16:27:45 +00:00
|
|
|
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'}`);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
if (apiError.code !== undefined) {
|
|
|
|
|
sections.push(`**Error Code:** ${apiError.code}`);
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
if (apiError.status !== undefined) {
|
|
|
|
|
sections.push(`**HTTP Status:** ${apiError.status}`);
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
if (apiError.request_id) {
|
|
|
|
|
sections.push(`**Request ID:** \`${apiError.request_id}\``);
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
sections.push(`**Category:** ${category}`);
|
|
|
|
|
sections.push(`**Timestamp:** ${timestamp}`);
|
|
|
|
|
sections.push('');
|
|
|
|
|
|
|
|
|
|
// Context section
|
|
|
|
|
if (context) {
|
|
|
|
|
sections.push('## 📍 Context');
|
|
|
|
|
sections.push('');
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
if (context.component) {
|
|
|
|
|
sections.push(`**Component:** ${context.component}`);
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
if (context.action) {
|
|
|
|
|
sections.push(`**Action:** ${context.action}`);
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
if (context.userId) {
|
|
|
|
|
sections.push(`**User ID:** ${context.userId}`);
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
context.additionalInfo &&
|
|
|
|
|
Object.keys(context.additionalInfo).length > 0
|
|
|
|
|
) {
|
2026-01-11 16:27:45 +00:00
|
|
|
sections.push('**Additional Info:**');
|
|
|
|
|
sections.push('```json');
|
|
|
|
|
sections.push(JSON.stringify(context.additionalInfo, null, 2));
|
|
|
|
|
sections.push('```');
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
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.errors && Object.keys(apiError.errors).length > 0) {
|
|
|
|
|
sections.push('## ⚠️ Validation Errors');
|
|
|
|
|
sections.push('');
|
|
|
|
|
Object.entries(apiError.errors).forEach(([field, messages]) => {
|
2026-01-13 18:47:57 +00:00
|
|
|
sections.push(
|
|
|
|
|
`- **${field}:** ${Array.isArray(messages) ? messages.join(', ') : messages}`,
|
|
|
|
|
);
|
2026-01-11 16:27:45 +00:00
|
|
|
});
|
|
|
|
|
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)}`);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
sections.push(`**Screen:** ${window.screen.width}x${window.screen.height}`);
|
|
|
|
|
sections.push(`**Viewport:** ${window.innerWidth}x${window.innerHeight}`);
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
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.status,
|
|
|
|
|
category,
|
|
|
|
|
timestamp,
|
|
|
|
|
userAgent,
|
|
|
|
|
url,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Copies issue report to clipboard
|
|
|
|
|
* @param report - The issue report to copy
|
|
|
|
|
* @returns Promise that resolves when copy is complete
|
|
|
|
|
*/
|
2026-01-13 18:47:57 +00:00
|
|
|
export async function copyIssueReportToClipboard(
|
|
|
|
|
report: IssueReport,
|
|
|
|
|
): Promise<void> {
|
2026-01-11 16:27:45 +00:00
|
|
|
const text = `${report.title}\n\n${report.body}`;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
typeof navigator !== 'undefined' &&
|
|
|
|
|
navigator.clipboard &&
|
|
|
|
|
navigator.clipboard.writeText
|
|
|
|
|
) {
|
2026-01-11 16:27:45 +00:00
|
|
|
try {
|
|
|
|
|
await navigator.clipboard.writeText(text);
|
|
|
|
|
return;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Fallback to legacy method
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
// 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();
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
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)
|
|
|
|
|
*/
|
2026-01-13 18:47:57 +00:00
|
|
|
export function openGitHubIssue(
|
|
|
|
|
report: IssueReport,
|
|
|
|
|
repository?: string,
|
|
|
|
|
): void {
|
2026-01-11 16:27:45 +00:00
|
|
|
// Try to detect repository from package.json or env
|
|
|
|
|
const repo = repository || getRepositoryUrl();
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:27:45 +00:00
|
|
|
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 {
|
2026-01-13 18:47:57 +00:00
|
|
|
if (userAgent.includes('Chrome') && !userAgent.includes('Edg'))
|
|
|
|
|
return 'Chrome';
|
2026-01-11 16:27:45 +00:00
|
|
|
if (userAgent.includes('Firefox')) return 'Firefox';
|
2026-01-13 18:47:57 +00:00
|
|
|
if (userAgent.includes('Safari') && !userAgent.includes('Chrome'))
|
|
|
|
|
return 'Safari';
|
2026-01-11 16:27:45 +00:00
|
|
|
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';
|
2026-01-13 18:47:57 +00:00
|
|
|
if (
|
|
|
|
|
userAgent.includes('iOS') ||
|
|
|
|
|
userAgent.includes('iPhone') ||
|
|
|
|
|
userAgent.includes('iPad')
|
|
|
|
|
)
|
|
|
|
|
return 'iOS';
|
2026-01-11 16:27:45 +00:00
|
|
|
return 'Unknown';
|
|
|
|
|
}
|