Skip to Content
UNIMAST
DocumentaciónDesarrollarErrores y Logging

Errores y Logging

UNIMAST ERP implementa un sistema robusto de manejo de errores y logging que proporciona visibilidad completa sobre el estado de la aplicación, facilita el debugging y mejora la experiencia del usuario a través de notificaciones claras y accionables.

🚨 Sistema de Manejo de Errores

Arquitectura de Errores

// Estructura jerárquica de manejo de errores try { // Operación principal const result = await performOperation(); return result; } catch (error) { // 1. Logging del error con contexto console.error('[MODULE] Operation failed:', { operation: 'performOperation', error: error.message, stack: error.stack, context: { userId, operationId } }); // 2. Transformación a error del usuario const userError = transformToUserError(error); // 3. Notificación al usuario toast.error(userError.message); // 4. Re-lanzar o manejar según el contexto throw userError; }

Tipos de Errores

// app/lib/constants.ts export const ERRORS = { // Errores de validación CUSTOMER_NAME_REQUIRED: 'El nombre del cliente es requerido', NCF_REQUIRED: 'El NCF es requerido', ITEMS_REQUIRED: 'Debe agregar al menos un ítem', DESCRIPTION_REQUIRED: 'La descripción es requerida', QUANTITY_MIN: 'La cantidad debe ser al menos 1', PRICE_MIN: 'El precio debe ser mayor a 0', // Errores de permisos PERMISSION_SELECTION_REQUIRED: 'Selecciona al menos 1 permiso', // Errores de autenticación NO_PERMISSION: 'No tienes permiso para realizar esta acción.', // Errores de configuración ECF_CERTIFICATE_NOT_FOUND: 'No se encontró el certificado ECF', ENCRYPTION_KEY_MISSING: 'ENCRYPTION_KEY is not set in environment variables' } as const; // Errores específicos del módulo eCF export const ECF_ERRORS = { NO_CERTIFICATE: 'No ECF certificate found', NO_SEQUENCES: 'No existen secuencias disponibles para el tipo de documento', INVALID_ECF_FORMAT: 'Invalid ECF format', SIGNATURE_FAILED: 'Unable to get the first 6 digits of the SignatureValue', DOCUMENT_NOT_FOUND: 'Document not found', NO_RESPONSE: 'No Response', NO_TRACK_ID: 'No trackId found', NO_STATUS: 'No status found', TRY_LATER: 'Try Later' } as const;

Clases de Error Personalizadas

// app/lib/errors/ecf-error.ts export class ECFError extends Error { constructor( message: string, public code: 'CERTIFICATE_NOT_FOUND' | 'NO_SEQUENCES' | 'AUTHENTICATION_FAILED' | 'VALIDATION_FAILED', public details?: Record<string, any>, public isUserFacing: boolean = true ) { super(message); this.name = 'ECFError'; // Mantener stack trace en desarrollo if (process.env.NODE_ENV === 'development') { Error.captureStackTrace(this, ECFError); } } } export class ValidationError extends Error { constructor( message: string, public field: string, public value: any ) { super(message); this.name = 'ValidationError'; } } export class PermissionError extends Error { constructor( message: string = 'No tienes permiso para realizar esta acción.', public requiredScopes?: string[] ) { super(message); this.name = 'PermissionError'; } } // Uso en el código if (!cert) { throw new ECFError( 'Certificado eCF no encontrado', 'CERTIFICATE_NOT_FOUND', { path: settings.ecfCertificatePath }, true ); } if (!session?.user) { throw new PermissionError('Debe iniciar sesión para continuar'); }

📝 Sistema de Logging

Estrategia de Logging por Nivel

