Перейти к содержимому

Browser-workflow мониторы (автоматизация браузера)

Обычный HTTP-монитор и content-монитор видят только тот HTML, что сервер отдал в первом ответе. Для современных SPA, страниц с тяжёлым JavaScript и всего, что прячется за авторизацией, этого мало: нужный текст появляется только после логина, нескольких кликов и отрисовки фронтендом.

Browser-workflow монитор прогоняет заданную последовательность действий в настоящем headless Chromium — как маленький Selenium-сценарий — и проверяет, что сценарий по-прежнему доходит до конца. Если любой шаг падает (элемент пропал, текст не совпал, страница не загрузилась) — приходит alert со скриншотом страницы в момент сбоя. Дополнительно монитор умеет извлекать текст со страницы и слать уведомление при его изменении.

app.notifly.ru → Мониторы → Создать → «Что вы хотите проверять?» → «Сценарий из нескольких шагов» → «Реальный браузер с кликами». Готовые мониторы живут на вкладке Browser.

В диалоге каждый шаг описывается словами — кнопка «Подобрать действие» вызывает ИИ-подбор, который сам находит действие и селектор и сразу показывает скриншот результата. Шаги входа отмечаются флажком «Авторизация». «Тестовый прогон» выполняет весь сценарий и сохраняет cookies сессии — при сохранении монитора они шифруются.

Монитор описывается полями:

  • appid — канал для алертов (свой или расшаренный вам с правом отправки).
  • name — название, 1..200 символов.
  • startUrl — начальный URL (обязателен, только http/https). С него начинается каждый прогон.
  • steps — JSON-массив шагов, от 1 до 20 штук. Выполняются строго последовательно.
  • cookies — опционально: сессия для авторизованных сценариев (см. ниже). Хранится в зашифрованном виде и наружу никогда не отдаётся.
  • timeoutSec — бюджет одного прогона сценария (а не шага). По умолчанию 60, максимум 120; браузерный сервис держит прогон в рамках 30–110 с. Ограничение на отдельный шаг задаётся в самом шаге.
  • intervalSec — как часто прогонять сценарий. Минимум 300 (5 минут), максимум 86400 (сутки).
  • alertMessage (обязателен, до 2000 символов), alertTitle, alertMessageMarkdown, alertPriority (0…10) — что и с каким приоритетом слать при падении.
  • notifyOnChange / notifyOnChangeVars — режим «уведомлять при изменении» извлечённых значений (см. ниже).

Каждый элемент массива steps — это объект:

ПолеНазначение
nameчеловеко-читаемое имя шага (обязательно, попадает в alert при падении)
descriptionописание шага на естественном языке — по нему ИИ подбирает action + selector
actionдействие (см. таблицу ниже)
selectorселектор цели: CSS, XPath или text=… (см. ниже)
valueзначение: URL / текст / клавиша / JS-выражение; поддерживает подстановку {{переменная}}
extractNameимя переменной для extractText и executeScript
authшаг авторизации: выполняется только при невалидной сессии
timeoutSecтаймаут этого шага, 1..120 с; 0 или отсутствие — встроенные ожидания действия

Селекторы. По умолчанию — CSS. XPath начинается с //, (// или xpath=; text=подстрока находит элемент по видимому тексту. Если основной селектор перестал находить элемент, браузер пробует ослабленные варианты — это снижает ложные срабатывания при косметических правках вёрстки. Подстановка {{переменная}} работает и в value, и в selector.

Таймаут шага — собственный дедлайн шага; общий бюджет прогона остаётся верхней границей. Он ограничивает шаг сверху, но не удлиняет встроенные ожидания: появления элемента (около 8–14 с на попытку, с одной повторной попыткой, если бюджет позволяет) и загрузки страницы при navigate (25 с). Для executeScript он заменяет лимит по умолчанию в 20 с.

Поддерживаются десять действий:

actionЧто делаетНужен selectorНужен value
navigateперейти на URL (value = URL)—да (URL)
clickкликнуть по элементуда—
typeввести value в поледатекст
selectвыбрать value в <select>дазначение опции
pressнажать клавишу value (например Enter)—клавиша
waitForдождаться появления селекторада—
assertTextпроверить, что текст селектора содержит valueдаожидаемый текст
extractTextизвлечь текст селектора в переменную extractNameда—
scrollпрокрутить страницу: value = top | bottom либо пиксели (300 вниз, -300 вверх); с селектором — прокрутить к элементуопционально—
executeScriptвыполнить JS-выражение value на странице; результат (строкой) — в переменную extractName, если она задана—JS-выражение

