Неделя 124. Невидимые трещины

Разбор этой статьи
Эту тему разобрали в подкасте. Слушай параллельно с чтением.
190 коммитов. 6 репозиториев. Три утечки данных, найденные за одну неделю.

15 декабря 2025 года начался период, который я сейчас называю «тихой охотой». Никакой инфраструктурной революции, никакого «Великого подключения». Просто неделя, когда мы обнаружили, что платформа, которая выглядела рабочей, была полна невидимых трещин. Файлы одного клиента показывались другому. Диалоги бота пересекали границы аккаунтов. Пагинация врала. И всё это работало так месяцами.
Когда файлы показывают не то

15 декабря, понедельник. Рутинная проверка библиотеки файлов. Один пользователь загружает документ. Другой пользователь его видит.
Не потому что система сломалась. Она так работала с самого начала. Запрос файлов не фильтровался по владельцу. Фильтр просто отсутствовал. При пяти пользователях это было незаметно. При растущей базе клиентов это стало опасным.
Фикс занял два коммита. Один на бэкенде: добавить проверку принадлежности файла при каждом запросе. Один на фронтенде: убрать race condition в хуке, который загружал список файлов.

В тот же день обнаружилась вторая утечка. Компонент диалога с ботом показывал данные из чужих разговоров. Причина та же: отсутствие изоляции на уровне запроса. Данные не были защищены, потому что когда писался этот код, в системе был один клиент. Зачем изолировать данные от самого себя?

16 декабря: третья утечка. Эндпоинты аутентификации и клиентских цен были доступны без сервисного токена. Любой, кто знал URL, мог получить данные. Мы закрыли доступ и добавили проверку токенов.
Три утечки за два дня. Ни одна из них не была «багом» в классическом смысле. Все три были следствием одного и того же: код, написанный для одного клиента, не был готов к тому, что клиентов станет много. Однопользовательские допущения, скрытые внутри многопользовательской системы.
Марафон отладки: 20 коммитов на одну таблицу

16 декабря начался марафон, который продолжался два дня.
Таблица заказчиков отображала неправильные данные. Не всегда. Не для всех. Иногда пагинация показывала 100 записей из 300. Иногда сортировка не работала. Иногда фильтр по типу бота возвращал пустой список, хотя данные были.
Мы начали копать. Первый debug-коммит: расширенное логирование для запросов. Второй: логирование сырого ответа от хранилища. Третий: вывод первых десяти идентификаторов для диагностики. Четвёртый, пятый, шестой…
К вечеру 16-го мы нашли первую причину. Промежуточный сервис имел жёсткий лимит: 100 записей на запрос. Бэкенд отправлял 300 идентификаторов, а получал ответ только по первым 100. Пагинация честно считала: «всего записей: 100». А их было 300.

Фикс: разбить запрос на пачки по 100 и собрать результат. Но это решило только часть проблемы.
Фильтр по типу бота не работал из-за несовпадения типов данных. Один сервис отправлял строку, другой ожидал число. Три коммита подряд на починку одного фильтра. Первый не помог. Второй не помог. Третий: универсальное сравнение через приведение типов. Заработало.
17 декабря: ещё один слой. Подсчёт общего количества записей всегда равнялся размеру текущей страницы. Если на странице 20 записей, total = 20. Если 50, total = 50. Пагинатор показывал одну страницу всегда, даже если реально их было десять.
Причина: запрос count использовал те же параметры, что и запрос данных, включая limit. Ограничение «дай мне 20» применялось и к подсчёту: «посчитай, но не больше 20».
20 коммитов. Два дня. Одна таблица. И все три бага жили на границах: между сервисами, между типами данных, между запросом данных и запросом количества.
Гонки данных: версионирование и RAG

Пока шла отладка таблицы заказчиков, параллельно строилась новая фича.
18 декабря мы запустили версионирование инструкций. До этого момента, когда пользователь редактировал текст инструкции для бота, старая версия исчезала. Нажал «Сохранить», и всё. Если новая версия работала хуже, вернуться было некуда.
Теперь каждое сохранение создавало новую версию. Можно посмотреть историю, сравнить, откатить. Простая идея. Непростая реализация.
19 декабря мы обнаружили, что система создаёт дубликаты версий. Одно сохранение порождало две одинаковые записи. Причина: при обновлении вызывались два обработчика, каждый создавал свою версию. Починили проверкой уникальности.
20 декабря добавили режим быстрого улучшения: пользователь нажимает одну кнопку, и система генерирует улучшенную версию инструкции. Один запрос вместо длинного диалога с настройками. К концу недели появился баг: после улучшения интерфейс показывал старый текст. Кэш фронтенда успевал сохранить предыдущую версию быстрее, чем приходила новая. Починили инвалидацией кэша при каждом обновлении.
17 декабря мы нашли два критических бага в RAG. Переменная store_id использовалась до объявления. Код работал только если звёзды сошлись в правильном порядке: нужный документ, нужная последовательность вызовов. Во всех остальных случаях бот падал при попытке найти ответ в базе знаний. Классический баг, невидимый в тестах, потому что тесты всегда шли по «счастливому пути».
Второй баг RAG: race condition при создании нового хранилища. Два одновременных запроса на векторизацию документа создавали два хранилища, и бот потом не знал, в каком искать. Починили блокировкой: один документ, одно хранилище, без исключений.
Стройка без остановки