// app/lib/logger/index.ts export enum LogLevel { ERROR = 'error', WARN = 'warn', INFO = 'info', DEBUG = 'debug' } export interface LogContext { module: string; operation: string; userId?: string; sessionId?: string; requestId?: string; metadata?: Record<string, any>; } export class Logger { private static instance: Logger; private logLevel: LogLevel; private constructor() { this.logLevel = (process.env.LOG_LEVEL as LogLevel) || LogLevel.INFO; } static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; } error(message: string, context: LogContext, error?: Error) { if (this.shouldLog(LogLevel.ERROR)) { console.error(`[${context.module}] ${message}`, { timestamp: new Date().toISOString(), level: LogLevel.ERROR, operation: context.operation, userId: context.userId, sessionId: context.sessionId, requestId: context.requestId, metadata: context.metadata, error: error ? { message: error.message, stack: error.stack, name: error.name } : undefined }); } } warn(message: string, context: LogContext, metadata?: Record<string, any>) { if (this.shouldLog(LogLevel.WARN)) { console.warn(`[${context.module}] ${message}`, { timestamp: new Date().toISOString(), level: LogLevel.WARN, operation: context.operation, userId: context.userId, metadata }); } } info(message: string, context: LogContext, metadata?: Record<string, any>) { if (this.shouldLog(LogLevel.INFO)) { console.info(`[${context.module}] ${message}`, { timestamp: new Date().toISOString(), level: LogLevel.INFO, operation: context.operation, userId: context.userId, metadata }); } } debug(message: string, context: LogContext, metadata?: Record<string, any>) { if (this.shouldLog(LogLevel.DEBUG)) { console.debug(`[${context.module}] ${message}`, { timestamp: new Date().toISOString(), level: LogLevel.DEBUG, operation: context.operation, userId: context.userId, metadata }); } } private shouldLog(level: LogLevel): boolean { const levels = [LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO, LogLevel.DEBUG]; return levels.indexOf(level) <= levels.indexOf(this.logLevel); } } export const logger = Logger.getInstance();

Logging en Módulos Específicos

Logging en eCF

// app/lib/ecf/sender/index.ts export async function processDocumentSubmission(inputData: ElectronicInvoice) { const startTime = Date.now(); const logContext = { module: 'ECF', operation: 'processDocumentSubmission', userId: inputData.userId, metadata: { encf: inputData.encf, type: inputData.type, amount: inputData.totals?.totalAmount } }; logger.info('Starting document submission', logContext); try { const cert = await getCertificate(); if (!cert || !cert.key || !cert.cert) { throw new ECFError('No ECF certificate found', 'CERTIFICATE_NOT_FOUND'); } const ecf = new ECF(cert, ECF_ENV); await ecf.authenticate(); // Procesar documento... const result = await sendToDGII(inputData); const duration = Date.now() - startTime; logger.info('Document processed successfully', { ...logContext, metadata: { ...logContext.metadata, duration, trackId: result.trackId } }); return result; } catch (error) { const duration = Date.now() - startTime; logger.error('Document processing failed', { ...logContext, metadata: { ...logContext.metadata, duration } }, error); throw error; } }

Logging en Workers de Cola

// app/lib/queue.ts export function mountWorkers() { const connection = new IORedis(process.env.REDIS_URL || '', { maxRetriesPerRequest: null }); // Worker para envío de documentos const sendTaxDocumentWorker = new Worker( sendTaxDocumentQueueName, async (job) => { const { encf, customerDirectory } = job.data; const logContext = { module: 'QUEUE', operation: 'sendTaxDocument', metadata: { encf, jobId: job.id } }; logger.info('Starting tax document send job', logContext); try { const result = await sendTaxDocument(encf, customerDirectory); logger.info('Tax document send job completed', { ...logContext, metadata: { ...logContext.metadata, result } }); return result; } catch (error) { logger.error('Tax document send job failed', logContext, error); throw error; } }, { connection } ); // Event handlers con logging sendTaxDocumentWorker.on('completed', (job) => { logger.info(`Send Tax Document Job ${job.id} completed`, { module: 'QUEUE', operation: 'jobCompleted', metadata: { jobId: job.id, queue: sendTaxDocumentQueueName } }); }); sendTaxDocumentWorker.on('failed', (job) => { logger.error(`Send Tax Document Job ${job.id} failed`, { module: 'QUEUE', operation: 'jobFailed', metadata: { jobId: job.id, queue: sendTaxDocumentQueueName, reason: job.failedReason, attempts: job.attemptsMade } }); }); }

Logging en Webhooks