Шаги click, type, select, waitFor, assertText, extractText требуют непустой selector — без него создание/обновление монитора вернёт 400. Для navigate обязателен value (URL), для extractText — extractName, для executeScript — value длиной до 4000 символов.

executeScript исполняется внутри страницы, промисы дожидаются (await), а null/undefined превращаются в пустую строку. Шаг падает, если выражение бросило исключение. Сетевые запросы из скрипта идут через тот же браузер и тот же SSRF-фильтр.

Проверяем, что после логина в личном кабинете виден баланс, и запоминаем его:

[
{"name": "Открыть дашборд", "action": "waitFor", "selector": "#dashboard"},
{"name": "Раскрыть счёт", "action": "click", "selector": "button.account-toggle"},
{"name": "Дождаться баланса", "action": "waitFor", "selector": ".balance-value"},
{
"name": "Проверить валюту",
"action": "assertText",
"selector": ".balance-value",
"value": "₽"
},
{
"name": "Запомнить баланс",
"action": "extractText",
"selector": ".balance-value",
"extractName": "balance"
},
{
"name": "Число заказов",
"action": "executeScript",
"value": "document.querySelectorAll('.order-row').length",
"extractName": "orders",
"timeoutSec": 10
}
]

Чтобы мониторить страницы за логином, нужна валидная сессия. Проще всего описать вход прямо в сценарии и отметить эти шаги auth: true:

  • Сохранённые cookies применяются в начале каждого прогона. После успешного прогона монитор перешифровывает и сохраняет свежие cookies — сессия продлевается сама; при падении сохранённая сессия не трогается.
  • Если в сценарии есть auth-шаги и сохранена сессия, сначала выполняются только проверочные шаги. Прошли и браузер не оказался на странице входа — готово. Иначе сессия считается протухшей: сценарий прогоняется целиком, вместе с входом, и новая сессия сохраняется.
  • «Страница входа» — URL первого auth-шага navigate (сравниваются хост и путь). Поэтому вход удобно начинать с шага navigate на страницу логина.
  • Если проверочные шаги используют переменную, которая извлекается только на auth-шаге, короткий прогон невозможен и сценарий всегда выполняется целиком. Без сохранённой сессии — тоже.

Если сценарий закончился на странице входа, прогон считается упавшим: шаг session — сессия протухла, login — повторный вход не помог.

Сессию можно получить и отдельно: POST /browser-workflow/login выполняет шаги входа в headless-браузере и возвращает итоговые cookies в открытом виде — их затем передают строкой в поле cookies при создании/обновлении монитора, где они шифруются перед сохранением.

Окно терминала
curl -X POST "$NOTIFLY_URL/browser-workflow/login" \
-H "X-Notifly-Key: <api-token>" \
-H "Content-Type: application/json" \
-d '{
"startUrl": "https://app.example.com/login",
"steps": [
{"action": "type", "selector": "#email", "value": "me@example.com"},
{"action": "type", "selector": "#password", "value": "secret"},
{"action": "click", "selector": "button[type=submit]"}
],
"timeoutSec": 60
}'

Ответ:

{
"cookies": [
/* массив cookie: name, value, domain, path, secure, httpOnly, expires */
],
"count": 7,
"url": "https://app.example.com/dashboard",
"screenshot": "/9j/4AAQ..."
}

screenshot — base64 JPEG итоговой страницы (best-effort, может отсутствовать). Если браузер не смог выполнить шаги, эндпоинт отвечает 502 с причиной.

Вручную подбирать CSS-селекторы скучно и хрупко. Поэтому есть эндпоинт POST /browser-workflow/build-step: вы описываете шаг словами (description), а ИИ сам подбирает action и стабильный selector, после чего тут же проверяет кандидата в реальном браузере и возвращает результат.

Как это работает под капотом:

  1. Сервис открывает startUrl, применяет переданные cookies и воспроизводит уже подтверждённые steps — получая актуальное состояние страницы.
  2. LLM по описанию, тексту страницы и списку интерактивных элементов предлагает действие и до трёх ранжированных селекторов-кандидатов.
  3. Кандидаты дёшево предвалидируются по HTML (синтаксис + наличие на странице), затем все проверяются за один прогон браузера.
  4. Если действие сработало — возвращается verified: true вместе с примером значения (sampleValue) для extractText/assertText. Если нет — ещё до двух попыток с учётом ошибки, затем последнее предложение с verified: false и пояснением в note.

Тело запроса:

