Обошёл блокировки в интернет магазине, но чуть не сломал корзину. Пишем автономный мониторинг на Playwright и n8n

от автора

Предыстория: блокировка CDN и первые звоночки

Наш магазин работает на InSales. Как и у большинства российских магазинов на этой платформе, часть системных JS и CSS файлов раздавалась через static.insales-cdn.com.

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

Решение напрашивалось: перестать зависеть от чужого CDN и раздавать критичные файлы через стороннюю, более надёжную инфраструктуру. Я перевёл загрузку системных библиотек на Yandex Cloud Storage/CDN — загрузка стала заметно быстрее и стабильнее, фото и иконки снова грузились без проблем.

Побочный эффект: как обход CDN чуть не сломал корзину

У медали была обратная сторона. У нас на сайте помимо стандартной InSales-вёрстки — куча кастомных виджетов: галерея на Splide, виджет подбора модели, сторис с Lottie-анимациями, а главное — сторонний слушатель корзины. Как только системные файлы стали грузиться с другого источника и в другом порядке, начали вылезать конфликты между системным и кастомным JS: где-то товар добавлялся в корзину дважды на один клик, а где-то ломались скрипты именно на странице корзины.

Руками поправил конечно, всё снова заработало. Но возник очевидный вопрос: а что будет, если InSales в очередной раз обновит системные файлы, поменяет порядок загрузки скриптов или структуру объекта корзины? Мой обходной путь основан на конкретной версии их скриптов — любое изменение в них потенциально может тихо что-то сломать, и я узнаю об этом не из мониторинга, а только из жалоб покупателей, что корзина не работает или сайт не открывается.

Почему не готовые мониторинги

Первая мысль — взять что-то готовое: UptimeRobot для аптайма, Lighthouse CI или PageSpeed для скорости. Но ни то, ни другое не покрывает мою реальную проблему:

  • Аптайм-мониторинги проверяют, что сервер отвечает 200, а у меня сайт может отвечать 200 и при этом иметь сломанную кнопку «в корзину» из-за конфликта скриптов.

  • Lighthouse даёт лабораторные метрики на одной статичной странице, но не умеет проверять транзакционную логику. Клик по кнопке, изменение состояния корзины, повторный клик (у меня отдельная проверка на то, что двойной клик не должен задвоить или затроить товар).

  • Ни один готовый инструмент не знает про специфику InSales, что состояние корзины удобнее и надёжнее читать из window.Cart.order.get(), а не парсить вёрстку, которая может визуально не измениться при изменении данных.

  • Сравнение скорости с конкурентами как внешний сигнал «если у меня стало так же плохо, как у них — значит дело не в блокировках, а в чём-то на моей стороне» — это тоже не то, что даёт универсальный SaaS-мониторинг из коробки.

В итоге решил написать узкоспециализированный инструмент под свою конкретную задачу — но сделать его достаточно универсальным, чтобы им мог воспользоваться кто угодно на похожем стеке.

Проект с полной документацией на GitHub — ecom-tech-speed-checker.

Ежедневный отчёт

Ежедневный отчёт

Архитектура решения

Инструмент — это два независимых сервиса на FastAPI + Playwright плюс n8n-воркфлоу, который их дирижирует и шлёт отчёты в Telegram.

Telegram / Schedule → n8n → health-runner (Playwright, Yandex Cloud Functions)                          → speed-runner  (Playwright, Yandex Cloud Functions)                          → сборка отчёта → Telegram (текст + PNG)

Оба раннера — самостоятельные HTTP-сервисы на FastAPI, упакованные в Docker-образ. У себя я держу их не на постоянно работающем VPS, а как Yandex Cloud Functions на базе контейнерного образа: нагрузка не постоянная (два плановых прогона в сутки плюс редкие ручные вызовы через Telegram), поэтому нет смысла оплачивать сервер, который 99% времени простаивает. Функция поднимается на вызов, гоняет Chromium через Playwright, отдаёт JSON или PNG и гаснет. Из нюансов — холодный старт: первый вызов после простоя ощутимо дольше из-за загрузки Chromium внутри контейнера, это стоит учитывать в таймаутах n8n (timeout: 300000 на HTTP-запросах в воркфлоу не просто так с запасом).

Для тех, кому нужен классический деплой — в репозитории есть docker-compose.example.yml, оба сервиса без изменений в коде разворачиваются и на обычном VPS.

