Diagnosi + fix dati Rails produzione: kamal exec, `?` larghezza piena, Base64, continuità thread.
I 400/500 in produzione non assomigliano per niente al locale. In locale rilanci i test, editi, rilanci. In produzione, un utente ha già cliccato "Paga" e sta fissando una schermata bianca.
Tre strade:
rails console, le tue modifiche non hanno audit, un errore di digitazione e sei nei guaiQuesto articolo tratta la terza strada. Due casi reali: un ? a larghezza piena infilato nei credentials che ha rotto i pagamenti x402 con 500, e un tweet a metà thread il cui status: :failed va riprovato mantenendo la continuità del thread. Strada facendo, Claude cercherà di sbagliare in 4 direzioni diverse per default — bisogna intercettare ognuna.
kamal app exec --reuseKamal è il tool di deploy Rails di 37signals. Ha un comando:
kamal app exec --reuse 'bin/rails runner "..."'
--reuse significa: non tirare su un nuovo container, esegui dentro il container web già in esecuzione. Niente nuovo build, niente docker pull, niente restart, niente re-iniezione di ENV. Il comando gira, il container torna a gestire le richieste.
L'output scorre via stdout fino al tuo terminale — in sostanza un puts dentro una console Rails di produzione, senza SSH, tmux né lasciare il portatile.
Sessione tipica:
$ 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
Andata e ritorno in 3-6 secondi. Due ordini di grandezza più veloce di un redeploy.
? a larghezza piena che ha causato 500 in produzioneCommit eba9ac9.
Sintomo: Poche ore dopo aver rilasciato i pagamenti x402, ogni richiesta di pagamento tornava 500. Log pieni di Net::HTTPBadResponse dal client HTTP. In locale, perfetto.
Diagnosi: Fai stampare a Claude la config x402 in produzione prima:
kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'
Output:
"0xAbC123...def?"
C'è un ? di troppo in coda — un punto interrogativo a larghezza piena (U+FF1F), non il ? a mezza larghezza. L'IME di qualcuno è scattato durante un'edit di config/credentials/production.yml.enc, e quel carattere si è infilato dentro.
Il config/credentials.yml.enc locale (decrittato con la master.key dev/test) non ce l'ha — in Rails 8 production e dev sono credentials cifrati separati, contenuti non condivisi.
Fix: Non puoi fare SSH ed editare il file direttamente (cifrato) né scaricarlo e editarlo in locale (la master.key non è sul portatile). La mossa è far scrivere a Claude uno script Ruby usa-e-getta e iniettarlo via EDITOR= in credentials:edit:
# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# Rimuovi il punto interrogativo a larghezza piena in fondo
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
Flusso di credentials:edit: decritta → scrivi su file temp → invoca $EDITOR → ricritta → cancella temp. Cambiare EDITOR con il nostro script Ruby automatizza l'edit, senza bisogno di guardare il cifrato in locale.
Dopo, git commit + kamal deploy una volta — questo deploy è obbligatorio perché production.yml.enc è cambiato. Ma la diagnosi non è costata un deploy.
Regola: Quando la produzione si rompe, leggi prima con kamal app exec --reuse. Non tirare a indovinare, non redeployare per primo.
Sintomo: Dopo la pubblicazione di un articolo, i tweet entrano nella tabella x_queue_tweets. Uno di loro — il 2° di un thread di 4 — finisce con status: :failed (rate limit dell'API X, validazione contenuto, qualsiasi cosa). Riprovare richiede di preservare la continuità del thread col 1° tweet.
Trovare il fallito:
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
"'
Risulta essere id=87, thread_id=15, thread_position=2.
Trappola 1: escape della shell. Il contenuto dei tweet spesso ha virgolette, a capo, backtick. Se scrivi:
# salta tutto — la shell mangia virgolette e backslash
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'
Il balletto Base64 — codifica in locale, passa la stringa base64, decodifica dentro il runner:
# Codifica in locale
echo -n 'Contenuto del tweet riscritto...' | base64
# => Q29udGVudXRvIGRlbCB0d2VldCByaXNjcml0dG8uLi4=
# Passa
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
t.update!(content: Base64.decode64('Q29udGVudXRvIGRlbCB0d2VldCByaXNjcml0dG8uLi4='), status: :scheduled)
puts t.status
\""
Le stringhe Base64 sono solo ASCII, sicure per la shell.
Trappola 2: continuità del thread. XQueue::PostTweetJob.perform_later(87) pubblica un tweet autonomo — non si concatena al tweet #1 — perché l'API X ha bisogno di reply_to_tweet_id, e il Job per default non lo porta.
Trovare l'x_tweet_id del tweet precedente (gli invii andati a buon fine riempiono questo campo):
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
Accoda col target di reply:
kamal app exec --reuse 'bin/rails runner "
XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
puts \"enqueued\"
"'
Il polling_interval del worker è 0,1 secondi — pesca il job quasi subito. Qualche secondo dopo, kamal app exec per vedere lo status passare da scheduled a posted e x_tweet_id riempito — thread continuo.
Regola: Le operazioni sui dati in produzione devono rispettare i vincoli a livello business, non solo "record aggiornato con successo". La continuità del thread è un vincolo business; Rails runner non la controlla per te.
Facendo questo tipo di lavoro, il primo istinto di Claude è spesso sbagliato. Pesca ognuna:
"Mi collego in SSH e do un'occhiata..."
Reindirizza: kamal app exec --reuse batte SSH — dentro il container, env Rails caricato, auditato (i log di kamal tengono traccia), non tocca la shell dell'host, niente drift del container (reuse garantisce la versione di produzione attuale).
"Scrivo una migration che rimuove il
?a larghezza piena da wallet_address..."
Reindirizza: Cambiare un valore di credentials non ha bisogno di migration (il DB non è stato toccato). Un Rails runner usa-e-getta lo fa in 10 secondi; una migration richiede un deploy e resta nello schema per sempre. Usa migration solo se la correzione potrebbe ripresentarsi; un errore di battitura è una cosa unica.
"Mi basta chiamare
update!(content: "...")..."
Reindirizza: Qualsiasi contenuto generato dall'utente (tweet, commenti, markdown inserito) deve passare per Base64. Il parsing shell di virgolette, backslash, $ e backtick è un campo minato classico — la produzione è il posto sbagliato per fare pratica.
perform_later senza parametri business"Rilancio con
PostTweetJob.perform_later(87)..."
Reindirizza: Chiedi prima "questo record è in relazione con altri?" I thread hanno relazioni reply_to, i job batch hanno batch_id, i job paginati hanno cursor — la lista degli argomenti di un Job è il portatore di quelle relazioni business. Salti un argomento, rompi una catena.
Debuggare + mutare dati Rails in produzione con Claude — 6 regole:
kamal app exec --reuse 'bin/rails runner "puts X"'. Localizza il problema prima di cambiare qualunque cosa.kamal app exec --reuse è lo strumento di default, né SSH né redeploy. Nel container, Rails caricato, andata e ritorno 3-6 secondi.EDITOR=ruby-script bin/rails credentials:edit --environment production. Lo script Ruby fa l'edit, senza guardare cifrati in locale.echo -n 'X' | base64 in locale, Base64.decode64 nel runner.Debuggare in produzione non è trapiantare il tuo flusso di dev — non hai spazio per iterare, non hai tolleranza all'errore. Quello che usi davvero è la superficie di introspezione che la produzione già offre (Rails runner + kamal exec + credentials:edit), ogni passo facendo il cambiamento più piccolo possibile. Claude può scrivere Ruby corretto — ma sapere "cosa si può fare in diretta vs. cosa va diagnosticato prima" è una decisione che non prende al posto tuo. Quella è la tua disciplina di produzione.