{
"startUrl": "https://app.example.com/login",
"steps": [
/* уже подтверждённые шаги: action, selector, value, extractName, timeoutSec */
],
"description": "нажать кнопку «Войти»",
"cookies": [
/* опционально: сессия */
],
"sessionId": "",
"timeoutSec": 60
}

sessionId — необязательный ключ тёплой браузерной сессии: передавайте одно и то же значение на протяжении работы мастера, и соседние запросы смогут переиспользовать открытый браузер (best-effort).

Ответ:

{
"action": "click",
"selector": "button[type=submit]",
"value": "",
"extractName": "",
"label": "Войти",
"reason": "...",
"sampleValue": "",
"verified": true,
"availableVars": ["balance"],
"cookies": [
/* итоговые cookies сессии */
],
"screenshot": "/9j/4AAQ..."
}

Если модель сочла шаг медленным, в ответе есть и рекомендованный timeoutSec шага. screenshot — base64 JPEG страницы после выполнения кандидата (best-effort).

Поле description — от 1 до 500 символов. availableVars показывает переменные, которые уже извлечены предыдущими шагами (extractText и executeScript с extractName) и доступны для подстановки {{var}} на этом шаге. Возвращаемые cookies стоит накапливать на клиенте и передавать в следующий build-step, чтобы авторизованные шаги не пришлось переигрывать заново.

Перед сохранением весь сценарий целиком можно прогнать через POST /browser-workflow/test-step — это «кнопка Проверить». Сервис выполняет все переданные шаги подряд (без логики короткого прогона) и сообщает, дошёл ли до конца.

Окно терминала
curl -X POST "$NOTIFLY_URL/browser-workflow/test-step" \
-H "X-Notifly-Key: <api-token>" \
-H "Content-Type: application/json" \
-d '{
"startUrl": "https://app.example.com/dashboard",
"cookies": [ /* сохранённая сессия */ ],
"steps": [ /* весь сценарий */ ],
"timeoutSec": 60
}'

Ответ:

{
"ok": true,
"failedStep": -1,
"url": "https://app.example.com/dashboard",
"screenshot": "/9j/4AAQ...",
"cookies": [
/* итоговые cookies всей сессии */
],
"count": 7,
"vars": {"balance": "12 340 ₽", "orders": "3"}
}

Если сценарий упал, ok: false, а failedStep укажет индекс сломавшегося шага (с нуля; -1 — сбой до первого шага, например не открылась стартовая страница) и error — причину. При успехе vars содержит все извлечённые значения — удобно сразу проверить, что забирается именно то, что нужно. Итоговые cookies админка сохраняет как сессию монитора.

Если включить notifyOnChange, монитор работает не только как «упал/не упал», но и как трекер значений: после каждого успешного прогона он сравнивает извлечённые переменные (extractText, executeScript с extractName) с прошлым успешным прогоном и шлёт уведомление, когда любое из них изменилось.

  • notifyOnChangeVars — JSON-массив имён переменных для отслеживания (строкой, например "[\"balance\"]"). Пусто = следить за всеми извлечёнными переменными.
  • В тексте уведомления — строки balance: старое → новое; заголовок по умолчанию — «Browser workflow «‹название›»: данные изменились».
  • Первое наблюдение значения уведомления не даёт; при падении прогона базовые значения сохраняются.

Так, например, можно ловить изменение цены товара, статуса заказа или остатка на складе на странице, которая рендерится только в браузере.