// app/lib/webhooks.ts export async function dispatchWebhookEvent( event: string, payload: any, webhook: Webhook ) { const logContext = { module: 'WEBHOOK', operation: 'dispatchWebhookEvent', metadata: { event, webhookId: webhook.id, webhookUrl: webhook.url, payloadId: payload.id } }; logger.info('Dispatching webhook event', logContext); try { const response = await fetch(webhook.url, { method: 'POST', headers: { ['X-Unimast-Webhook-Id']: webhook.id, ['X-Unimast-Event-Type']: event, ['X-Unimast-Event-Id']: payload.id, ['X-Unimast-Event-Timestamp']: new Date().toISOString(), 'User-Agent': 'Unimast-Webhook/1.0', 'Content-Type': 'application/json' }, body: JSON.stringify({ type: event, payload }) }); if (response.status >= 300 || response.status < 200) { const error = new Error(`Webhook failed with status ${response.status}`); logger.error('Webhook delivery failed', { ...logContext, metadata: { ...logContext.metadata, status: response.status, statusText: response.statusText } }, error); throw error; } logger.info('Webhook delivered successfully', { ...logContext, metadata: { ...logContext.metadata, status: response.status } }); return response; } catch (error) { logger.error('Webhook dispatch failed', logContext, error); throw error; } }

🔔 Sistema de Notificaciones

Configuración de Sonner

// app/components/ui/sonner.tsx "use client" import { useTheme } from "next-themes" import { Toaster as Sonner } from "sonner" type ToasterProps = React.ComponentProps<typeof Sonner> const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme() return ( <Sonner theme={theme as ToasterProps["theme"]} className="toaster group" toastOptions={{ classNames: { toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", description: "group-[.toast]:text-muted-foreground", actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", }, duration: 5000, // 5 segundos por defecto position: "top-right" }} {...props} /> ) } export { Toaster }

Patrones de Notificación

// app/lib/notifications/index.ts import { toast } from 'sonner'; export interface NotificationOptions { duration?: number; action?: { label: string; onClick: () => void; }; description?: string; } export class NotificationService { // Notificaciones de éxito static success(message: string, options?: NotificationOptions) { return toast.success(message, { duration: options?.duration, description: options?.description, action: options?.action ? { label: options.action.label, onClick: options.action.onClick } : undefined }); } // Notificaciones de error static error(message: string, options?: NotificationOptions) { return toast.error(message, { duration: options?.duration || 8000, // Errores más largos description: options?.description, action: options?.action ? { label: options.action.label, onClick: options.action.onClick } : undefined }); } // Notificaciones de advertencia static warning(message: string, options?: NotificationOptions) { return toast.warning(message, { duration: options?.duration, description: options?.description }); } // Notificaciones de información static info(message: string, options?: NotificationOptions) { return toast.info(message, { duration: options?.duration, description: options?.description }); } // Notificaciones de carga static loading(message: string, id?: string) { return toast.loading(message, { id }); } // Actualizar notificación de carga static update(id: string, message: string, type: 'success' | 'error' | 'info' = 'success') { toast[type](message, { id }); } // Dismiss notificación static dismiss(id: string) { toast.dismiss(id); } } // Uso en componentes export function UserModal({ user, roles }: UserModalProps) { const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = async (values: UserFormData) => { setIsSubmitting(true); const loadingId = NotificationService.loading('Guardando usuario...'); try { if (user) { await updateUser(user.id, values); NotificationService.update(loadingId, 'Usuario actualizado exitosamente'); } else { await createUser(values); NotificationService.update(loadingId, 'Usuario creado exitosamente'); } onClose(); } catch (error) { NotificationService.update(loadingId, `Error: ${error.message}`, 'error'); } finally { setIsSubmitting(false); } }; return ( <Dialog> <DialogContent> <Form {...form}> <form onSubmit={form.handleSubmit(handleSubmit)}> {/* Campos del formulario */} <DialogFooter> <Button type="button" variant="outline" onClick={onClose}> Cancelar </Button> <Button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Guardando...' : (user ? 'Actualizar' : 'Crear')} </Button> </DialogFooter> </form> </Form> </DialogContent> </Dialog> ); }

Manejo de Errores en Formularios

