Anda tahu persis di mana produksi crash. Stack trace menunjuk ke baris 147 dari invoice_service.py. Exceptionnya adalah KeyError pada "customer_id". Anda menarik kode, menjalankan test, semuanya lolos. Anda mengenai endpoint secara manual dengan payload sampel, ia berfungsi dengan baik.

Bugnya nyata. Pelanggan mengenainya. Anda tidak bisa membuatnya terjadi di mesin Anda.

Ini adalah pengalaman standar debugging dari laporan error. Stack trace memberi tahu di mana. Mereka tidak memberi tahu apa yang tiba di sana. Tanpa input eksak yang memicu kegagalan, Anda merekonstruksi tempat kejadian dari foto garis kapur.

Apa artinya crash replay sebenarnya

Crash replay adalah praktik menangkap keadaan input lengkap yang memicu kegagalan produksi dan mengeksekusi ulang jalur kode dengan keadaan eksak itu di lingkungan lokal. Tujuannya adalah mengubah “ia crash di baris 147” menjadi “ini adalah kasus test yang membuat baris 147 crash setiap saat.”

Kebanyakan pengembang sudah melakukan versi manual dari ini. Anda membaca stack trace, menebak request mana yang menyebabkannya, mencoba merekonstruksi payload dari log, dan berharap database lokal Anda memiliki data serupa. Ini gagal dengan alasan yang sama mengapa astrologi gagal: Anda mencocokkan pola tanpa informasi yang cukup.

Perbedaan antara menebak dan memutar ulang adalah serialization. Anda harus menangkap input fungsi eksak, respons database eksak, dan nilai kembalian API eksternal eksak pada saat kegagalan. Kemudian Anda memasukkannya kembali.

Cara menangkap input produksi untuk replay

Pola paling sederhana yang efektif adalah decorator yang mencegat argumen fungsi, menyerialisasinya ke disk, dan mengirimnya ke suatu tempat yang bisa Anda akses nanti. Ketika fungsi crash, Anda memiliki snapshot beku dari dunia yang memproduksinya.

Berikut implementasi Python yang berfungsi:

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

Terapkan pada fungsi yang sedang crash:

@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}

Ketika KeyError: "customer_id" terjadi di produksi, Anda mendapatkan file JSON yang terlihat seperti ini:

{
  "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": "..."
}

Argumen pertama adalah dictionary kosong. Itulah seluruh bugnya. Pemanggil hulu melewatkan {} alih-alih record pelanggan.

Anda sekarang memiliki kasus test:

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

Test ini gagal sebelum perbaikan dan lolos setelah Anda menambahkan validasi input. Lebih penting lagi, Anda tidak perlu menebak. Crash memberi tahu Anda persis apa yang harus diuji.

Mengapa capture melewatkan dependency yang tidak bisa dilihatnya

Pola ini menangkap argumen fungsi, bukan state global. Jika generate_invoice membaca dari database, memanggil API eksternal, atau memeriksa environment variable, nilai-nilai itu tidak ada di args dan kwargs. Replay hanya akan berfungsi jika lingkungan lokal Anda kebetulan cocok.

Anda bisa memperluas decorator untuk secara eksplisit menangkap dependency eksternal:

@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]
    # ...

Tapi db_conn adalah objek koneksi. Anda tidak bisa menyerialisasinya ke JSON. Yang bisa Anda serialization adalah query dan hasilnya. Pendekatan yang lebih prinsipil adalah memisahkan logika murni dari side effect. Lewatkan hasil query sebagai argumen, bukan koneksi:

@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),
    }

Ini adalah functional core, imperative shell. Ini membuat capture menjadi trivial karena tidak ada state tersembunyi. Setiap input yang penting ada di daftar argumen.

Masalah non-determinisme yang tidak bisa Anda hapus dengan capture

Bahkan dengan capture input yang sempurna, beberapa crash tidak bisa direproduksi. Race condition bergantung pada timing thread. Memory corruption bergantung pada allocator state. API eksternal mengembalikan data berbeda di setiap pemanggilan. Random number generator menghasilkan urutan berbeda kecuali Anda memberi seed.

Jika crash Anda adalah race condition, memutar ulang input yang sama pada test lokal single-thread tidak akan memicunya. Anda memerlukan pola konkurensi yang sebenarnya, yang berarti menjalankan thread asli, yang berarti Anda telah berpindah dari “replay” ke “distributed tracing plus load testing.” Itu adalah tool yang berbeda.

Untuk kebanyakan bug tingkat aplikasi, input replay sudah cukup. Untuk heisenbugs, tidak. Ketahui yang mana yang Anda hadapi sebelum menghabiskan tiga jam mencoba memutar ulang masalah timing.

Membuat capture operasional di produksi

Decorator di atas menulis ke disk lokal. Di produksi, Anda ingin capture ini dikirim ke object storage atau error tracker Anda. Integrasinya mudah: ganti panggilan open(capture_path, "w") dengan unggahan S3 atau lampiran pada issue Sentry Anda.

Kebanyakan tim harus mulai dengan satu fungsi. Pilih service yang paling sering crash. Tambahkan decorator. Tunggu crash berikutnya. Ketika tiba, Anda akan memiliki payload JSON yang mengubah sesi menebak tiga puluh menit menjadi penulisan test lima menit.

Jika Anda sudah menggunakan Sentry, fitur Breadcrumbs menangkap sebagian konteks ini secara otomatis. Untuk pendekatan kustom, pola ini muat dalam tiga puluh baris Python dan berfungsi di runtime apa pun yang mendukung decorator atau middleware.

Tangkap satu endpoint minggu ini

Tambahkan capture ke endpoint dengan error tertinggi Anda minggu ini. Bukan setiap endpoint. Bukan setiap fungsi. Satu. Ketika ia crash, tulis kasus test dari capture sebelum Anda memperbaiki bug. Jalankan test, saksikan gagal, terapkan perbaikan, saksikan lolos.

Loop itu, dari crash produksi ke kasus test yang bisa direproduksi, adalah apa yang membedakan debugging dari arkeologi.