Free

Debug Rails Produksi Pakai Claude, Tanpa SSH atau Redeploy

Diagnosis + fix data Rails produksi: kamal exec, `?` fullwidth, Base64, kontinuitas thread.


Produksi 400/500 sama sekali gak mirip lokal. Di lokal lo jalanin test ulang, edit, jalanin lagi. Di produksi, user udah klik "Bayar" dan lagi nontonin layar kosong.

Tiga jalur:

  1. SSH ke server — bisa, tapi shell di dalam container mungkin gak ada rails console, perubahan lo gak ke-audit, salah ketik sekali lo mati
  2. Redeploy — push fix, minimal 10 menit, mungkin sekejap gak available, dan banyak masalah bukan masalah kode (data atau credentials yang salah)
  3. Jalanin Rails runner di dalam container yang lagi jalan — tanpa restart, tanpa redeploy, tanpa SSH, presisi level pisau bedah

Artikel ini soal jalur ketiga. Dua kasus nyata: fullwidth yang nyantol di credentials sampe pembayaran x402 jadi 500, dan tweet di tengah thread yang status: :failed harus dikirim ulang sambil jaga kontinuitas thread. Di sepanjang jalan, Claude bakal coba salah arah di 4 arah berbeda by default — lo harus intercept satu-satu.


Alatnya: kamal app exec --reuse

Kamal adalah tool deploy Rails dari 37signals. Dia punya command:

kamal app exec --reuse 'bin/rails runner "..."'

--reuse artinya: jangan bangunin container baru, eksekusi di dalam container web yang lagi jalan. Tanpa build baru, tanpa docker pull, tanpa restart, tanpa re-injeksi ENV. Command jalan, container lanjut nangani request.

Outputnya nyampe stdout lo lewat terminal — efektif puts di dalam Rails console produksi, tanpa SSH, tmux, atau ninggalin laptop.

Sesi tipikal:

$ kamal app exec --reuse 'bin/rails runner "puts User.count"'
Launching command with version abc123 from existing container...
  INFO [ok] Finished in 3.8 seconds with exit status 0
App Host: deploy.how2claude.com
12847

Bolak-balik 3-6 detik. Dua orde magnitudo lebih cepet dari redeploy.

Kasus 1: Fullwidth yang Bikin 500 di Produksi

Commit eba9ac9.

Gejala: Beberapa jam setelah rilis pembayaran x402, tiap request pembayaran jadi 500. Log penuh Net::HTTPBadResponse dari HTTP client. Lokal jalan sempurna.

Diagnosis: Suruh Claude print konfigurasi x402 di produksi dulu:

kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'

Output:

"0xAbC123...def?"

Ada ekstra di ujung — tanda tanya fullwidth (U+FF1F), bukan ? halfwidth. IME seseorang switch pas lagi edit config/credentials/production.yml.enc, karakternya keselip masuk.

config/credentials.yml.enc lokal (yang didekrip pake master.key dev/test) gak punya ini — production sama dev di Rails 8 adalah encrypted credentials terpisah, kontennya gak sharing.

Fix: Lo gak bisa SSH dan edit file langsung (udah dienkripsi) dan lo gak bisa pull ke lokal dan edit (master.key gak ada di laptop). Jalannya adalah Claude nulis script Ruby sekali pakai dan inject via EDITOR= ke credentials:edit:

# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# Hilangin tanda tanya fullwidth di ujung
content.gsub!(/(wallet_address: 0x[0-9a-fA-F]+)?\s*$/, '\1')
File.write(ARGV[0], content)
EDITOR="ruby script/fix_prod_wallet.rb" \
  bin/rails credentials:edit --environment production

Alur credentials:edit: dekrip → tulis ke file temp → panggil $EDITOR → re-enkrip → hapus temp. Tuker EDITOR pake script Ruby kita dan edit teratomatisasi, gak perlu melototin ciphertext di lokal.

Setelah itu, git commit + kamal deploy sekali — deploy ini wajib karena production.yml.enc berubah. Tapi diagnosis gak makan deploy sama sekali.

Aturan: Pas produksi rusak, baca dulu pake kamal app exec --reuse. Jangan nebak, jangan redeploy duluan.

Kasus 2: Retry XQueue::Tweet Gagal di Tengah Thread

Gejala: Setelah artikel publish, tweet masuk ke tabel x_queue_tweets. Salah satunya — yang ke-2 dari thread 4 — berakhir status: :failed (rate limit X API, validasi konten, apapun). Retry-nya butuh jaga kontinuitas thread sama tweet ke-1.

Cari yang gagal:

kamal app exec --reuse 'bin/rails runner "
  XQueue::Tweet.where(status: :failed).each do |t|
    puts \"#{t.id}: thread=#{t.thread_id} pos=#{t.thread_position} content=#{t.content.inspect}\"
  end
"'

Ternyata id=87, thread_id=15, thread_position=2.

Jebakan 1: escape shell. Konten tweet sering ada kutipan, newline, backtick. Kalo lo nulis:

# meledak — shell makan kutipan dan backslash
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'

Tarian Base64 — encode di lokal, lempar string base64, decode di dalam runner:

