Sie wissen genau, wo in der Produktion der Absturz war. Der Stacktrace zeigt auf Zeile 147 von invoice_service.py. Die Exception ist ein KeyError auf "customer_id". Sie ziehen den Code, führen die Tests aus, alles ist bestanden. Sie rufen den Endpunkt manuell mit einer Beispiel-Payload auf, es funktioniert einwandfrei.

Der Bug ist echt. Kunden stoßen auf ihn. Sie können ihn auf Ihrer Maschine nicht reproduzieren.

Das ist die Standard-Erfahrung beim Debuggen aus Fehlerberichten. Stacktraces sagen Ihnen wo. Sie sagen Ihnen nicht, was dort angekommen ist. Ohne die exakten Eingaben, die den Fehler ausgelöst haben, rekonstruieren Sie einen Tatort aus einem Foto der Kreideumrisse.

Was Crash-Replay tatsächlich bedeutet

Crash-Replay ist die Praxis, den vollständigen Eingabezustand, der einen Produktivfehler ausgelöst hat, aufzuzeichnen und den Code-Pfad mit diesem exakten Zustand in einer lokalen Umgebung erneut auszuführen. Das Ziel ist, aus „es ist auf Zeile 147 abgestürzt” „hier ist ein Testfall, der Zeile 147 jedes Mal zum Absturz bringt” zu machen.

Die meisten Entwickler machen das bereits manuell. Sie lesen den Stacktrace, raten, welcher Request es verursacht hat, versuchen, die Payload aus Logs zu rekonstruieren, und hoffen, dass Ihre lokale Datenbank ähnliche Daten hat. Das scheitert aus demselben Grund wie Astrologie: Sie matchen Muster ohne genug Informationen.

Der Unterschied zwischen Raten und Replay ist Serialisierung. Sie müssen die exakten Funktions-Eingaben, die exakten Datenbank-Antworten und die exakten externen API-Rückgabewerte im Moment des Fehlers erfassen. Dann füttern Sie sie zurück ein.

Wie man Produktiv-Eingaben für das Replay erfasst

Das einfachste effektive Pattern ist ein Decorator, der die Argumente einer Funktion abfängt, sie auf die Festplatte serialisiert und sie irgendwohin verschickt, wo Sie später Zugriff haben. Wenn die Funktion abstürzt, haben Sie einen eingefrorenen snapshot der Welt, die ihn produziert hat.

Hier ist eine funktionierende Python-Implementierung:

import json
import functools
import traceback
from pathlib import Path
from datetime import datetime, timezone

CAPTURE_DIR = Path("/var/crash-captures")


