De 3 minutes à 500ms : le bug d'inscription qui n'avait aucun sens
J'ai traqué un délai d'inscription de 3 minutes qui s'est avéré être l'utilisateur de Schrödinger — existant et n'existant pas en même temps à cause du délai de réplication de base de données entre les écritures et les lectures.
Tu connais ce sentiment quand des utilisateurs signalent un bug qui n'a absolument aucun sens ? « L'application prend 3 minutes à charger après mon inscription. » Trois minutes ? Ce n'est pas un temps de chargement, c'est une pause café. Voici l'histoire de comment j'ai traqué l'un des bugs les plus bizarres de l'histoire de DIALØGUE.
Le début du mystère
Tout a commencé innocemment. Un nouvel utilisateur s'est inscrit via Google SSO, impatient d'essayer DIALØGUE. Puis… rien. Eh bien, pas exactement rien — il avait notre magnifique skeleton de chargement. Pendant trois longues minutes.
La partie bizarre ? Ça n'arrivait qu'aux nouveaux utilisateurs. Les utilisateurs existants pouvaient se connecter instantanément. Et ce n'était pas constant — parfois c'était 1 minute, parfois 3, occasionnellement ça fonctionnait immédiatement.
Ma première pensée : « Ça doit être un problème de cold start. » (Le narrateur : Ce n'était pas un problème de cold start.)
L'enquête
Round 1 : Accuser le Frontend
```typescript
// Premier suspect : le hook de chargement de profil
useEffect(() => \{
if (user) {
fetchUserProfile(); // Ça prenait une éternité
\}
}, [user]);
```
J'ai ajouté des timers partout. L'appel API prenait effectivement 3 minutes. Mais pourquoi ? Le backend devrait soit retourner des données soit renvoyer une erreur, pas juste… attendre.
Round 2 : Accuser le Backend
J'ai plongé dans nos Supabase Edge Functions :
```typescript
// Edge Function pour obtenir le profil utilisateur
const \{ data: profile \} = await supabase
.from('users')
.select('*')
.eq('id', userId)
.single();
if (!profile) \{
// Nouvel utilisateur - créer le profil
await createUserProfile(userId);
\}
```
Ça avait l'air bien. Ça devrait être rapide, non ? Il était temps d'ajouter plus de logs.
Round 3 : L'intrigue s'épaissit
Après avoir ajouté des logs partout (et je veux dire partout), j'ai découvert quelque chose de bizarre :
[00:00] L'utilisateur se connecte avec Google
[00:01] Le déclencheur Auth se lance - crée l'enregistrement utilisateur
[00:01] Le frontend demande le profil
[00:01] L'Edge Function interroge l'utilisateur... AUCUN RÉSULTAT
[00:02] L'Edge Function essaie de créer l'utilisateur...
[00:02] Erreur de contrainte de base de données : l'utilisateur existe déjà
[00:03] La fonction réessaie...
[03:00] La fonction expire finalement
Attends quoi ? L'utilisateur n'existe pas, mais il existe déjà ? L'utilisateur de Schrödinger ? T.T
La révélation
Après avoir fixé les logs de base de données jusqu'à avoir les yeux qui brûlent, j'ai finalement vu. Notre base de données avait des processus concurrents :
- Déclencheur Auth Supabase : Crée l'enregistrement utilisateur à l'inscription
- Edge Function : Essaie de créer l'utilisateur s'il n'est pas trouvé
- Délai de réplication de base de données : L'INSERT du déclencheur n'avait pas encore été répliqué vers le replica de lecture
Voici ce qui se passait :
-- Déclencheur Auth (sur la base de données primaire)
INSERT INTO users (id, email) VALUES ($1, $2);
-- Edge Function (lecture depuis le replica)
SELECT * FROM users WHERE id = $1; -- Ne retourne rien !
-- Edge Function (essayant d'aider)
INSERT INTO users (id, email) VALUES ($1, $2); -- CONFLIT !
La fonction réessayait avec backoff exponentiel, chaque tentative heurtant la même race condition jusqu'à ce que :
- La réplication rattrape finalement (1-3 minutes)
- La fonction expire (3 minutes)
Mise à jour : le vrai coupable et la solution finale robuste
Après l'article initial, nous avons continué à déboguer, et bien que nos changements backend étaient des améliorations, la racine du mystère restait insaisissable. La percée est venue quand nous avons déplacé notre attention du backend vers le flux d'hydratation et d'authentification côté client.
Le vrai coupable : une race condition côté client
Le problème n'était pas un déclencheur de base de données lent ou une Edge Function froide. Le vrai problème était une race condition classique côté client :
- Redirection OAuth : Un nouvel utilisateur se connecte avec Google et est redirigé vers notre application.
- Session asynchrone : La bibliothèque client Supabase (
supabase-js) commence à traiter le token depuis l'URL pour établir une session. C'est un processus asynchrone. - Rendu prématuré : Notre application React, cependant, se rend immédiatement. Elle demande la session de l'utilisateur avant que le processus asynchrone de l'étape 2 soit terminé.
- L'échec : L'application obtient une session
null, conclut que l'utilisateur n'est pas connecté, et rend un état vide ou d'erreur. Quelques instants plus tard, la session devient disponible, mais il est trop tard — l'interface a déjà pris sa décision.
Nos contournements initiaux, comme l'ajout de retries côté client, n'étaient que des symptômes de la lutte contre cette race condition fondamentale.
La solution : une source unique de vérité pour l'authentification
La solution correcte et finale était de refactoriser la gestion d'état de l'authentification de notre frontend pour être vraiment event-driven et robuste.
1. Logique centralisée dans SupabaseProvider :
Nous avons refactorisé notre SupabaseProvider pour être la source unique et faisant autorité de vérité pour l'authentification. Nous avons supprimé tous les autres écouteurs et vérifications des autres hooks.
2. Utilisation correcte de onAuthStateChange :
Le cœur du fix était de se fier exclusivement à l'écouteur onAuthStateChange de Supabase.
// Logique simplifiée dans SupabaseProvider.tsx
export function SupabaseProvider(\{ children \}) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true); // Commencer en état de chargement
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => \{
setUser(session?.user ?? null);
// On considère l'authentification « terminée » seulement quand cet écouteur se déclenche.
setLoading(false);
\}
);
return () => subscription.unsubscribe();
}, []);
// ...
}
Ce pattern garantit que toute l'application reste en état loading jusqu'à ce que Supabase confirme que la session de l'utilisateur est soit valide soit nulle. Il n'y a plus de race condition.
La happy ending
DIALØGUE intègre désormais les nouveaux utilisateurs en moins d'une seconde. Plus de pauses café à l'inscription. Plus d'utilisateurs confus se demandant s'ils ont cassé quelque chose.
Le fix est en production depuis 3 semaines. Zéro problème de timeout. Zéro race condition. Juste des inscriptions fluides et rapides comme ça aurait dû être depuis le début.
Est-ce que ça valait de passer une semaine à déboguer ça ? Quand je vois de nouveaux utilisateurs créer leur premier podcast en quelques minutes après leur inscription — absolument. :D
Tu as déjà traqué un bug où la chose existait et n'existait pas simultanément ? J'ai le sentiment que chaque développeur a au moins une histoire de bug de Schrödinger. J'adorerais entendre la tienne !
Cordialement,
Chandler
Tu veux essayer l'inscription désormais rapide ? Crée ton podcast IA sur DIALØGUE. Je promets que ça ne prendra plus 3 minutes :)