# Encode lokal
echo -n 'Konten tweet yang ditulis ulang...' | base64
# => S29udGVuIHR3ZWV0IHlhbmcgZGl0dWxpcyB1bGFuZy4uLg==

# Kirim
kamal app exec --reuse "bin/rails runner \"
  t = XQueue::Tweet.find(87)
  t.update!(content: Base64.decode64('S29udGVuIHR3ZWV0IHlhbmcgZGl0dWxpcyB1bGFuZy4uLg=='), status: :scheduled)
  puts t.status
\""

String Base64 itu ASCII-only, aman buat shell.

Jebakan 2: kontinuitas thread. XQueue::PostTweetJob.perform_later(87) ngepost tweet sendirian — gak nyambung ke tweet #1 — karena X API butuh reply_to_tweet_id, dan Job default-nya gak bawa ini.

Cari x_tweet_id tweet sebelumnya (yang udah terkirim sukses ngisi field ini):

kamal app exec --reuse "bin/rails runner \"
  t = XQueue::Tweet.find(87)
  prev = XQueue::Tweet.where(thread_id: t.thread_id, thread_position: t.thread_position - 1).first
  puts 'prev x_tweet_id: ' + prev&.x_tweet_id.to_s
\""
# => prev x_tweet_id: 1834567890123456789

Enqueue sambil bawa target reply:

kamal app exec --reuse 'bin/rails runner "
  XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
  puts \"enqueued\"
"'

polling_interval worker adalah 0.1 detik — nge-pickup job hampir instan. Beberapa detik kemudian, kamal app exec buat cek status flip dari scheduled ke posted dan x_tweet_id keisi — thread tersambung.

Aturan: Operasi data produksi harus ngormatin constraint layer bisnis, bukan cuma "record berhasil diupdate". Kontinuitas thread itu constraint bisnis; Rails runner gak ngecek itu buat lo.

4 Arah yang Claude Salahin By Default (dan Gimana Arahin Ulang)

Pas ngerjain kerjaan kayak gini, insting pertama Claude sering salah. Tangkep satu-satu:

1. Pengin SSH ke server

"Gue SSH dulu ke server buat liat..."

Arahin ulang: kamal app exec --reuse ngalahin SSH — di dalam container, env Rails udah ter-load, ter-audit (log kamal nyimpen record), gak nyentuh shell host, gak ada container drift (reuse ngejamin versi produksi saat ini).

2. Pengin nulis migration buat benerin data

"Gue tulis migration buat ngapus fullwidth dari wallet_address..."

Arahin ulang: Nukar satu nilai credentials gak butuh migration (DB gak disentuh). Rails runner sekali pakai selesai dalam 10 detik; migration butuh deploy dan hidup di schema selamanya. Pake migration cuma kalo fix-nya bisa aja mesti diulang; typo itu urusan sekali jadi.

3. Nyelipin karakter spesial ke string shell

"Tinggal update!(content: "...") aja..."

Arahin ulang: Konten apapun yang dibuat user (tweet, komen, markdown input user) harus lewat Base64. Parse shell buat kutipan, backslash, $, dan backtick itu ranjau klasik — produksi bukan tempat buat latihan.

4. perform_later tanpa parameter bisnis

"Tinggal PostTweetJob.perform_later(87) lagi..."

Arahin ulang: Tanya dulu "record ini nyambung ke yang lain gak?" Thread punya relasi reply_to, batch job punya batch_id, paginated job punya cursor — daftar argumen Job itu pembawa relasi-relasi itu. Skip satu argumen, putus satu rantai.

Checklist

Debug + mutasi data Rails produksi pake Claude — 6 aturan:

  1. Baca sebelum nulis. kamal app exec --reuse 'bin/rails runner "puts X"'. Lokalisir masalah sebelum ubah apapun.
  2. kamal app exec --reuse itu alat default, bukan SSH, bukan redeploy. Di container, Rails ter-load, bolak-balik 3-6 detik.
  3. Perubahan credentials via EDITOR=ruby-script bin/rails credentials:edit --environment production. Script Ruby ngerjain edit-nya, tanpa melototin ciphertext lokal.
  4. Escape shell itu ranjau; routing konten spesial lewat Base64. echo -n 'X' | base64 di lokal, Base64.decode64 di runner.
  5. Rails runner gak maksain constraint bisnis buat lo. reply_to thread, batch_id, cursor pagination — enqueue sama semuanya.
  6. Tulis migration cuma kalo "ini bisa kejadian lagi". Fix data sekali-pake lewat runner, dua orde lebih cepet.

Debug di produksi bukan mindahin alur dev ke produksi — lo gak punya ruang iterasi, lo gak punya toleransi error. Yang beneran lo pake itu surface introspeksi yang udah disediain produksi (Rails runner + kamal exec + credentials:edit), tiap langkah bikin perubahan sekecil mungkin. Claude bisa nulis Ruby yang bener — tapi ngerti "apa yang bisa langsung dilakuin vs harus didiagnosis dulu" itu keputusan yang dia gak buatin buat lo. Itu disiplin produksi lo.