// app/lib/notifications/form-errors.ts import { NotificationService } from './index'; export interface FormError { field?: string; message: string; code?: string; } export class FormErrorHandler { // Manejar errores de validación static handleValidationErrors(errors: FormError[]) { if (errors.length === 1) { NotificationService.error(errors[0].message); } else if (errors.length > 1) { const message = `Se encontraron ${errors.length} errores de validación`; const description = errors.map(e => `• ${e.message}`).join('\n'); NotificationService.error(message, { description }); } } // Manejar errores de API static handleApiError(error: any, context?: string) { let message = 'Ha ocurrido un error inesperado'; let description: string | undefined; if (error.message) { message = error.message; } if (error.details) { description = typeof error.details === 'string' ? error.details : JSON.stringify(error.details, null, 2); } if (context) { message = `${context}: ${message}`; } NotificationService.error(message, { description }); } // Manejar errores de permisos static handlePermissionError(error: any) { if (error.name === 'PermissionError') { NotificationService.error( 'Acceso denegado', { description: 'No tienes permisos para realizar esta acción. Contacta al administrador.', duration: 10000 } ); } else { this.handleApiError(error, 'Error de permisos'); } } // Manejar errores de red static handleNetworkError(error: any) { if (error.name === 'TypeError' && error.message.includes('fetch')) { NotificationService.error( 'Error de conexión', { description: 'No se pudo conectar con el servidor. Verifica tu conexión a internet.', action: { label: 'Reintentar', onClick: () => window.location.reload() } } ); } else { this.handleApiError(error, 'Error de red'); } } }

🚀 Mejores Prácticas

1. Logging Estructurado

// ✅ CORRECTO: Logging con contexto completo export async function processInvoice(invoiceId: string, userId: string) { const logContext = { module: 'INVOICE', operation: 'processInvoice', userId, metadata: { invoiceId } }; logger.info('Starting invoice processing', logContext); try { const invoice = await getInvoice(invoiceId); if (!invoice) { throw new Error(`Invoice ${invoiceId} not found`); } const result = await processPayment(invoice); logger.info('Invoice processed successfully', { ...logContext, metadata: { ...logContext.metadata, amount: invoice.totalAmount, status: result.status } }); return result; } catch (error) { logger.error('Invoice processing failed', logContext, error); throw error; } } // ❌ INCORRECTO: Logging básico sin contexto export async function processInvoice(invoiceId: string) { try { const invoice = await getInvoice(invoiceId); const result = await processPayment(invoice); return result; } catch (error) { console.error('Error:', error); throw error; } }

2. Manejo de Errores en Capas

// ✅ CORRECTO: Manejo de errores en capas con transformación export async function createUser(userData: CreateUserData) { try { // Validación de datos const validatedData = userSchema.parse(userData); // Verificación de permisos const session = await getSession(); if (!session) { throw new PermissionError('Debe iniciar sesión para crear usuarios'); } // Verificación de duplicados const existingUser = await prisma.user.findUnique({ where: { email: validatedData.email } }); if (existingUser) { throw new ValidationError('El correo electrónico ya está registrado', 'email', validatedData.email); } // Creación del usuario const user = await prisma.user.create({ data: { ...validatedData, password: validatedData.password ? await hashPassword(validatedData.password) : null } }); return user; } catch (error) { // Transformar errores de Zod a errores del usuario if (error.name === 'ZodError') { const validationErrors = error.errors.map(e => ({ field: e.path.join('.'), message: e.message })); throw new ValidationError('Datos de entrada inválidos', validationErrors); } // Re-lanzar errores conocidos if (error instanceof PermissionError || error instanceof ValidationError) { throw error; } // Logging de errores inesperados logger.error('Unexpected error in createUser', { module: 'USER', operation: 'createUser', metadata: { userData: { email: userData.email } } }, error); // Transformar a error genérico del usuario throw new Error('Error interno del servidor. Contacte al administrador.'); } }

3. Notificaciones Contextuales

// ✅ CORRECTO: Notificaciones con contexto y acciones export function InvoiceActions({ invoice }: InvoiceActionsProps) { const handleCancel = async () => { const loadingId = NotificationService.loading('Cancelando factura...'); try { await cancelInvoice(invoice.id); NotificationService.update(loadingId, 'Factura cancelada exitosamente', 'success'); // Mostrar acción adicional NotificationService.success('Factura cancelada', { description: 'La factura ha sido cancelada y se ha generado una nota de crédito', action: { label: 'Ver nota de crédito', onClick: () => router.push(`/ecf/issued/${invoice.ncf}`) } }); } catch (error) { if (error.name === 'PermissionError') { NotificationService.update(loadingId, 'No tienes permisos para cancelar facturas', 'error'); } else if (error.message.includes('ya fue pagada')) { NotificationService.update(loadingId, 'No se puede cancelar una factura pagada', 'error'); } else { NotificationService.update(loadingId, 'Error al cancelar la factura', 'error'); } } }; return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="h-8 w-8 p-0"> <MoreHorizontal className="h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem onClick={handleCancel}> <X className="mr-2 h-4 w-4" /> Cancelar Factura </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ); }