n8n в этой схеме не выполняет никакой логики проверок сам, т.к. он оркестратор: собирает конфиг проекта, дёргает раннеры, ждёт ответа, склеивает результат в отчёт и шлёт в Telegram.

health-runner: что и как проверяем

Это сервис, который гоняет headless Chromium по критичным пользовательским сценариям. Список проверок по умолчанию:

  • открывается ли главная страница, карточка товара, корзина;

  • виден ли слайдер галереи на странице товара;

  • кладётся ли товар в корзину на один клик — и не задваивается ли на быстрый стресс клик (double_click_add_guard отдельная защита от гонки в JS-обработчике);

  • корректно ли добавляется товар с выбранной опцией/аксессуаром;

  • не меняется ли количество при смене опции, если это не предполагается (частый баг — выбор другого варианта товара неожиданно увеличивает qty);

  • нет ли задублированных позиций в корзине;

  • корректны ли количество и сумма в корзине;

  • срабатывает ли попап мотиватора скидки (в карточке товара и перед оформлением);

  • удаляется ли позиция из корзины.

Ключевая механика — сравнение состояния корзины до и после клика через Cart.order.get(), а не через хрупкий парсинг вёрстки, которая визуально может не измениться, даже если данные внутри уже другие:

if (window.Cart && Cart.order && typeof Cart.order.get === 'function') {  const order = Cart.order.get();  const lines = Array.isArray(order.order_lines) ? order.order_lines : [];  result.has_cart_object = true;  result.qty = lines.reduce((sum, line) => sum + (+line.quantity || 0), 0);  result.total_price = Number(order.total_price || 0);  result.source = 'cart_object';}

Если объекта Cart на странице нет (кастомная платформа, не InSales) — раннер откатывается на CSS-селекторы, которые задаются в конфиге отдельно под каждый чек. То есть темплейт применим не только к моей платформе.

Каждая проверка возвращает статус ok / warning / failed / error, а при провале — скриншот и собранные консольные ошибки/упавшие запросы со страницы, чтобы не лезть перепроверять руками, что именно сломалось.

speed-runner: не просто секундомер

Формальный onload на страницах с ленивыми виджетами почти бесполезен как метрика, он может наступить и раньше, и позже момента, когда пользователь реально видит готовую страницу. Поэтому speed-runner снимает серию скриншотов во время загрузки и сравнивает их между собой попиксельно (в grayscale, с уменьшением до 240×135), чтобы найти момент, когда картинка перестаёт визуально меняться:

async def measure_visual_ready(page, started, max_wait_ms):    frames = []    for _ in range(max(1, int(max_wait_ms / 500))):        frames.append({"time": round(time.time() - started, 2),        "shot": await page.screenshot(full_page=False, type="png", timeout=3000)})        await page.wait_for_timeout(500)    final_shot = frames[-1]["shot"]    for frame in frames:        if visual_similarity(frame["shot"], final_shot) >= 0.94:            return frame["time"]    return frames[-1]["time"]

Это и есть показатель стабилизации первого экрана, которая ложится в основу отчёта. Плюс к ней раннер считает время самого долгого запроса, число запросов дольше 3 и 10 секунд, разбивку запросов на origin/CDN/сторонние сервисы и топ-3 самых медленных ресурса.

Отдельная фишка — сравнение не только своего сайта, но и пары referenced-страниц конкурентов. Если моя скорость вдруг сравнялась с их проблемной — это внешний сигнал что у нас тоже что-то не так, даже если внутренние метрики этого явно не показывают.

Как выглядит отчёт на практике

Дважды в день (и по команде /report, /health или /speed в Telegram) в чат прилетает примерно такое:

Техническая сводкаЕжедневный отчётДата: 10.07.2026, 06:03Режим: fullРаботоспособность: ✅ 11/11 успешноКарточка товара:✅ Common.v2, smartCart patch, EventBus загружены успешно✅ Галерея видима✅ Добавление в корзину работает✅ Добавление товара с аксессуаром работает✅ Комплектация обновляется без дублей✅ Стресс-тест не провоцирует двойное добавлениеКорзина:✅ Cart init, common.v2, EventBus загружены успешно✅ Нет дублей строк✅ Количество и сумма соответствуют действительности✅ Попап мотиватора открывается и выводит контент✅ Удаление товара работает нормальноSpeed: ✅ всё окНаш магазин: первый экран стабилизируется за 2-4с, медленных запросов >3с — 0.Конкурент 1: первый экран ~18с.Конкурент 2: первый экран ~21-23с.Вывод: задержек на нашей главной не видно, конкуренты заметно медленнее по первому экрану.Итог: ✅ критических проблем нет

