Diagnostic + correction de données Rails en prod : kamal exec, `?` pleine largeur, Base64, continuité de thread.
Les 400/500 en production n'ont rien à voir avec le local. En local, tu relances les tests, tu édites, tu relances. En production, un utilisateur a déjà cliqué sur « Payer » et regarde un écran blanc.
Trois voies :
rails console, tes changements n'ont pas d'audit, une faute de frappe et tu plongesCet article traite de la troisième voie. Deux cas réels : un ? pleine largeur coincé dans les credentials qui a cassé les paiements x402 avec des 500, et un tweet au milieu d'un thread dont le status: :failed doit être réessayé tout en préservant la continuité du thread. En chemin, Claude va essayer de partir dans 4 mauvaises directions par défaut — il faut intercepter chacune.
kamal app exec --reuseKamal est l'outil de déploiement Rails de 37signals. Il a une commande :
kamal app exec --reuse 'bin/rails runner "..."'
--reuse signifie : ne pas lancer un nouveau conteneur, exécuter dans le conteneur web en cours d'exécution. Pas de nouveau build, pas de docker pull, pas de redémarrage, pas de réinjection d'ENV. La commande s'exécute, le conteneur reprend le traitement des requêtes.
La sortie arrive sur stdout jusqu'à ton terminal — c'est l'équivalent d'un puts dans une console Rails de prod, sans SSH, tmux, ni quitter le portable.
Session type :
$ 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
Aller-retour en 3-6 secondes. Deux ordres de grandeur plus rapide qu'un redéploiement.
? pleine largeur qui a provoqué des 500 en productionCommit eba9ac9.
Symptôme : Quelques heures après la mise en ligne des paiements x402, chaque requête de paiement finissait en 500. Les logs étaient pleins de Net::HTTPBadResponse du client HTTP. En local, nickel.
Diagnostic : Faire afficher à Claude la config x402 en prod d'abord :
kamal app exec --reuse 'bin/rails runner "puts X402.configuration.wallet_address.inspect"'
Sortie :
"0xAbC123...def?"
Il y a un ? de trop en fin — un point d'interrogation pleine largeur (U+FF1F), pas le ? demi-largeur. L'IME de quelqu'un a basculé pendant une édition de config/credentials/production.yml.enc, et ce caractère a glissé dedans.
Le config/credentials.yml.enc local (déchiffré avec la master.key dev/test) ne l'a pas — en Rails 8, production et dev sont des credentials chiffrés séparés, les contenus ne sont pas partagés.
Correctif : Impossible de SSH pour éditer le fichier directement (chiffré) ou de le rapatrier pour éditer en local (la master.key n'est pas sur ton portable). La solution est de faire écrire un script Ruby jetable à Claude et de l'injecter via EDITOR= dans credentials:edit :
# script/fix_prod_wallet.rb
content = File.read(ARGV[0])
# Supprimer le point d'interrogation pleine largeur en fin
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
Flux de credentials:edit : déchiffrer → écrire dans un fichier temporaire → invoquer $EDITOR → rechiffrer → supprimer le temp. Remplacer EDITOR par notre script Ruby automatise l'édition, pas besoin de regarder le chiffré en local.
Ensuite, git commit + kamal deploy une fois — ce deploy est obligatoire parce que production.yml.enc a changé. Mais le diagnostic n'a pas coûté un deploy.
Règle : Quand la prod casse, lire d'abord avec kamal app exec --reuse. Ne pas deviner, ne pas redéployer en premier.
Symptôme : Après publication d'un article, les tweets entrent dans la table x_queue_tweets. L'un d'eux — le 2e d'un thread de 4 — finit avec status: :failed (rate limit X API, validation de contenu, peu importe). Le rejouer demande de préserver la continuité avec le 1er tweet du thread.
Trouver l'échec :
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
"'
Ça donne id=87, thread_id=15, thread_position=2.
Piège 1 : échappement shell. Les contenus de tweets ont souvent des guillemets, des retours à la ligne, des backticks. Si tu écris :
# ça explose — le shell bouffe les guillemets et antislashs
kamal app exec --reuse 'bin/rails runner "t = XQueue::Tweet.find(87); t.update!(content: \"...\")"'
La danse Base64 — encoder en local, passer une chaîne base64, décoder dans le runner :
# Encoder en local
echo -n 'Contenu du tweet réécrit...' | base64
# => Q29udGVudSBkdSB0d2VldCByw6nDqWNyaXQuLi4=
# Passer
kamal app exec --reuse "bin/rails runner \"
t = XQueue::Tweet.find(87)
t.update!(content: Base64.decode64('Q29udGVudSBkdSB0d2VldCByw6nDqWNyaXQuLi4='), status: :scheduled)
puts t.status
\""
Les chaînes Base64 sont ASCII pur, sûres pour le shell.
Piège 2 : continuité du thread. XQueue::PostTweetJob.perform_later(87) publie un tweet isolé — ne se chaîne pas au tweet #1 — parce que l'API X attend reply_to_tweet_id, et le Job ne le transporte pas par défaut.
Trouver le x_tweet_id du tweet précédent (les envois réussis remplissent ce champ) :
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
Mettre en file avec la cible de reply :
kamal app exec --reuse 'bin/rails runner "
XQueue::PostTweetJob.perform_later(87, reply_to_tweet_id: \"1834567890123456789\")
puts \"enqueued\"
"'
Le polling_interval du worker est de 0,1 seconde — il récupère le job quasi instantanément. Quelques secondes plus tard, kamal app exec pour voir le status passer de scheduled à posted et x_tweet_id rempli — thread continu.
Règle : Les opérations de données en prod doivent respecter les contraintes métier, pas seulement « enregistrement mis à jour avec succès ». La continuité du thread est une contrainte métier ; Rails runner ne la vérifie pas pour toi.
Dans ce genre de travail, la première intuition de Claude est souvent fausse. Chope chacune :
« Je me connecte en SSH pour jeter un œil... »
Redirige : kamal app exec --reuse bat SSH — dans le conteneur, env Rails chargé, audité (les logs kamal gardent trace), ne touche pas au shell hôte, pas de dérive de conteneur (reuse garantit la version prod actuelle).
« J'écris une migration pour retirer le
?pleine largeur de wallet_address... »
Redirige : Changer une valeur de credentials n'a pas besoin de migration (la DB n'a pas été touchée). Un Rails runner jetable le fait en 10 secondes ; une migration exige un deploy et reste dans le schema pour toujours. N'utilise une migration que si la correction pourrait se rejouer ; une faute de frappe, c'est one-shot.
« J'appelle juste
update!(content: "...")... »
Redirige : Tout contenu généré par l'utilisateur (tweets, commentaires, markdown saisi) doit passer par Base64. Le parsing shell des guillemets, antislashs, $ et backticks est un champ de mines classique — la prod n'est pas l'endroit pour s'entraîner.
perform_later sans paramètres métier« Je relance juste
PostTweetJob.perform_later(87)... »
Redirige : Pose-toi d'abord la question : « cet enregistrement est-il en relation avec d'autres ? » Les threads ont des relations reply_to, les jobs par lot des batch_id, les jobs paginés des cursor — la liste des arguments du Job est le porteur de ces relations métier. Zappe un argument, casse une chaîne.
Débugger + muter des données Rails en prod avec Claude — 6 règles :
kamal app exec --reuse 'bin/rails runner "puts X"'. Localise le problème avant de changer quoi que ce soit.kamal app exec --reuse est l'outil par défaut, ni SSH ni redéploiement. Dans le conteneur, Rails chargé, aller-retour 3-6 secondes.EDITOR=ruby-script bin/rails credentials:edit --environment production. Le script Ruby fait l'édition, pas besoin de regarder le chiffré en local.echo -n 'X' | base64 en local, Base64.decode64 dans le runner.Débugger en prod, ce n'est pas transplanter ton flux de dev — tu n'as pas de marge d'itération, pas de tolérance à l'erreur. Ce que tu utilises réellement, c'est la surface d'introspection que la prod offre déjà (Rails runner + kamal exec + credentials:edit), chaque étape faisant le plus petit changement possible. Claude peut écrire du Ruby correct — mais savoir « ce qui peut se faire directement vs ce qu'il faut diagnostiquer d'abord » est un arbitrage qu'il ne fera pas à ta place. C'est ta discipline de prod.