4. Logging de Performance

// ✅ CORRECTO: Logging de métricas de performance export async function processECFDocument(encf: string) { const startTime = Date.now(); const logContext = { module: 'ECF', operation: 'processECFDocument', metadata: { encf } }; logger.info('Starting ECF document processing', logContext); try { // Medir tiempo de autenticación const authStart = Date.now(); const cert = await getCertificate(); const ecf = new ECF(cert, ECF_ENV); await ecf.authenticate(); const authDuration = Date.now() - authStart; // Medir tiempo de envío const sendStart = Date.now(); const result = await sendToDGII(encf); const sendDuration = Date.now() - sendStart; const totalDuration = Date.now() - startTime; logger.info('ECF document processed successfully', { ...logContext, metadata: { ...logContext.metadata, totalDuration, authDuration, sendDuration, trackId: result.trackId } }); return result; } catch (error) { const totalDuration = Date.now() - startTime; logger.error('ECF document processing failed', { ...logContext, metadata: { ...logContext.metadata, totalDuration } }, error); throw error; } }

🔧 Configuración y Variables de Entorno

Variables de Entorno para Logging

# .env # Nivel de logging LOG_LEVEL=info # error | warn | info | debug # Configuración de notificaciones TOAST_DURATION=5000 # Duración por defecto en ms TOAST_POSITION=top-right # Posición de las notificaciones # Configuración de errores ERROR_REPORTING_ENABLED=true # Habilitar reporte de errores ERROR_REPORTING_URL=https://... # URL para reporte de errores ERROR_REPORTING_KEY=... # Clave para reporte de errores

Configuración de Logging por Entorno

// app/lib/logger/config.ts export const LOG_CONFIG = { development: { level: LogLevel.DEBUG, enableConsole: true, enableFile: false, enableRemote: false }, staging: { level: LogLevel.INFO, enableConsole: true, enableFile: true, enableRemote: false }, production: { level: LogLevel.WARN, enableConsole: false, enableFile: true, enableRemote: true } } as const; export function getLogConfig() { const env = process.env.NODE_ENV || 'development'; return LOG_CONFIG[env as keyof typeof LOG_CONFIG] || LOG_CONFIG.development; }

🔍 Troubleshooting

Problemas Comunes

1. Errores de Logging

# Verificar nivel de logging echo $LOG_LEVEL # Verificar configuración node -e "console.log(process.env.LOG_LEVEL)" # Forzar nivel de logging LOG_LEVEL=debug pnpm dev

2. Errores de Notificaciones

// Debug de notificaciones export function debugNotifications() { console.log('=== UNIMAST ERP Notifications Debug ==='); // Verificar que Sonner esté configurado console.log('Sonner configured:', typeof toast === 'function'); // Probar notificaciones toast.success('Test success notification'); toast.error('Test error notification'); toast.warning('Test warning notification'); toast.info('Test info notification'); }

3. Errores de Workers

// Debug de workers export function debugWorkers() { console.log('=== UNIMAST ERP Workers Debug ==='); // Verificar conexión Redis console.log('Redis URL:', process.env.REDIS_URL ? 'Set' : 'Not Set'); // Verificar workers montados console.log('Workers mounted:', 'Check queue.ts mountWorkers function'); // Verificar logs de workers console.log('Worker logs:', 'Check console for worker event logs'); }

Debug de Sistema de Errores

// Función de debug para sistema de errores export function debugErrorSystem() { console.log('=== UNIMAST ERP Error System Debug ==='); // Configuración de logging console.log('Log Level:', process.env.LOG_LEVEL || 'info'); console.log('Environment:', process.env.NODE_ENV || 'development'); // Configuración de notificaciones console.log('Toast Duration:', '5000ms (default)'); console.log('Toast Position:', 'top-right'); // Sistema de errores console.log('Error Classes:', 'ECFError, ValidationError, PermissionError'); console.log('Error Constants:', 'Defined in lib/constants.ts'); // Logging console.log('Logger:', 'Singleton instance with structured logging'); console.log('Log Context:', 'Module, operation, userId, metadata'); }

¿Necesitas más detalles sobre algún aspecto específico? Revisa la documentación de coding-standards para entender cómo se implementan las mejores prácticas de manejo de errores, o la documentación de background jobs para aprender sobre el logging en colas de trabajo.