Tiga bug nyata di mana klik tak bereaksi — Claude selalu salah sampai satu kalimat tambahan di prompt menguncinya.
Bug ada dua jenis. Yang melempar error — kasih stack trace-nya ke Claude dan dalam 30 detik kamu punya jawabannya. Yang tidak — tombol yang tidak melakukan apa-apa, halaman yang tidak bergerak, form yang gagal diam-diam — ini yang Claude sering salah pada percobaan pertama. Bukan karena bodoh. Karena dia tidak bisa lihat.
Saya kena tiga bug seperti ini berturut-turut saat membangun alur pembayaran how2claude. Berikut post-mortem-nya, dan pola prompt yang sekarang saya pakai untuk bug senyap.
Saya menyambungkan pembayaran kripto x402. Lokal berjalan mulus. Klik pertama di produksi: invalid_string at payTo di console. Signing flow bahkan belum dimulai — skema Zod dari facilitator sudah menolak duluan.
Wallet-nya alamat 0x... 42 karakter, kelihatannya baik-baik saja. Saya minta Claude cek field wallet di config/credentials/production.yml.enc:
w = Rails.application.credentials.dig(:x402, :wallet_address).to_s
puts "length: #{w.length}"
# => 43
43 karakter. Alamat EVM harusnya 42. Karakter ke-43 adalah tanda tanya fullwidth Mandarin ? (U+FF1F) yang masuk saat copy-paste dari input method Mandarin.
Scan pertama Claude tidak menandai apa pun — buat dia string-nya dimulai dengan 0x dan terlihat benar. Dia tidak menghitung panjangnya atas inisiatif sendiri. Tambahkan ini ke prompt: "alamat ini satu karakter lebih panjang dari yang diharapkan — print tiap codepoint-nya." Begitu 0xFF1F muncul, kasus selesai.
Tombol Subscribe di halaman pricing — klik, halaman tidak berpindah. Tanpa error. Tab network menunjukkan POST keluar, Stripe mengembalikan 302 ke checkout.stripe.com — lalu... tidak ada apa-apa.
Saya minta Claude cek controller dulu. Logika-nya benar: redirect_to session.url, allow_other_host: true. JS — tidak ada listener yang relevan.
Akhirnya saya perhatikan header response-nya: Content-Type: text/vnd.turbo-stream.html. Turbo menangkap submit button_to sebagai request Turbo Stream, dan Turbo Stream tidak mengikuti 302 cross-origin — jadi redirect-nya ditelan dan halaman diam-diam tetap di tempat.
Fix:
<%= button_to "Subscribe", ..., data: { turbo: false } %>
Bug yang sama kembali menimpa saya sebulan kemudian di tombol Google OAuth. Interceptor di level framework adalah wilayah subur untuk bug senyap — Claude default-nya berpikir linear request/response dan tidak akan mencari lapisan tengah yang mengubah semantik. Tambahkan ke prompt: "telusuri setiap interceptor level framework yang dilewati klik ini — daftar setiap middleware/lapisan JS yang memproses request ini dari browser → server → browser."
Stimulus controller untuk toggle bulanan/tahunan di halaman pricing — klik tombol, tidak ada yang berubah. Metode controller-nya ter-fire (dikonfirmasi dengan console.log), tapi this.monthlyTarget adalah undefined.
Tebakan pertama Claude: typo di nama target. Ternyata tidak. data-pricing-target="monthly" ada di DOM.
Masalahnya ada di scope. data-controller="pricing" terpasang di container tombol toggle, tapi dua seksi grid-nya berada di luar container itu. Stimulus hanya mencari target di dalam subtree elemen controller; yang di luar tidak ada baginya. Saya pindahkan data-controller ke <section> yang membungkus semuanya — beres.
Bug ini teriak "kode-nya benar" — semua nama cocok, semua atribut ada, fitur-nya saja yang rusak. Claude default-nya membaca kode baris per baris; dia tidak akan memvisualisasikan struktur DOM atas inisiatif sendiri. Tambahkan ke prompt: "gambar pohon leluhur dan keturunan dari elemen dengan data-controller='pricing' — tandai data-pricing-target mana yang berada di dalam subtree dan mana yang di luar."
Ketiga bug terlihat identik dari luar: klik, tidak terjadi apa-apa, tidak ada error. Claude menebak salah tiap kali, dan tiap kali satu kalimat tambahan di prompt mengunci jawabannya. Polanya sama:
1. Beritahu selisih terkuantifikasi antara yang diharapkan dan yang nyata — jangan cuma "ini salah"
Bukan "ada masalah di alamat wallet", tapi "satu karakter lebih panjang dari yang diharapkan".
Bukan "tombol tidak berfungsi", tapi "response-nya 302, tapi browser tidak mengikutinya".
Bukan "toggle rusak", tapi "metode controller-nya fire, tapi target-nya undefined".
Makin sempit selisihnya, makin kecil ruang pencarian Claude.
2. Arahkan dia ke lapisan yang tidak terlihat — framework, browser, encoding
Bug senyap hampir tidak pernah tinggal di kode bisnis kamu. Mereka tinggal di Turbo, di scope Stimulus, di character encoding, di CSP, di CORS, di service worker. Default Claude adalah membaca kode kamu. Suruh dia secara eksplisit melihat lapisan-lapisan lain itu.
3. Minta state perantara, bukan kesimpulan
"Print setiap codepoint." "Daftar header response." "Dump subtree DOM." Materialkan state perantara alih-alih menyuruh Claude menalar sampai ke jawaban. Bagian "senyap" dari bug senyap adalah satu langkah dalam rantai penalaran punya asumsi tersembunyi yang tidak berlaku. Mematerialkan state perantara adalah cara kamu memaksa asumsi itu keluar.
Error menguji apa yang Claude tahu. Bug senyap menguji kualitas sinyal yang kamu berikan padanya. Makin spesifik, makin cepat dia menemukan jawabannya.