Free

Отлаживаем тихие баги с Claude

Три реальных бага, где клик ничего не делает — Claude промахивался, пока одна фраза в промпте не фиксировала ответ.


Баги бывают двух типов. Те, что бросают ошибку — отдал Claude стек-трейс, и через 30 секунд у тебя ответ. Те, что не бросают — кнопка, которая ничего не делает, страница, которая не двигается, форма, которая молча падает — с этими Claude ошибается с первой попытки. Не потому что тупой. Потому что не видит.

Недавно, собирая флоу оплаты на how2claude, я подряд наступил на три таких бага. Вот разбор полётов и промпт-паттерны, которые я теперь использую для тихих багов.

Баг 1: Знак вопроса полной ширины, спрятавшийся в адресе кошелька

Подключил крипто-платежи x402. Локально работало. Первый клик в продакшене: invalid_string at payTo в консоли. Signing flow даже не стартовал — Zod-схема facilitator'а отклонила запрос раньше.

Кошелёк — адрес 0x... из 42 символов, на глаз идеальный. Попросил Claude проверить поле wallet в config/credentials/production.yml.enc:

w = Rails.application.credentials.dig(:x402, :wallet_address).to_s
puts "length: #{w.length}"
# => 43

43 символа. EVM-адреса — 42. 43-й символ — китайский вопросительный знак полной ширины (U+FF1F), заехал во время копипаста из китайского метода ввода.

Первый скан Claude ничего не отметил — для него это была строка, начинающаяся с 0x и выглядящая правильно. Он не посчитал длину по собственной инициативе. Добавь в промпт: «этот адрес на один символ длиннее ожидаемого — выведи каждый codepoint по отдельности». Вылазит 0xFF1F, дело закрыто.

Баг 2: Кнопка Stripe Checkout не реагировала на клик

Кнопка Subscribe на странице тарифов — кликаешь, страница не двигается. Без ошибок. Вкладка network показывала, что POST уходит, Stripe возвращает 302 на checkout.stripe.com — и потом... ничего.

Сначала поручил Claude проверить контроллер. Логика ок: redirect_to session.url, allow_other_host: true. JS — ни одного релевантного listener'а.

Наконец заметил заголовок ответа: Content-Type: text/vnd.turbo-stream.html. Turbo перехватывал submit у button_to как Turbo Stream запрос, а Turbo Stream не идёт за 302 с другого origin — редирект проглатывался, и страница молча оставалась на месте.

Фикс:

<%= button_to "Subscribe", ..., data: { turbo: false } %>

Тот же баг через месяц снова ударил по кнопке Google OAuth. Перехватчики уровня фреймворка — плодородная почва для тихих багов. Claude по умолчанию рассуждает линейно «запрос/ответ» и не полезет искать промежуточный слой, переписавший семантику. Добавь в промпт: «пройди по каждому перехватчику уровня фреймворка, через который идёт этот клик — перечисли каждый middleware/JS-слой, обрабатывающий этот запрос на пути браузер → сервер → браузер».

Баг 3: Переключатель Monthly/Yearly не реагировал

Stimulus-контроллер для переключения месячного/годового тарифа на странице цен — жмёшь кнопку, ничего не переключается. Метод контроллера срабатывал (подтвердил console.log), но this.monthlyTarget был undefined.

Первая догадка Claude: опечатка в имени target. Не было. data-pricing-target="monthly" был в DOM.

Проблема была в scope. data-controller="pricing" висел на контейнере кнопки переключения, а две секции грида лежали снаружи этого контейнера. Stimulus ищет target только в поддереве элемента контроллера; внешние для него не существуют. Поднял data-controller на <section>, оборачивающий всё — починилось.

Этот баг прямо кричит «код правильный» — все имена совпадают, все атрибуты на месте, просто фича не работает. Claude по умолчанию читает код построчно, он не будет визуализировать структуру DOM по своей инициативе. Добавь в промпт: «нарисуй дерево предков и потомков элемента с data-controller='pricing' — отметь, какие data-pricing-target попадают в поддерево, а какие нет».

Три промпт-паттерна для тихих багов

Три бага снаружи выглядели одинаково: клик, ничего не происходит, без ошибок. Claude каждый раз первой догадкой промахивался, и каждый раз одна дополнительная фраза в промпте фиксировала ответ. Общий паттерн:

1. Скажи ему количественную разницу между ожиданием и реальностью — не просто «неправильно»

Не «с адресом кошелька проблема», а «он на один символ длиннее ожидаемого».
Не «кнопка не работает», а «ответ 302, но браузер за ним не пошёл».
Не «переключатель сломан», а «метод контроллера срабатывает, но target undefined».

Чем уже дельта, тем уже пространство поиска у Claude.

2. Направь его на невидимые слои — фреймворк, браузер, кодировка

Тихие баги почти никогда не живут в твоём бизнес-коде. Они живут в Turbo, в scope Stimulus, в кодировке символов, в CSP, в CORS, в service worker'ах. По умолчанию Claude читает твой код. Скажи ему явно: иди и посмотри в эти другие слои.

3. Проси промежуточное состояние, а не выводы

«Выведи каждый codepoint.» «Перечисли заголовки ответа.» «Сделай дамп поддерева DOM.» Материализуй промежуточное состояние вместо того чтобы просить Claude дорассудить до ответа. «Тихая» часть тихого бага — это то, что в цепочке рассуждений есть один шаг со скрытым предположением, которое не выполняется. Материализация промежуточного состояния — это способ заставить это предположение всплыть наружу.


Ошибки проверяют, что Claude знает. Тихие баги проверяют качество сигнала, который ты ему даёшь. Чем конкретнее — тем быстрее он находит ответ.