Следом отдельным сообщением приходит PNG-таблица с разбивкой по метрикам (визуальная загрузка, технический хвост, картинки, JS-библиотеки) в разрезе моего сайта и референсных страниц — удобно смотреть подробную динамику глазами.

Если что-то из проверок падает — вместо ✅ в соответствующей строке будет ❌ с текстом ошибки, а в артефактах отчёта — скриншот и консольные ошибки с той страницы, где сломалось.

Оркестрация в n8n

Воркфлоу запускается по расписанию два раза в сутки, либо вручную по команде в Telegram-боте (/report, /health, /speed, /help). Внутри — нормализация запуска (ручной/по расписанию), сборка конфига проекта, условный вызов health- и speed-раннеров в зависимости от режима, нормализация ответов (раннер мог упасть, вернуть не-JSON, зависнуть по таймауту — всё это обрабатывается отдельно, чтобы одна упавшая проверка не роняла весь отчёт), сборка финального текста и отправка.

Про безопасность

Оба раннера это, по сути, публичный HTTP-эндпоинт, который открывает headless-браузер по присланному URL. Это классический вектор SSRF, если не думать о защите заранее, поэтому:

  • запрос авторизуется API-ключом через x-api-key / Authorization: Bearer, сверка идёт через hmac.compare_digest, а не ==, чтобы не давать атакующему канал по времени ответа;

  • домен, на который идёт запрос, резолвится и проверяется, что за ним не приватный/loopback/link-local IP и не адрес облачной метадаты — раннер не пойдёт стучаться в 127.0.0.1 или во внутреннюю инфраструктуру;

  • список разрешённых хостов (ALLOWED_HOSTS) можно и нужно ограничивать своим доменом;

  • /docs и /openapi.json в проде отключены, чтобы не светить схему API наружу.

Это не идеальная защита — например, есть теоретическое окно для DNS rebinding между проверкой хоста и реальным коннектом браузера, честно описанное в комментарии в коде и в SECURITY.md но для сценария «гоняем по своему домену и паре референсных страниц» этого достаточно. Если решите открыть раннер под произвольные чужие URL — этого уровня защиты уже будет мало.

Быстрый старт

Полная инструкция — в репозитории (DEPLOY.md и CONFIGURATION.md), но в двух словах:

  1. Собираете Docker-образ health-runner и speed-runner, задаёте HEALTH_RUNNER_API_KEY, SPEED_RUNNER_API_KEY, ALLOWED_HOSTS.

  2. Разворачиваете — я использую Yandex Cloud Functions на базе этого образа, но подходит любой Docker-хостинг (в репозитории есть docker-compose.example.yml для классического варианта на VPS).

  3. Импортируете в n8n n8n/tech-checker-template.json, прописываете переменные окружения HEALTH_RUNNER_API_KEY, SPEED_RUNNER_API_KEY, REPORT_CHAT_ID — они не хранятся в самом воркфлоу, только подтягиваются из $env при выполнении, так что при повторном экспорте темплейта реальные ключи никуда не утекут.

  4. В Code-ноде «Build Report Config» заполняете свои URL страниц и CSS-селекторы под свою вёрстку (если это не InSales — заполняете селекторы вместо опоры на Cart.order).

  5. Подключаете Telegram-бота, включаете расписание.

Дальше два раза в день в чат прилетает отчёт, а по команде — в любой момент.

Что это дало на практике

Главный результат это не факт наличия отчётов, а то, что регрессии теперь ловятся до жалоб покупателей. Любое обновление на стороне InSales, любая моя правка в кастомных виджетах — видна в отчёте в течение максимум полусуток, а не постфактум через тикет в поддержку с недовольным клиентом. Плюс появился объективный ориентир по скорости сравнение с конкурентами показывает, где я объективно быстрее, а где ещё есть куда расти, независимо от того, что «внутри всё вроде ок».

Репозиторий

https://github.com/DmitriiDmallSick/ecom-tech-speed-checker

ссылка на оригинал статьи https://habr.com/ru/articles/1057772/