Skip to content
··4 Min. Lesezeit

Von 3 Minuten auf 500ms: Der Signup-Bug, der keinen Sinn ergab

Ich habe eine 3-minütige Signup-Verzögerung aufgespürt, die sich als Schrödingers Nutzer herausstellte – gleichzeitig existierend und nicht existierend aufgrund von Datenbankreplikationsverzögerungen zwischen Schreibvorgängen und Lesevorgängen.

Kennst du das Gefühl, wenn Nutzer einen Bug melden, der absolut keinen Sinn ergibt? "Die App braucht 3 Minuten zum Laden nach der Anmeldung." Drei Minuten? Das ist keine Ladezeit, das ist eine Kaffeepause. Das ist die Geschichte, wie ich einen der seltsamsten Bugs in DIALØGUEs Geschichte aufgespürt habe.

Das Geheimnis beginnt

Es begann harmlos genug. Ein neuer Nutzer hat sich mit Google SSO angemeldet, voller Vorfreude auf DIALØGUE. Dann... nichts. Nun, nicht genau nichts – er bekam unser wunderschön gestaltetes Lade-Skeleton. Für volle drei Minuten.

Das Seltsame? Es passierte nur bei neuen Nutzern. Bestehende Nutzer konnten sich sofort anmelden. Und es war nicht konsistent – manchmal war es 1 Minute, manchmal 3, gelegentlich funktionierte es sofort.

Mein erster Gedanke: "Muss ein Cold-Start-Problem sein." (Erzähler: Es war kein Cold-Start-Problem.)

Die Untersuchung

Runde 1: Das Frontend beschuldigen

```typescript
// Erster Verdächtiger: Der Profil-Lade-Hook
useEffect(() => \{
  if (user) {
    fetchUserProfile(); // Das hat ewig gedauert
  \}
}, [user]);
```

Überall Timer hinzugefügt. Der API-Aufruf dauerte tatsächlich 3 Minuten. Aber warum? Das Backend sollte entweder Daten zurückgeben oder einen Fehler ausgeben, nicht einfach... warten.

Runde 2: Das Backend beschuldigen

Ich bin in unsere Supabase Edge Functions eingetaucht:

```typescript
// Edge Function zum Abrufen des Nutzerprofils
const \{ data: profile \} = await supabase
  .from('users')
  .select('*')
  .eq('id', userId)
  .single();

if (!profile) \{
  // Neuer Nutzer - Profil erstellen
  await createUserProfile(userId);
\}
```

Das sah in Ordnung aus. Sollte schnell sein, oder? Zeit, mehr Logging hinzuzufügen.

Runde 3: Die Geschichte verdickt sich

Nachdem ich überall Logs hinzugefügt hatte (und ich meine überall), entdeckte ich etwas Bizarres:

[00:00] Nutzer meldet sich mit Google an
[00:01] Auth-Trigger wird ausgelöst – erstellt Nutzerdatensatz
[00:01] Frontend fordert Profil an
[00:01] Edge Function fragt nach Nutzer... KEIN ERGEBNIS
[00:02] Edge Function versucht, Nutzer zu erstellen...
[00:02] Datenbank-Constraint-Fehler: Nutzer existiert bereits
[00:03] Funktion wiederholt...
[03:00] Funktion läuft schließlich auf Timeout

Warte, was? Der Nutzer existiert nicht, aber existiert auch schon? Schrödingers Nutzer? T.T

Die Offenbarung

Nachdem ich in Datenbankprotokollen gestarrt hatte, bis meine Augen schmerzten, habe ich es endlich gesehen. Unsere Datenbank hatte konkurrierende Prozesse:

  1. Supabase Auth Trigger: Erstellt Nutzerdatensatz bei der Anmeldung
  2. Edge Function: Versucht, Nutzer zu erstellen, wenn er nicht gefunden wird
  3. Datenbankreplikationsverzögerung: Das INSERT des Triggers hatte noch nicht zum Read Replica repliziert

Folgendes passierte:

-- Auth Trigger (auf der primären Datenbank)
INSERT INTO users (id, email) VALUES ($1, $2);

-- Edge Function (liest vom Replica)
SELECT * FROM users WHERE id = $1; -- Gibt nichts zurück!

-- Edge Function (versucht zu helfen)
INSERT INTO users (id, email) VALUES ($1, $2); -- KONFLIKT!

