Skip to content

Latest commit

 

History

History
519 lines (398 loc) · 12.9 KB

File metadata and controls

519 lines (398 loc) · 12.9 KB

🔍 ROOT CAUSE ANALYSIS: Error al Cargar Conversaciones

Fecha: 2025-01-22 Prioridad: 🔴 CRÍTICA Estado: En análisis


🚨 SÍNTOMA

Problema reportado: "Error al abrir conversaciones del historial"

Comportamiento observado:

  • Usuario puede guardar conversaciones nuevas
  • Al intentar cargar conversación desde el sidebar, ocurre un error
  • Posible error de hidratación o tipo en el frontend

🔍 5 WHYS ANALYSIS

1. Why: ¿Por qué falla al cargar conversaciones del historial?

Respuesta: Los mensajes cargados desde la base de datos tienen timestamps como strings, pero el componente espera Date objects.

Evidencia:

  • conversation-store.ts:96 hace cast directo: return data as unknown as Conversation
  • No hay transformación de datos entre DB y frontend
  • Supabase guarda timestamps como ISO strings en JSONB

2. Why: ¿Por qué los timestamps son strings en lugar de Date objects?

Respuesta: Supabase almacena todo el campo messages como JSONB, lo cual serializa los Dates a strings ISO.

Evidencia:

// conversation-store.ts:41
messages: messages as unknown as JsonValue,

Cuando se guarda:

// Timestamp original (frontend)
timestamp: new Date()  // Date object

// Supabase lo convierte a:
timestamp: "2025-01-22T20:00:00.000Z"  // string

3. Why: ¿Por qué no se deserializan los timestamps al cargar?

Respuesta: La función loadConversation() no tiene lógica de transformación de datos.

Evidencia:

// conversation-store.ts:82-96
export async function loadConversation(conversationId: string) {
  const { data, error } = await supabase
    .from('conversations')
    .select('*')
    .eq('id', conversationId)
    .single()

  // ❌ NO HAY TRANSFORMACIÓN - Cast directo
  return data as unknown as Conversation
}

Comparación con guardar:

  • Al guardar: ✅ Usa type casting explícito para JSONB
  • Al cargar: ❌ No hay parsing ni validación

4. Why: ¿Por qué esto causa un error?

Respuesta: React / TypeScript esperan Date pero reciben string, causando:

  1. Type Mismatch:
// chat/page.tsx:48
setMessages(conversation.messages)
// messages tiene timestamp: string (de DB)
// pero Message interface define timestamp: Date
  1. Errores en rendering:
// ChatMessage puede intentar:
message.timestamp.toLocaleString()  // ❌ Error: string no tiene este método
  1. Hidration errors:
  • Server renderiza con strings
  • Client espera Date objects
  • React detecta mismatch

5. Why (ROOT CAUSE): ¿Cuál es la causa fundamental?

🎯 ROOT CAUSE: Falta de capa de serialización/deserialización entre DB y aplicación

Problemas arquitecturales:

  1. No hay Data Transfer Objects (DTOs)

    • Frontend y DB comparten mismas interfaces
    • No hay separación de concerns
    • Cambios en DB afectan directamente frontend
  2. No hay validación de esquema

    • Cast con as unknown as T bypasea type checking
    • No se valida estructura de datos de DB
    • TypeScript no puede ayudar en runtime
  3. No hay transformación de tipos

    • Dates ↔ ISO strings no se manejan
    • JSONB ↔ TypeScript objects sin validación
    • Metadata puede tener estructura inconsistente
  4. Inconsistencia en el ciclo de vida:

┌──────────────────────────────────────────────────────┐
│  FRONTEND (TypeScript types)                         │
│  ↓ Save: Date → string (implícito por Supabase)     │
│  DATABASE (JSONB: todos strings)                     │
│  ↓ Load: string → NO TRANSFORMATION ❌               │
│  FRONTEND (espera Date pero recibe string) 💥        │
└──────────────────────────────────────────────────────┘

📊 COMPARACIÓN: ANTES vs DESPUÉS

❌ IMPLEMENTACIÓN ACTUAL (PROBLEMA)

// 1. Frontend crea mensaje
const message = {
  id: "123",
  role: "user",
  content: "Hola",
  timestamp: new Date()  // ✅ Date object
}

// 2. Se guarda en DB
await saveConversation(userId, [message])
// Supabase convierte: new Date() → "2025-01-22T20:00:00.000Z"

