Зачем я запускаю smoke‑тесты прямо в production и как они однажды сломали мне аналитику

Стоит сразу сказать: подход, о котором пойдёт речь, довольно специфичный и подойдёт далеко не всем. Но, как показала практика, в нём есть смысл. Да, у моего пет-проекта — мессенджера Pulse — есть автоматические тесты, которые регулярно ходят прямо в прод.
Они авторизуются под настоящими аккаунтами, создают реальные сообщения, проходят через тот же бэк, базу, вебсокет и остальные компоненты, которыми пользуются обычные пользователи (коих немного, отсюда автотестовые и появились).

Звучит как начало статьи «как не надо делать». Но я как раз считаю, что такие тесты нужны.
Потому что зелёный CI отвечает на один вопрос:
работает ли код в окружении, которое мы подготовили для тестирования?
А после выкладки меня интересует немного другой:
работает ли прямо сейчас тот продукт, который получает реальный пользователь?
Это не одно и то же.
Можно иметь идеально зелёный pipeline и одновременно сломанный production: неправильную конфигурацию nginx, недоступный внешний сервис, ошибку миграции, проблемы с сетевым стеком, права на каталог, сертификат, неверный environment variable или десяток других вещей, которые unit- и integration-тесты никогда не увидят.
Поэтому в какой-то момент у Pulse появился отдельный pulse-smoke.
И недавно он довольно эффектно напомнил мне, что production smoke - это тоже настоящий production traffic. Только понял я это, когда решил посмотреть статистику своего же мессенджера.
Зачем вообще тестировать production
Для начала важное уточнение.
Речь не о том, чтобы запускать весь test suite на живой пользовательской базе.
Я не предлагаю делать:
go test ./... --database=productionи надеяться на лучшее. Ни в коем случае так не делайте, если жизнь ваша и вашего продукта дорога.
Production smoke должен отвечать только на несколько критических вопросов.
Например:
можно ли авторизоваться;
работает ли основной API;
устанавливается ли realtime-соединение;
можно ли отправить сообщение;
доходит ли оно до второго пользователя;
корректно ли обновляются состояния;
открываются ли основные сценарии, зависящие сразу от нескольких компонентов системы.
То есть это не проверка всех corner cases.
Это проверка того, что критический пользовательский путь действительно жив после деплоя.
Условно:
Клиент A
|
| login
v
Backend
|
| message
v
PostgreSQL
|
| realtime event
v
WebSocket
|
v
Клиент BЕсли на любом участке этой цепочки прод вдруг где-то по какой-то мифической или не очень причине собран неправильно, смок должен это увидеть.
Именно поэтому запускать такой тест только в тестовой среде для меня было недостаточно.
Тест доказывает работоспособность теста, не более.
А вот продакшн смок доказывает что проверенные критические сценарии в данный момент работаю на продовой среде , что мне кажется все таки более показательно и информативно.
Почему для этого используются настоящие аккаунты
В Пульсе для смоков есть отдельные пользователи.
Сейчас это аккаунты вида:
Smoke Monitor A ...
Smoke Monitor B ...Один изображает первого участника сценария, второй - такого же второго участника.
Это позволяет проверить не абстрактный HTTP 200 OK, а сценарий целиком.
Условно:
Пользак A получает рабочую сессию.
Пользак A отправляет сообщение.
Бек его сохраняет.
Пользак B получает событие.
Состояния сходятся.
Смок убеждается, что ожидаемый результат действительно появился.
Созданные данные очищаются.
То есть если здоровительный ендпоинт отвечает:
{"status":"ok"}а сообщения фактически перестали доставляться - обычная проверка /health будет счастлива.
А вот мониторы - нет.
Для мессенджера это особенно важно. У него слишком много вещей находятся между «сервер отвечает» и «пользователь действительно получил сообщение»:
HTTP
Database
Authorization
Conversation membership
WebSocket
Realtime routing
Delivery state
Read state
Media
Client-visible stateКаждая часть отдельно может выглядеть здоровой, в то время как пользовательский сценарий - может радостно сдохнуть по какой-либо причине.
И всё прекрасно работало
Результаты прогонов складываются на status.pulsehq.ru — я каждый день вижу, какой production-сценарий прошёл или упал.
Smoke запускался. Ошибки обнаруживались. После деплоя можно было быстро понять, что прод действительно успешно пережил обновление. В общем, всё было хорошо и прекрасно...
Пока однажды мне не захотелось посмотреть, как растёт Пульс. У меня маленький проект, поэтому никакой огромной аналитической платформы пока нет и врятли предвидится в ближайшее время, поэтому источником моей истины выступает продовый PostgreSQL.
Первый запрос был очень простой:
SELECT count(*)
FROM messages;Результат:
7381Неплохо. Потом начал смотреть глубже. За последние 30 дней:
2235 сообщенийЗа семь дней:
1052За сутки:
152Я уже начал радоваться. Потом посмотрел отправителей.
И среди лидеров обнаружили:
Smoke Monitor A
Smoke Monitor BПричём далеко не в самом низу списка.
Каждое пятое сообщение оказалось написано тестами
После этого пришлось пересчитать статистику уже нормально.
Взял период с 11 августа до начала октября. Получил:
Всего новых message rows: 2736
Из них production smoke: 581
Удалены обычными пользователями: 7
Чистые пользовательские: 2148То есть примерно 21% новых строк в messages за этот период создали автоматические production smoke-тесты.
Каждое пятое. Это был довольно хороший момент для переоценки фразы:
«у нас столько-то сообщений».
Нет. У нас столько-то строк в таблице messages. Это несколько другая метрика.
Но дальше стало ещё интереснее
В Pulse удаление сообщения — soft delete: строка остаётся в таблице, а deleted_at получает значение:
deleted_at IS NOT NULLПоэтому было решено посмотреть, сколько сообщений удаляют пользователи. За последние 30 дней оказалось:
379 deleted messagesВыглядело любопытно. Неужели пользователи настолько активно редактируют историю?Проверили авторов.
Оказалось, production smoke объясняет 373 из этих 379 удалений. То есть около 98%. Причина довольно простая. Smoke создаёт реальное состояние, проверяет его, а затем выполняет cleanup.С пользовательской точки зрения тестовые сообщения исчезли. С точки зрения PostgreSQL они всё ещё существовали как soft-deleted записи.
Получился отличный пример того, почему:
cleanup != absence of analytical footprintСистема была очищена для пользователя. Но не для аналитика.
Smoke не ломал продукт. Он ломал понимание продукта
Это важное различие. Сами тесты работали правильно. Они не засоряли обычным пользователям список диалогов и не превращали Messenger в свалку тестовых сообщений. Проблема появилась только тогда, когда я стал задавать базе продуктовые вопросы.
Например:
SELECT count(*)
FROM messages;На самом деле означает:
сколько строк существует в
messages?
Но мне очень хочется прочитать ответ как:
сколько сообщений написали пользователи?
Это уже моя ошибка.
А запрос:
SELECT count(*)
FROM messages
WHERE deleted_at IS NOT NULL;отвечает:
сколько сообщений находится в удалённом состоянии?
но совсем не обязательно:
сколько сообщений решили удалить обычные пользователи?
В проде живут не только пользователи.
Там живут ещё:
технические аккаунты;
мониторинг;
синтетический трафик;
миграции;
фоновые процессы;
автоматические проверки.
И всё это может выглядеть абсолютно настоящим. Потому что оно и есть настоящее.
А затем smoke начал портить DAU и MAU
После сообщений я решил посмотреть аудиторию.
Сырая статистика показала:
DAU: 6
WAU: 14
MAU: 22Для небольшого проекта цифры вполне приятные. Но мы то уже помним - два наших Smoke Monitor регулярно заходят в production.
Следовательно, они тоже:
active usersС точки зрения базы всё правильно. После их исключения акков получилось:
DAU: 4
WAU: 12
MAU: 20И это уже была реальная продуктовая статистика. Похожая ситуация произошла с уникальными отправителями. За последние 30 дней база показывала:
15 sendersПосле исключения smoke:
13За последние семь дней:
10 -> 8За сутки:
4 -> 2Последняя цифра особенно хорошо показывает проблему маленьких продуктов.
Если у вас миллион DAU, два смок пользователя ничего визуально не меняют. Если DAU равен шести — они составляют треть аудитории.
Как smoke можно увидеть даже без знания его имени
После этого расследование стало довольно забавным. Группировка сообщений по минутам.
Видится примерно такую картину:
2026-10-04 04:12 Smoke Monitor A 13 messages
2026-10-03 04:12 Smoke Monitor A 13 messages
2026-10-02 04:12 Smoke Monitor A 13 messages
2026-10-01 04:11 Smoke Monitor A 13 messages
2026-09-30 04:10 Smoke Monitor A 13 messages
...Очень дисциплинированный пользователь, едрит мадри его в качель...
Каждый день приходит примерно в четыре утра, быстро отправляет двенадцать-тринадцать сообщений и исчезает. Retention мечты. Конечно, это был наш монитор, послушный и надежный как швейцарские часы :D
И именно здесь мне стало понятно, что синтетический трафик - это, пожалуй, полноценный класс production-данных. Его нельзя считать ошибкой или мусором.
Его нужно уметь классифицировать, определять и отделять. Примерно как мух от котлет...
Что оказалось неправильно в подходе
Сам production smoke я после этого выключать не захотел.
Наоборот.
Я по-прежнему считаю его полезным. Ошибка была скорее в другом: я хорошо отделил тестового пользователя логически, но почти никак не отделили его аналитически.
Имя:
Smoke Monitor A 20260917понятно человеку. Для базы это просто обычная строка username. В итоге аналитический запрос начинает выглядеть так:
WHERE username NOT LIKE 'Smoke Monitor %'Работает? - Да!.
Красиво? - Не очень...
Если завтра аккаунт назовут (гипотетические коллеги, например?):
Production Probe Aаналитика неожиданно снова «вырастет». Намного правильнее иметь явную принадлежность аккаунта.
Например:
account_type = human
account_type = synthetic
account_type = serviceили хотя бы:
is_synthetic = trueТогда продуктовый запрос становится честнее:
WHERE is_synthetic = falseА мониторинг при этом может совершенно спокойно продолжать жить в проде.
Synthetic traffic должен быть first-class citizen
Это, пожалуй, главный вывод всей истории. Если в проде система регулярно генерирует синтетический трафик, это не временный костыль. Это часть продуктовой архитектуры. Значит, ей нужны нормальные свойства, о чем многие часто забывают. Да и я напоролся на эти же грабли, потому что спешил. Итого, что должно быть:
Отдельная identity. Смок конечно не должен работать от имени случайного реального пользователя.
Явная классификация - аналитика должна понимать разницу между человеком и синтетикой.
Ограниченный scope - техюзверь должен иметь ровно те возможности, которые нужны для проверки.
Предсказуемый cleanup - Причём надо понимать разницу между:
user-visible cleanupи
physical database cleanupОтдельные метрики. Но на самом деле такую активность тоже интересно считать.
Например:
smoke runs
smoke success rate
scenario latency
created synthetic messages
cleanup failures
last successful runПросто эти цифры не должны смешиваться с продуктовыми.
Удалять ли тестовые записи физически?
Первой реакцией может быть:
ну так делайте hard delete после теста, проблема решена.
Но мне это решение не очень нравится. Во-первых, жесткое уаление само по себе может обойти тот сценарий, который мы как раз и пытаемся проверять. Во-вторых, след успешного смока иногда полезен для расследования.
Если ночью что-то сломалось, возможность посмотреть:
какие данные создал тест
что успело сохраниться
на каком состоянии остановился сценарийможет быть полезнее идеально стерильной таблицы.
В-третьих, тестовые данные перестают быть проблемой, если система знает, что они тестовые.
Поэтому сейчас мне ближе подход:
не прятать synthetic traffic,
а корректно его маркироватьТогда можно иметь обе картины.
Операционную:
в production прошло 7388 message eventsи продуктовую:
пользователи создали N реальных сообщенийОбе метрики правильные. Они просто отвечают на разные вопросы.
Ещё одна неожиданность: историческая аналитика тоже врёт
Пока мы разбирались со смоками, обнаружился ещё один класс данных.
В один день система показывала огромный всплеск:
613 сообщений
28 отправителей
39 диалоговНа первый взгляд — отличный день.
Почти вирусный рост.
Но у большого количества этих сообщений timestamp оказался абсолютно одинаковым до микросекунды.
Это была миграция старой истории. Когда то давно. Я уже сам не помню, что за миграция это была. Получается, что даже после удаления синтетического трафика нельзя автоматически считать:
GROUP BY created_at::dateграфиком реальной активности пользователей. Иногда created_at отвечает на вопрос:
когда эта строка появилась в нынешней модели данных?
а не:
когда человек совершил действие?
Короче в итоге получено несколько классов событий:
human activity
synthetic production activity
historical import
service activityИ внезапно простая таблица сообщений стала выглядеть гораздо интереснее.
Что теперь считается «реальным сообщением»
Для текущей аналитики Pulse правило получилось примерно таким:
visible message
+
non-synthetic sender
+
non-migration eventВ SQL минимальная версия сейчас выглядит примерно так:
SELECT count(*)
FROM messages m
JOIN users u ON u.id = m.from_user
WHERE m.deleted_at IS NULL
AND u.is_synthetic = false;Поля is_synthetic у нас на момент расследования ещё не было - именно расследование и показало, зачем оно нужно.
До этого приходилось исключать известные smoke identities отдельно. В целом как говорится "и так сойдет" и работать можно, но если подобное условие начинает появляться в каждом аналитическом запросе, значит классификация должна переехать в модель данных.
Значит ли это, что продуктовы смоки — плохая идея?
Для меня — нет. Наоборот, после этой истории я ещё больше уверен, что он нужен. Они поймали уже немало вещей, которые невозможно нормально доказать одним CI. Потому что прод — это не только бинарник.
Production — это:
код
+
конфигурация
+
база
+
миграции
+
reverse proxy
+
TLS
+
сетевые зависимости
+
внешние сервисы
+
runtime stateМожно протестировать каждый компонент отдельно и всё равно получить неработающий продукт в сборе. Production smoke проверяет именно сборку. Но за это приходится платить. Synthetic traffic становится частью реальности production.
И если делать вид, что его не существует, он обязательно всплывёт где-нибудь ещё. У нас он всплыл в продуктовой аналитике.
У кого-то может всплыть:
в billing;
в rate limits;
в fraud detection;
в рекомендациях;
в push-уведомлениях;
в retention;
в audit;
в storage metrics.
Чем серьёзнее и сложнее система, тем важнее отделять тестовый трафик от человеческого не по соглашению, а на уровне модели.
Самая забавная часть
Всё расследование началось вообще не с проблемы. Я просто хотел порадоваться росту своего мессенджера и пытался найти интересные цифры. Все таки 11 августа последний раз данные снимал! Посмотрел:
7381 сообщенийПотом:
1052 за неделюПотом:
379 удалённых за месяцА через некоторое время сидел и вычислял, какая часть моего DAU вообще не является людьми. В результате статистика стала немного скромнее. Зато намного честнее. И автотесты в проде я оставил и буду дальше их развивать. Но это уже другая история...
Подводя итог - А что думаете вы? Нужны ли в проде smoke-тесты, которые создают настоящие данные, если эти данные изолированы от пользовательских сценариев? Или production должен оставаться полностью свободным от синтетики?
Весь описанный цирк происходит внутри моего пет-проекта - Pulse Messenger. Автотесты продолжают работать, а после этого расследования у него, похоже, скоро появится ещё и нормальный признак синтетического идентити.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.