Параллельно с охотой на баги шло строительство. 14-15 декабря мы запустили библиотеку файлов для системы дожимов.
Дожимы в Botseller работают так: если клиент написал боту, но не дошёл до целевого действия, система отправляет ему сообщение через некоторое время. Раньше это были только текстовые сообщения. Теперь к дожимам можно прикреплять файлы: прайс-листы, каталоги, презентации.
Полная реализация заняла два дня: загрузка файлов, категоризация по расширению, превью, drag & drop для сортировки, интеграция с редактором дожимов. И сразу баг: запрос списка файлов отдавал файлы всех пользователей. Та самая первая утечка, с которой начался понедельник.
К концу недели мы унифицировали реестр ботов. Раньше каждый тип мессенджера обрабатывался по-своему: отдельный код для Telegram, отдельный для WhatsApp, отдельный для каждого следующего канала. 13 типов, 13 разных подходов.
21 декабря: единый паттерн. Один интерфейс для всех типов. Одна точка регистрации. Одна логика отображения. Это не про красоту кода. Это про то, что 14-й тип бота можно будет добавить за день, а не за неделю.
Урок недели: баги живут на границах

Оглядываясь на неделю 124, я вижу закономерность, которую не замечал тогда.
Ни один серьёзный баг той недели не жил внутри одного сервиса. Все они жили на границах.
Утечка файлов: между загрузкой файла (одним сервисом) и выдачей списка (другим). Пагинация: между запросом данных (бэкенд) и ответом хранилища (data service с лимитом 100). Тип бота: между строкой в одном месте и числом в другом. RAG: между созданием хранилища и его использованием.
Принцип невидимых трещин:
Баги прячутся на стыках. Каждый сервис в отдельности работает правильно. Файловый сервис корректно сохраняет файлы. Хранилище данных корректно отдаёт 100 записей. Бэкенд корректно отображает то, что получил. Все правы. А результат неправильный.
Однопользовательские допущения не кричат. Код, написанный для одного клиента, не выбрасывает ошибок когда клиентов становится десять. Он просто тихо отдаёт чужие данные. Нет исключения, нет красной строки в логах. Просто неправильный результат, который кажется правильным.
Отладка на стыках дороже в пять раз. Двадцать коммитов на одну таблицу. Не потому что баг был сложный. Потому что причина была в одном сервисе, симптом в другом, а единственный способ увидеть картину целиком - расставить логи по обе стороны границы.
Что дальше

На следующей неделе, неделе 125, начнётся «Великое подключение». 314 коммитов, 13 репозиториев, деплой трассировки в предновогоднюю пятницу. Трассировка, которая сама создаст баг. HOTFIX, который уронит прод. И платёжные провайдеры для двух стран за два дня.
Но всё это станет возможным только потому, что на неделе 124 мы закрыли невидимые трещины. Изолировали данные. Починили пагинацию. Научили систему корректно считать свои записи. Скучная работа, без которой красивый деплой недели 125 закончился бы катастрофой.
FAQ
Как три утечки данных могли существовать незамеченными?
Все три утечки были следствием однопользовательских допущений: код писался, когда в системе был один клиент. При одном клиенте нет «чужих» данных. Утечки проявляются только при росте базы пользователей, и даже тогда не вызывают ошибок. Система просто тихо показывает данные другого клиента.
Почему на починку одной таблицы ушло 20 коммитов?
Потому что баг был не в одном месте. Пагинация врала из-за лимита промежуточного сервиса (100 записей). Фильтр не работал из-за несовпадения типов данных. Подсчёт общего количества использовал ограничение выборки. Три разных бага в трёх разных сервисах, которые вместе создавали одну проблему: «таблица показывает неправильные данные».
Что такое версионирование инструкций?
Раньше редактирование текста инструкции для бота перезаписывало предыдущую версию. Новая фича сохраняет каждое изменение как отдельную версию: можно посмотреть историю, сравнить версии и откатить к предыдущей, если новая работает хуже.
Почему один сервис отдавал строку, а другой ожидал число?
Типичная проблема микросервисной архитектуры. Сервисы писались в разное время разными разработчиками. Один закодировал тип бота как строку «telegram», другой как число 1. Оба были внутренне согласованы, но на границе между ними возникало несовпадение, и фильтр не находил совпадений.
Что такое RAG и зачем он нужен боту?
RAG позволяет боту искать ответы в загруженных документах: прайс-листах, каталогах, инструкциях. Вместо того чтобы заранее «учить» бота каждому ответу, вы загружаете документ, и бот находит релевантные фрагменты при вопросе клиента.
Сколько типов мессенджеров поддерживал Botseller к декабрю 2025?
13 типов: Telegram, WhatsApp, несколько вариаций каждого мессенджера для разных сценариев интеграции, плюс веб-виджеты и другие каналы. Унификация реестра на этой неделе позволила добавлять новые каналы быстрее.