// 3. Se carga de DB
const conversation = await loadConversation(conversationId)
// conversation.messages[0].timestamp = "2025-01-22T20:00:00.000Z"  ❌ string

// 4. Frontend intenta usar
setMessages(conversation.messages)
// React recibe timestamp: string cuando espera Date
// 💥 ERROR: Type mismatch, hydration error, o runtime error

✅ SOLUCIÓN PROPUESTA

Estrategia: Implementar capa de transformación

1. Definir DTOs (Data Transfer Objects)

// conversation-store.ts

// DTO para la base de datos
interface ConversationDB {
  id: string
  user_id: string
  messages: Array<{
    id: string
    role: 'user' | 'assistant'
    content: string
    timestamp: string  // ← ISO string en DB
  }>
  metadata: {
    title?: string
    created_at: string
    updated_at: string
  }
}

// Interface para el frontend (ya existe)
export interface Conversation {
  id: string
  user_id: string
  messages: Message[]  // timestamp es Date
  metadata: {
    title?: string
    created_at: string
    updated_at: string
  }
}

2. Implementar funciones de transformación

// Helpers de transformación
function dbMessageToMessage(dbMessage: any): Message {
  return {
    id: dbMessage.id,
    role: dbMessage.role,
    content: dbMessage.content,
    timestamp: new Date(dbMessage.timestamp)  // ✅ string → Date
  }
}

function messageTodb Message(message: Message): any {
  return {
    id: message.id,
    role: message.role,
    content: message.content,
    timestamp: message.timestamp.toISOString()  // ✅ Date → string
  }
}

3. Modificar loadConversation con transformación

export async function loadConversation(conversationId: string): Promise<Conversation | null> {
  const supabase = await createClient()

  const { data, error } = await supabase
    .from('conversations')
    .select('*')
    .eq('id', conversationId)
    .single()

  if (error) {
    console.error('Error loading conversation:', error)
    return null
  }

  if (!data) return null

  // ✅ TRANSFORMACIÓN EXPLÍCITA
  const conversation: Conversation = {
    id: data.id,
    user_id: data.user_id,
    messages: (data.messages as any[]).map(dbMessageToMessage),  // ✅ Transformar cada mensaje
    metadata: data.metadata as any
  }

  return conversation
}

4. Validación con Zod (Opcional pero recomendado)

import { z } from 'zod'

const MessageSchema = z.object({
  id: z.string(),
  role: z.enum(['user', 'assistant']),
  content: z.string(),
  timestamp: z.string().transform(str => new Date(str))  // ✅ Valida Y transforma
})

const ConversationSchema = z.object({
  id: z.string(),
  user_id: z.string(),
  messages: z.array(MessageSchema),
  metadata: z.object({
    title: z.string().optional(),
    created_at: z.string(),
    updated_at: z.string()
  })
})

// En loadConversation:
const validated = ConversationSchema.parse(data)
return validated

🔧 IMPLEMENTACIÓN PASO A PASO

Fase 1: Fix Inmediato (Sin breaking changes)

  1. ✅ Modificar loadConversation() para transformar timestamps
  2. ✅ Modificar getUserConversations() igual
  3. ✅ Agregar try-catch para manejar errores de parsing
  4. ✅ Logging para debugging

Fase 2: Mejora Arquitectural (Recomendada)

  1. ✅ Crear types/conversation-dto.ts con interfaces DB y Frontend
  2. ✅ Crear lib/chat/transformers.ts con funciones de transformación
  3. ✅ Instalar y configurar Zod para validación
  4. ✅ Actualizar todos los conversation stores
  5. ✅ Tests unitarios para transformers

Fase 3: Prevención (Long-term)

  1. ✅ Documentar convenciones de serialización
  2. ✅ ESLint rule para evitar as unknown as T
  3. ✅ Code review checklist para DB operations
  4. ✅ Agregar integration tests

🎯 SOLUCIÓN ELEGIDA: Fase 1 (Inmediata)

Ventajas:

  • ✅ Fix rápido sin refactor grande
  • ✅ No rompe código existente
  • ✅ Fácil de validar
  • ✅ Se puede hacer Fase 2 después

Desventajas:

  • ⚠️ No previene futuros problemas similares
  • ⚠️ Deuda técnica permanece

Trade-off aceptable: Arreglar el bug ahora, refactor después.


