|
| 1 | +/** |
| 2 | + * Centralised error reporting for the API client layer. |
| 3 | + * |
| 4 | + * - Structured in-memory log (last 100 entries) |
| 5 | + * - Remote reporting via POST /api/errors (best-effort, never throws) |
| 6 | + * - User-friendly message map for contract codes, HTTP statuses, named codes |
| 7 | + */ |
| 8 | + |
| 9 | +export interface ErrorEntry { |
| 10 | + ts: string; |
| 11 | + level: 'error' | 'warn'; |
| 12 | + message: string; |
| 13 | + code?: string | number; |
| 14 | + status?: number; |
| 15 | + context?: Record<string, unknown>; |
| 16 | +} |
| 17 | + |
| 18 | +// ── In-memory log ───────────────────────────────────────────────────────────── |
| 19 | + |
| 20 | +const _log: ErrorEntry[] = []; |
| 21 | +const MAX_LOG = 100; |
| 22 | + |
| 23 | +export function logError( |
| 24 | + message: string, |
| 25 | + opts: Omit<ErrorEntry, 'ts' | 'level' | 'message'> = {} |
| 26 | +): ErrorEntry { |
| 27 | + const entry: ErrorEntry = { ts: new Date().toISOString(), level: 'error', message, ...opts }; |
| 28 | + _log.unshift(entry); |
| 29 | + if (_log.length > MAX_LOG) _log.pop(); |
| 30 | + console.error('[api-error]', message, opts); |
| 31 | + return entry; |
| 32 | +} |
| 33 | + |
| 34 | +export function logWarn( |
| 35 | + message: string, |
| 36 | + opts: Omit<ErrorEntry, 'ts' | 'level' | 'message'> = {} |
| 37 | +): ErrorEntry { |
| 38 | + const entry: ErrorEntry = { ts: new Date().toISOString(), level: 'warn', message, ...opts }; |
| 39 | + _log.unshift(entry); |
| 40 | + if (_log.length > MAX_LOG) _log.pop(); |
| 41 | + console.warn('[api-warn]', message, opts); |
| 42 | + return entry; |
| 43 | +} |
| 44 | + |
| 45 | +export function getErrorLog(): ErrorEntry[] { |
| 46 | + return [..._log]; |
| 47 | +} |
| 48 | + |
| 49 | +// ── Remote reporting ────────────────────────────────────────────────────────── |
| 50 | + |
| 51 | +const REPORT_ENDPOINT = '/api/errors'; |
| 52 | + |
| 53 | +export async function reportError( |
| 54 | + error: unknown, |
| 55 | + context: Record<string, unknown> = {} |
| 56 | +): Promise<void> { |
| 57 | + const message = error instanceof Error ? error.message : String(error); |
| 58 | + const stack = error instanceof Error ? error.stack : undefined; |
| 59 | + const entry = logError(message, { context: { stack, ...context } }); |
| 60 | + try { |
| 61 | + await fetch(REPORT_ENDPOINT, { |
| 62 | + method: 'POST', |
| 63 | + headers: { 'Content-Type': 'application/json' }, |
| 64 | + body: JSON.stringify(entry), |
| 65 | + keepalive: true, // survives page unload |
| 66 | + }); |
| 67 | + } catch { /* best-effort — never throw */ } |
| 68 | +} |
| 69 | + |
| 70 | +// ── User-friendly message map ───────────────────────────────────────────────── |
| 71 | + |
| 72 | +const MESSAGES: Record<string | number, string> = { |
| 73 | + // Contract error codes |
| 74 | + 1: 'This contract has already been set up.', |
| 75 | + 2: 'Contract is not yet initialized.', |
| 76 | + 3: 'Amount must be greater than zero.', |
| 77 | + 4: 'Fee must be between 0 and 100%.', |
| 78 | + 5: 'This arbitrator is not registered.', |
| 79 | + 6: 'Trade not found. Check the trade ID.', |
| 80 | + 7: 'This action is not allowed in the current trade state.', |
| 81 | + 8: 'A calculation error occurred. Please try again.', |
| 82 | + 9: 'No fees available to withdraw.', |
| 83 | + 10: 'You are not authorized to perform this action.', |
| 84 | + 11: 'The contract is currently paused.', |
| 85 | + // HTTP status codes |
| 86 | + 400: 'Invalid request. Please check your input.', |
| 87 | + 401: 'Session expired. Please reconnect your wallet.', |
| 88 | + 403: 'You do not have permission to do that.', |
| 89 | + 404: 'The requested resource was not found.', |
| 90 | + 408: 'Request timed out. Please try again.', |
| 91 | + 429: 'Too many requests. Please wait a moment.', |
| 92 | + 500: 'Server error. Our team has been notified.', |
| 93 | + 503: 'Service unavailable. You may be offline.', |
| 94 | + // Named API codes |
| 95 | + DATABASE_ERROR: 'A database error occurred. Please try again shortly.', |
| 96 | + STELLAR_ERROR: 'Could not reach the Stellar network. Check your connection.', |
| 97 | + NETWORK_ERROR: 'Network error. Please check your internet connection.', |
| 98 | + INVALID_FORMAT: 'Unexpected data format received.', |
| 99 | + RATE_LIMITED: 'Too many requests. Please slow down.', |
| 100 | +}; |
| 101 | + |
| 102 | +export function friendlyMessage(error: unknown): string { |
| 103 | + if (!error) return 'An unexpected error occurred. Please try again.'; |
| 104 | + const e = error as any; |
| 105 | + if (e.code !== undefined) return MESSAGES[e.code] ?? e.message ?? 'An unexpected error occurred.'; |
| 106 | + if (e.status !== undefined) return MESSAGES[e.status] ?? 'An unexpected error occurred.'; |
| 107 | + if (error instanceof TypeError && String(error.message).includes('fetch')) return MESSAGES[503]; |
| 108 | + return (error instanceof Error ? error.message : String(error)) || 'An unexpected error occurred.'; |
| 109 | +} |
0 commit comments