def capture_for_replay(func):
    """Decorator that captures inputs and outputs for replay debugging."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        capture = {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "function": func.__qualname__,
            "module": func.__module__,
            "args": args,
            "kwargs": kwargs,
            "exception": None,
            "traceback": None,
        }

        try:
            result = func(*args, **kwargs)
            capture["result"] = result
            return result
        except Exception as exc:
            capture["exception"] = {
                "type": type(exc).__name__,
                "message": str(exc),
            }
            capture["traceback"] = traceback.format_exc()

            # Write crash capture to disk
            CAPTURE_DIR.mkdir(parents=True, exist_ok=True)
            filename = (
                f"{func.__name__}_"
                f"{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.json"
            )
            capture_path = CAPTURE_DIR / filename

            with open(capture_path, "w") as f:
                json.dump(capture, f, default=str, indent=2)

            raise  # Re-raise so normal error handling continues

    return wrapper

Wenden Sie ihn auf die Funktion an, die abstürzt:

@capture_for_replay
def generate_invoice(customer_data: dict, line_items: list) -> dict:
    customer_id = customer_data["customer_id"]  # This is line 147
    # ... rest of invoice logic
    return {"invoice_id": "INV-123", "total": 0}

Wenn KeyError: "customer_id" in der Produktion ausgelöst wird, bekommen Sie eine JSON-Datei, die so aussieht:

{
  "timestamp": "2026-08-15T14:32:11+00:00",
  "function": "generate_invoice",
  "module": "billing.invoice_service",
  "args": [
    {},
    [{"sku": "PRO-1", "price": 99.0}]
  ],
  "kwargs": {},
  "exception": {
    "type": "KeyError",
    "message": "'customer_id'"
  },
  "traceback": "..."
}

Das erste Argument war ein leeres Dictionary. Das ist der gesamte Bug. Der upstream-Caller hat {} statt eines Kundendatensatzes übergeben.

Sie haben jetzt einen Testfall:

def test_generate_invoice_with_empty_customer():
    with pytest.raises(KeyError, match="customer_id"):
        generate_invoice({}, [{"sku": "PRO-1", "price": 99.0}])

Dieser Test schlägt vor dem Fix fehl und ist danach bestanden. Wichtiger noch: Sie mussten nicht raten. Der Crash hat Ihnen genau gesagt, was zu testen ist.

Warum Capture dependencies nicht sehen kann, die es nicht sieht

Dieses Pattern erfasst Funktionsargumente, nicht globalen Zustand. Wenn generate_invoice aus einer Datenbank liest, eine externe API aufruft oder eine Umgebungsvariable prüft, sind diese Werte nicht in args und kwargs. Das Replay funktioniert nur, wenn Ihre lokale Umgebung zufällig übereinstimmt.

Sie können den Decorator erweitern, um external dependencies explizit zu erfassen:

@capture_for_replay
def generate_invoice(
    customer_data: dict,
    line_items: list,
    db_conn
) -> dict:
    tax_rate = db_conn.execute(
        "SELECT rate FROM tax_rates WHERE region = ?",
        (customer_data["region"],)
    ).fetchone()[0]
    # ...

Aber db_conn ist ein Verbindungsobjekt. Sie können es nicht zu JSON serialisieren. Was Sie serialisieren können, ist die Query und das Ergebnis. Der prinzipiellere Ansatz ist, reine Logik von Seiteneffekten zu trennen. Übergeben Sie das Query-Ergebnis als Argument, nicht die Verbindung:

@capture_for_replay
def generate_invoice(
    customer_data: dict,
    line_items: list,
    tax_rate: float
) -> dict:
    # Pure function. All inputs are serializable.
    total = sum(item["price"] for item in line_items)
    total_with_tax = total * (1 + tax_rate)
    return {
        "invoice_id": "INV-123",
        "total": round(total_with_tax, 2),
    }

Das ist functional core, imperative shell. Es macht Capture trivial, weil es keinen versteckten Zustand gibt. Jede Eingabe, die zählt, ist in der Argumentliste.

Das Nichtdeterminismus-Problem, das Sie nicht wegcapturen können

Selbst mit perfektem Eingabe-Capture sind manche Crashes nicht reproduzierbar. Race Conditions hängen vom Thread-Timing ab. Speicher-Korruption hängt vom Allocator-Zustand ab. Externe APIs liefern bei jedem Aufruf unterschiedliche Daten. Zufallszahlengeneratoren produzieren unterschiedliche Sequenzen, es sei denn, Sie seeden sie.

Wenn Ihr Crash eine Race Condition ist, wird das Replay der gleichen Eingaben in einem single-threaded lokalen Test ihn nicht auslösen. Sie brauchen das tatsächliche Concurrency-Pattern, was bedeutet, die Original-Threads laufen zu lassen, was bedeutet, dass Sie sich von „Replay” zu „Distributed Tracing plus Load Testing” bewegt haben. Das ist ein anderes Tool.

Für die meisten Anwendungs-level-Bugs ist Eingabe-Replay ausreichend. Für Heisenbugs nicht. Wissen Sie, mit welchem Sie es zu tun haben, bevor Sie drei Stunden damit verbringen, ein Timing-Problem zu replayen.

Capture in der Produktion betriebsbereit machen

Der Decorator oben schreibt auf die lokale Festplatte. In der Produktion wollen Sie, dass diese Captures in Object Storage oder Ihren Error-Tracker verschifft werden. Die Integration ist geradlinig: Ersetzen Sie den open(capture_path, "w")-Aufruf durch einen S3-Upload oder einen Anhang an Ihr Sentry-Issue.

Die meisten Teams sollten mit einer Funktion anfangen. Wählen Sie den Service, der am häufigsten abstürzt. Fügen Sie den Decorator hinzu. Warten Sie auf den nächsten Crash. Wenn er eintrifft, haben Sie eine JSON-Payload, die eine dreißigminütige Ratesitzung in einen fünfminütigen Test verwandelt.

Wenn Sie bereits Sentry nutzen, erfasst die Breadcrumbs-Funktion automatisch einen Teil dieses Kontexts. Für einen eigenen Ansatz passt das Pattern in dreißig Zeilen Python und funktioniert in jeder Runtime, die Decorator oder Middleware unterstützt.

Erfassen Sie diese Woche einen Endpunkt

Fügen Sie Capture Ihrem Endpunkt mit den meisten Fehlern diese Woche hinzu. Nicht jeden Endpunkt. Nicht jede Funktion. Einen. Wenn er abstürzt, schreiben Sie den Testfall aus dem Capture, bevor Sie den Bug fixen. Führen Sie den Test aus, sehen Sie zu, wie er fehlschlägt, wenden Sie den Fix an, sehen Sie zu, wie er bestanden ist.

Diese Schleife, vom Produktiv-Crash zum reproduzierbaren Testfall, ist das, was Debuggen von Archäologie unterscheidet.