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 erroresConfiguració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 dev2. 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.