📝 CAMBIOS REQUERIDOS

Archivo: src/lib/chat/conversation-store.ts

Cambio 1: Agregar helper de transformación

/**
 * Transform DB message (timestamp as string) to frontend Message (timestamp as Date)
 */
function transformMessage(dbMessage: any): Message {
  return {
    id: dbMessage.id,
    role: dbMessage.role,
    content: dbMessage.content,
    timestamp: new Date(dbMessage.timestamp),
  }
}

Cambio 2: Modificar loadConversation

export async function loadConversation(conversationId: string): Promise<Conversation | null> {
  const supabase = await createClient()

  const { data, error } = await supabase
    .from('conversations')
    .select('*')
    .eq('id', conversationId)
    .single()

  if (error) {
    console.error('Error loading conversation:', error)
    return null
  }

  if (!data) return null

  // Transform messages: string timestamps → Date objects
  try {
    const transformedMessages = (data.messages as any[]).map(transformMessage)

    return {
      id: data.id,
      user_id: data.user_id,
      messages: transformedMessages,
      metadata: data.metadata as any,
    }
  } catch (err) {
    console.error('Error transforming conversation data:', err)
    return null
  }
}

Cambio 3: Modificar getUserConversations

export async function getUserConversations(userId: string): Promise<Conversation[]> {
  const supabase = await createClient()

  const { data, error } = await supabase
    .from('conversations')
    .select('*')
    .eq('user_id', userId)
    .order('metadata->updated_at', { ascending: false })

  if (error) {
    console.error('Error loading conversations:', error)
    return []
  }

  if (!data) return []

  // Transform each conversation
  try {
    return data.map(conv => ({
      id: conv.id,
      user_id: conv.user_id,
      messages: (conv.messages as any[]).map(transformMessage),
      metadata: conv.metadata as any,
    }))
  } catch (err) {
    console.error('Error transforming conversations data:', err)
    return []
  }
}

✅ BENEFICIOS DE LA SOLUCIÓN

  1. ✅ Fix del bug principal:

    • Conversaciones cargan correctamente
    • Timestamps son Date objects válidos
    • No más type mismatches
  2. ✅ Error handling robusto:

    • Try-catch previene crashes
    • Logs para debugging
    • Retorna null/[] en caso de error
  3. ✅ Type safety mejorada:

    • Transformación explícita documentada
    • Menos reliance en as unknown as T
    • TypeScript puede rastrear mejor los tipos
  4. ✅ Mantiene compatibilidad:

    • No breaking changes
    • Misma API pública
    • Funciona con código existente

🧪 PLAN DE TESTING

Tests Manuales:

  1. Crear conversación:

    • Chat nuevo
    • Enviar mensaje
    • Verificar que se guarda
  2. Cargar conversación:

    • Refresh página
    • Abrir sidebar
    • Click en conversación vieja
    • Verificar: No hay error
    • Verificar: Mensajes aparecen correctamente
    • Verificar: Timestamps se muestran bien
  3. Múltiples conversaciones:

    • Crear 3-4 conversaciones
    • Alternar entre ellas
    • Verificar que cada una carga correctamente
  4. Edge cases:

    • Conversación con 1 solo mensaje
    • Conversación muy antigua
    • Conversación con muchos mensajes (50+)

Verificación de Console:

✅ No debe haber: "Error loading conversation"
✅ No debe haber: "Error transforming"
✅ No debe haber: Hydration errors
✅ No debe haber: Type errors en runtime

📊 MÉTRICAS DE ÉXITO

Métrica Antes Después (Esperado)
Conversaciones cargan ❌ Error ✅ 100% success
Type errors ⚠️ Runtime errors ✅ 0 errors
Console errors 🔴 Múltiples ✅ 0 errors
User experience 💔 Roto ✅ Funcional

🚀 PRÓXIMOS PASOS (Después del fix)

1. Refactor Arquitectural (Fase 2)

  • Implementar DTOs separados
  • Agregar Zod validation
  • Crear transformers module

2. Prevención

  • Documentar serialization patterns
  • Crear guía de contribution
  • ESLint rules custom

3. Testing

  • Unit tests para transformers
  • Integration tests para conversation flow
  • E2E tests con Playwright

Estado: Listo para implementación Prioridad: 🔴 CRÍTICA Esfuerzo estimado: 15 minutos Risk: Bajo (cambios aislados con fallbacks)