Монитор создаётся со статусом pending и первой проверкой «на сейчас». Дальше он переходит в up / down по результатам прогонов. При падении в карточке монитора видны lastError и lastFailedStep — текст последней ошибки и метка сломавшегося шага (#3 Дождаться баланса, либо session / login).

Алерт — заголовок alertTitle (пустой — «Browser workflow «‹название›» failed») и текст alertMessage с дописанными строками Шаг: … и Ошибка: ….

  • Алерт уходит при переходе в down и когда сценарий начинает падать на другом шаге. Пока падает тот же шаг, повторов нет, даже если текст ошибки меняется: отметка отправленного алерта хранится в alertedAt.
  • Re-arm: PUT с изменением startUrl, steps, intervalSec, timeoutSec, notifyOnChange или notifyOnChangeVars сбрасывает alertedAt и ставит прогон «на сейчас» — если сценарий всё ещё падает, придёт свежий алерт. Новые cookies и правка текстов ничего не перезапускают.
  • Отдельного уведомления о восстановлении нет: монитор просто возвращается в up.
  • Сбои самого браузерного сервиса (браузер не поднялся и т.п.) не считаются падением сайта: алерта нет, статус не меняется, монитор перепроверится позже.
  • В окне обслуживания прогоны идут и пишутся в историю, но алерт о падении откладывается: alertedAt не ставится, и если после окна сценарий всё ещё падает, первая же проверка пришлёт алерт. Уведомления об изменениях в окне не шлются, а базовые значения не сдвигаются — после окна сравнение пойдёт со значениями до работ.

Алерт — обычное сообщение канала: его досылка во внешние каналы подчиняется тихим часам. Если монитор добавлен на статус-страницу, переходы up↔down открывают и закрывают на ней авто-инцидент.

POST /browser-workflow/:id/pause ставит статус paused: сценарий не прогоняется, а результат уже запущенного прогона отбрасывается. POST /browser-workflow/:id/resume возвращает pending и ставит прогон «на сейчас». Оба отвечают 200 без тела.

Когда шаг падает, браузер снимает JPEG страницы (до 1280 px по ширине) и кладёт его в приватное объектное хранилище. Ключ кадра попадает в extras алерт-сообщения под именем notifly::screenshot:

{"notifly::screenshot": "browser-shots/browser-workflow/42/1759000000000.jpg"}

В ленте сообщений админки и Android-приложения под текстом алерта показывается превью, по клику — полный размер. Через API картинка отдаётся по GET /browser-shot/<key> (image/jpeg) — только владельцу монитора; API-токен, ограниченный по каналам, видит лишь кадры мониторов своих каналов. Чужой, удалённый или вытесненный кадр отвечает 404.

  • Хранится только кадр, попавший в алерт. Скриншоты прогонов, не породивших сообщение (тот же шаг продолжает падать), удаляются сразу.
  • Кадры занимают хранилище тарифа (storageMB: Free 50 МБ, Pro 1 ГБ, Business 10 ГБ): при переполнении самые старые кадры вытесняются, и у старых сообщений превью пропадает.
  • При удалении монитора удаляются и его скриншоты.
  • Для сбоев session и login (обнаруживаются уже после прогона) скриншота нет.

Вкладка История монитора показывает прогоны и график сбоев по шагам; через API — GET /monitor-history/browser_workflow_monitor/:id. В метаданных события видно, понадобился ли повторный вход (reloggedIn).

Каждый прогон сценария — одно событие дневной квоты dailyEvents, независимо от числа шагов. Если квота исчерпана, прогоны откладываются до 00:05 МСК следующего дня — см. Квоты и тарифы.

Авторизация — заголовок X-Notifly-Key с API-токеном (M…): чтение — любым токеном, изменения — токеном с доступом write (иначе 403). Токен, ограниченный по каналам, видит и меняет только мониторы своих каналов.

Метод и путьПрава API-токенаНазначение
GET /browser-workflowreadсписок мониторов (без cookies)
POST /browser-workflowwriteсоздать монитор
PUT /browser-workflow/:idwriteобновить монитор
DELETE /browser-workflow/:idwriteудалить монитор (вместе со скриншотами)
POST /browser-workflow/:id/pausewriteпоставить на паузу (status=paused)
POST /browser-workflow/:id/resumewriteснять с паузы (status=pending)
POST /browser-workflow/build-stepwriteИИ-подбор и проверка одного шага
POST /browser-workflow/loginwriteпрогнать логин и вернуть cookies сессии
POST /browser-workflow/test-stepwriteтестовый прогон всего сценария
GET /browser-shot/<key>readскриншот сбоя по ключу из extras сообщения

Те же операции с мониторами есть в Go/Python SDK и MCP-сервере.

Окно терминала
curl -X POST "$NOTIFLY_URL/browser-workflow" \
-H "X-Notifly-Key: <api-token>" \
-H "Content-Type: application/json" \
-d '{
"appid": 12,
"name": "Баланс в ЛК",
"startUrl": "https://app.example.com/dashboard",
"steps": "[{\"name\":\"Дождаться баланса\",\"action\":\"waitFor\",\"selector\":\".balance-value\"},{\"name\":\"Запомнить баланс\",\"action\":\"extractText\",\"selector\":\".balance-value\",\"extractName\":\"balance\"}]",
"cookies": "[ /* plaintext cookies из /login */ ]",
"intervalSec": 900,
"timeoutSec": 60,
"notifyOnChange": true,
"alertMessage": "Сценарий «Баланс в ЛК» сломался"
}'

См. также: Активные мониторы · Content-монитор · Workflow-монитор (HTTP) · Полный API.