Skip to content
··5 min de lectura

De 3 Minutos a 500ms: El Bug de Registro Que No Tenía Ningún Sentido

Rastreé un retraso de 3 minutos en el registro que resultó ser el usuario de Schrödinger: existiendo y no existiendo al mismo tiempo debido al lag de replicación de base de datos entre escrituras y lecturas.

¿Conoces esa sensación cuando los usuarios reportan un bug que no tiene ningún sentido? "La app tarda 3 minutos en cargar después de registrarme." ¿Tres minutos? Eso no es un tiempo de carga, eso es una pausa para el café. Esta es la historia de cómo rastreé uno de los bugs más raros en la historia de DIALØGUE.

El Misterio Comienza

Todo empezó de manera inocente. Un nuevo usuario se registró usando Google SSO, emocionado por probar DIALØGUE. Luego… nada. Bueno, no exactamente nada: vieron nuestro elegante esqueleto de carga. Durante tres minutos enteros.

¿La parte extraña? Solo le pasaba a los usuarios nuevos. Los usuarios existentes podían iniciar sesión al instante. Y no era consistente: a veces era 1 minuto, a veces 3, ocasionalmente funcionaba de inmediato.

Mi primer pensamiento: "Debe ser un problema de arranque en frío." (Narrador: No era un problema de arranque en frío.)

La Investigación

Ronda 1: Culpar al Frontend

```typescript
// Primer sospechoso: El hook de carga del perfil
useEffect(() => \{
  if (user) {
    fetchUserProfile(); // Este tardaba una eternidad
  \}
}, [user]);
```

Agregué temporizadores en todas partes. La llamada a la API tardaba efectivamente 3 minutos. ¿Pero por qué? El backend debería devolver datos o dar un error, no simplemente… esperar.

Ronda 2: Culpar al Backend

Profundicé en nuestras Edge Functions de Supabase:

```typescript
// Edge Function para obtener el perfil de usuario
const \{ data: profile \} = await supabase
  .from('users')
  .select('*')
  .eq('id', userId)
  .single();

if (!profile) \{
  // Usuario nuevo - crear perfil
  await createUserProfile(userId);
\}
```

Esto parecía estar bien. Debería ser rápido, ¿verdad? Momento de agregar más logs.

Ronda 3: La Trama Se Complica

Después de agregar logs en todas partes (y quiero decir en todas partes), descubrí algo extraño:

[00:00] El usuario inicia sesión con Google
[00:01] El trigger de Auth se ejecuta - crea el registro del usuario
[00:01] El frontend solicita el perfil
[00:01] La Edge Function busca al usuario... SIN RESULTADO
[00:02] La Edge Function intenta crear al usuario...
[00:02] Error de restricción de base de datos: El usuario ya existe
[00:03] La función reintenta...
[03:00] La función finalmente expira

Espera, ¿qué? ¿El usuario no existe, pero al mismo tiempo ya existe? ¿El usuario de Schrödinger? T.T

La Revelación

Después de mirar los logs de la base de datos hasta que me dolieron los ojos, finalmente lo vi. Nuestra base de datos tenía procesos en competencia:

  1. Trigger de Supabase Auth: Crea el registro del usuario al registrarse
  2. Edge Function: Intenta crear el usuario si no se encuentra
  3. Lag de Replicación de Base de Datos: El INSERT del trigger aún no se había replicado a la réplica de lectura

Esto es lo que estaba pasando:

-- Trigger de Auth (en la base de datos primaria)
INSERT INTO users (id, email) VALUES ($1, $2);

-- Edge Function (leyendo desde la réplica)
SELECT * FROM users WHERE id = $1; -- ¡No devuelve nada!

-- Edge Function (intentando ayudar)
INSERT INTO users (id, email) VALUES ($1, $2); -- ¡CONFLICTO!

La función reintentaba con backoff exponencial, cada intento chocando con la misma condición de carrera hasta que:

  • La replicación finalmente se ponía al día (1-3 minutos)
  • La función expiraba (3 minutos)

Actualización: El Verdadero Culpable y la Solución Final y Robusta

Después del post inicial, continuamos depurando y, aunque nuestros cambios en el backend fueron mejoras, la raíz del misterio seguía siendo elusiva. El avance llegó cuando trasladamos el foco del backend al flujo de hidratación y autenticación del lado del cliente.

El Verdadero Culpable: Una Condición de Carrera del Lado del Cliente

El problema no era un trigger de base de datos lento ni una Edge Function fría. El verdadero problema era una clásica condición de carrera del lado del cliente:

  1. Redirección OAuth: Un nuevo usuario inicia sesión con Google y es redirigido de vuelta a nuestra app.
  2. Sesión Asíncrona: La librería cliente de Supabase (supabase-js) comienza a procesar el token de la URL para establecer una sesión. Este es un proceso asíncrono.
  3. Renderizado Prematuro: Sin embargo, nuestra app React se renderiza de inmediato. Solicita la sesión del usuario antes de que el proceso asíncrono del paso 2 se complete.
  4. El Fallo: La app obtiene una sesión null, concluye que el usuario no está autenticado y renderiza un estado en blanco o de error. Momentos después, la sesión queda disponible, pero ya es demasiado tarde: la UI ya tomó su decisión.

Nuestras soluciones provisionales iniciales, como agregar reintentos del lado del cliente, eran solo síntomas de luchar contra esta condición de carrera fundamental.

La Solución: Una Fuente Única de Verdad para la Autenticación

La solución correcta y definitiva fue rediseñar la gestión del estado de autenticación en el frontend para que fuera verdaderamente impulsada por eventos y robusta.

1. Lógica Centralizada en SupabaseProvider: Refactorizamos nuestro SupabaseProvider para que sea la única fuente autorizada de verdad para la autenticación. Eliminamos todos los demás listeners y verificaciones de otros hooks.

2. Usando onAuthStateChange Correctamente: El núcleo del arreglo fue confiar exclusivamente en el listener onAuthStateChange de Supabase.

// Lógica simplificada en SupabaseProvider.tsx

export function SupabaseProvider(\{ children \}) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true); // Comenzar en estado de carga

  useEffect(() => {
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (event, session) => \{
        setUser(session?.user ?? null);
        // Solo consideramos la autenticación "terminada" una vez que este listener se ejecuta.
        setLoading(false);
      \}
    );

    return () => subscription.unsubscribe();
  }, []);

  // ...
}

Este patrón garantiza que toda la aplicación permanezca en estado de loading hasta que Supabase confirme que la sesión del usuario es válida o nula. Ya no hay condición de carrera.

El Final Feliz

DIALØGUE ahora incorpora nuevos usuarios en menos de un segundo. No más pausas para el café durante el registro. No más usuarios confundidos preguntándose si rompieron algo.

El arreglo lleva 3 semanas en producción. Cero problemas de timeout. Cero condiciones de carrera. Solo registros rápidos y fluidos como debería haber sido desde el principio.

¿Valió la pena pasar una semana depurando esto? Cuando veo a nuevos usuarios crear sin problemas su primer podcast a los pocos minutos de registrarse, absolutamente. :D

¿Alguna vez rastreaste un bug donde la cosa simultáneamente existía y no existía? Siento que cada desarrollador tiene al menos una historia de bug de Schrödinger. ¡Me encantaría escuchar la tuya!

Un abrazo,

Chandler

¿Quieres probar el registro ahora veloz? Crea tu podcast con IA en DIALØGUE. Te prometo que ya no tardará 3 minutos :)

Seguir leyendo

Mi Trayectoria
Conectar
Idioma
Preferencias