Die Funktion würde mit exponentiellem Backoff wiederholen, wobei jeder Versuch dieselbe Race Condition trifft, bis entweder:

  • Die Replikation aufgeholt hat (1-3 Minuten)
  • Die Funktion auf Timeout gelaufen ist (3 Minuten)

Update: Der eigentliche Schuldige und die endgültige, robuste Lösung

Nach dem ersten Beitrag haben wir weiter debuggt, und obwohl unsere Backend-Änderungen Verbesserungen waren, blieb die Wurzel des Geheimnisses schwer fassbar. Der Durchbruch kam, als wir unseren Fokus vom Backend auf die clientseitige Hydration und den Authentifizierungsfluss verlagerten.

Der eigentliche Schuldige: Eine clientseitige Race Condition

Das Problem war kein langsamer Datenbank-Trigger oder eine kalte Edge Function. Das eigentliche Problem war eine klassische clientseitige Race Condition:

  1. OAuth-Redirect: Ein neuer Nutzer meldet sich mit Google an und wird zurück zu unserer App weitergeleitet.
  2. Asynchrone Session: Die Supabase-Client-Bibliothek (supabase-js) beginnt, den Token aus der URL zu verarbeiten, um eine Session herzustellen. Das ist ein asynchroner Prozess.
  3. Vorzeitiges Rendering: Unsere React-App rendert jedoch sofort. Sie fragt nach der Session des Nutzers, bevor der asynchrone Prozess aus Schritt 2 abgeschlossen ist.
  4. Das Scheitern: Die App erhält eine null-Session, schlussfolgert, dass der Nutzer nicht angemeldet ist, und rendert einen leeren oder Fehlerzustand. Einen Moment später ist die Session verfügbar, aber es ist zu spät – die UI hat bereits ihre Entscheidung getroffen.

Unsere anfänglichen Workarounds, wie das Hinzufügen von clientseitigen Wiederholungsversuchen, waren nur Symptome des Kampfens gegen diese grundlegende Race Condition.

Die Lösung: Eine einzige Quelle der Wahrheit für die Authentifizierung

Die korrekte und endgültige Lösung war die Neugestaltung unseres Frontend-Authentifizierungsstate-Managements zu einem wirklich ereignisgesteuerten und robusten System.

1. Zentralisierte Logik in SupabaseProvider: Wir haben unseren SupabaseProvider umstrukturiert, um die einzige, maßgebliche Quelle der Wahrheit für die Authentifizierung zu sein. Wir haben alle anderen Listener und Prüfungen aus anderen Hooks entfernt.

2. onAuthStateChange korrekt verwenden: Der Kern des Fixes war, sich ausschließlich auf Supabasas onAuthStateChange-Listener zu verlassen.

// Vereinfachte Logik in SupabaseProvider.tsx

export function SupabaseProvider(\{ children \}) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true); // Im Ladezustand starten

  useEffect(() => {
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (event, session) => \{
        setUser(session?.user ?? null);
        // Wir betrachten die Authentifizierung erst als "abgeschlossen",
        // wenn dieser Listener ausgelöst wird.
        setLoading(false);
      \}
    );

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

  // ...
}

Dieses Muster stellt sicher, dass die gesamte Anwendung im loading-Zustand bleibt, bis Supabase bestätigt, ob die Session des Nutzers gültig oder null ist. Es gibt keine Race Condition mehr.

Das glückliche Ende

DIALØGUE nimmt neue Nutzer jetzt in unter einer Sekunde auf. Keine Kaffeepausen mehr beim Signup. Keine verwirrten Nutzer mehr, die sich fragen, ob sie etwas kaputt gemacht haben.

Der Fix ist seit 3 Wochen in Produktion. Null Timeout-Probleme. Null Race Conditions. Nur ein reibungsloser, schneller Signup, wie er von Anfang an hätte sein sollen.

War es den Aufwand wert, eine Woche für das Debugging zu investieren? Wenn ich sehe, wie neue Nutzer innerhalb von Minuten nach der Anmeldung ihren ersten Podcast erstellen – absolut. :D

Hast du jemals einen Bug aufgespürt, bei dem die Sache gleichzeitig existierte und nicht existierte? Ich glaube, jeder Entwickler hat mindestens eine Schrödingers-Bug-Geschichte. Ich würde gerne deine hören!

Viele Grüße,

Chandler

Möchtest du den jetzt schnellen Signup ausprobieren? Erstelle deinen KI-Podcast bei DIALØGUE. Ich verspreche, es wird nicht mehr 3 Minuten dauern :)

Weiterlesen

Mein Weg
Vernetzen
Sprache
Einstellungen