// Copy utilities for log management import { StructuredLog } from '@/components/JobDevTools'; /** * Format a single log entry for copying as plain text * Format: [TIMESTAMP] [LEVEL] [CATEGORY] MESSAGE * Includes metrics/network data if present */ export function formatLogForCopy(log: StructuredLog): string { const timestamp = formatTimestamp(log.timestamp); let line = `[${timestamp}] [${log.level}] [${log.category}] ${log.message}`; // Include metrics if present if (log.metrics && Object.keys(log.metrics).length > 0) { line += `\n metrics: ${JSON.stringify(log.metrics)}`; } // Include network data if present if (log.network && Object.keys(log.network).length > 0) { line += `\n network: ${JSON.stringify(log.network)}`; } return line; } /** * Format multiple logs for copying as plain text * Joins logs with newlines and includes a header with count */ export function formatLogsForCopy(logs: StructuredLog[]): string { if (logs.length === 0) { return 'No logs to copy.'; } const header = `=== ${logs.length} Log${logs.length !== 1 ? 's' : ''} ===`; const separator = '='.repeat(header.length); const formattedLogs = logs.map(formatLogForCopy).join('\n'); return `${header}\n${separator}\n${formattedLogs}\n${separator}`; } /** * Copy text to clipboard with fallback for older browsers * Returns true if copy was successful, false otherwise */ export async function copyToClipboard(text: string): Promise { // Try modern Clipboard API first if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { try { await navigator.clipboard.writeText(text); return true; } catch (err) { console.warn('Clipboard API failed, trying fallback:', err); } } // Fallback to execCommand for older browsers try { const textArea = document.createElement('textarea'); textArea.value = text; // Make the textarea invisible textArea.style.position = 'fixed'; textArea.style.left = '-9999px'; textArea.style.top = '-9999px'; textArea.style.opacity = '0'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); const success = document.execCommand('copy'); document.body.removeChild(textArea); return success; } catch (err) { console.error('Fallback copy failed:', err); return false; } } /** * Format logs as pretty-printed JSON for export */ export function formatLogsAsJson(logs: StructuredLog[]): string { const exportData = { exportedAt: new Date().toISOString(), count: logs.length, logs: logs.map((log) => ({ timestamp: log.timestamp, timestamp_ms: log.timestamp_ms, level: log.level, category: log.category, message: log.message, ...(log.metrics && { metrics: log.metrics }), ...(log.network && { network: log.network }), })), }; return JSON.stringify(exportData, null, 2); } /** * Format logs as CSV for export * Includes headers and properly escapes values */ export function formatLogsAsCsv(logs: StructuredLog[]): string { if (logs.length === 0) { return 'timestamp,timestamp_ms,level,category,message,metrics,network'; } const headers = ['timestamp', 'timestamp_ms', 'level', 'category', 'message', 'metrics', 'network']; const headerRow = headers.join(','); const rows = logs.map((log) => { const values = [ escapeCsvValue(log.timestamp), log.timestamp_ms.toString(), log.level, log.category, escapeCsvValue(log.message), escapeCsvValue(log.metrics ? JSON.stringify(log.metrics) : ''), escapeCsvValue(log.network ? JSON.stringify(log.network) : ''), ]; return values.join(','); }); return [headerRow, ...rows].join('\n'); } /** * Download content as a file */ export function downloadAsFile(content: string, filename: string, mimeType: string): void { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.style.display = 'none'; document.body.appendChild(link); link.click(); // Clean up document.body.removeChild(link); URL.revokeObjectURL(url); } /** * Get file extension and MIME type for a format */ export function getExportFileInfo(format: ExportFormat): { extension: string; mimeType: string } { switch (format) { case 'json': return { extension: 'json', mimeType: 'application/json' }; case 'csv': return { extension: 'csv', mimeType: 'text/csv' }; case 'text': default: return { extension: 'txt', mimeType: 'text/plain' }; } } /** * Format logs based on export format */ export function formatLogsForExport(logs: StructuredLog[], format: ExportFormat): string { switch (format) { case 'json': return formatLogsAsJson(logs); case 'csv': return formatLogsAsCsv(logs); case 'text': default: return formatLogsForCopy(logs); } } // Helper functions /** * Format timestamp for display */ function formatTimestamp(timestamp: string): string { try { const date = new Date(timestamp); return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3, }); } catch { return timestamp; } } /** * Escape a value for CSV (handles quotes and commas) */ function escapeCsvValue(value: string): string { if (!value) return ''; // If value contains comma, newline, or quote, wrap in quotes and escape internal quotes if (value.includes(',') || value.includes('\n') || value.includes('"')) { return `"${value.replace(/"/g, '""')}"`; } return value; } // Types export type ExportFormat = 'text' | 'json' | 'csv';