Коротко. Готовый образ Playwright, расписание в CI до начала рабочего дня, уведомление при падении туда, куда приходят тревоги. Свой репортёр пишет успех и время шагов в textfile-коллектор node_exporter, а три правила ловят падение, замедление вдвое против недели и отсутствие запусков.

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

Симптом

  • Автотесты есть, но их запускают руками перед релизом, а в остальное время они молчат.
  • Прогон по расписанию падает неделю подряд, отчёт лежит в CI, и его никто не открывает.
  • Пользователи жалуются, что вход стал дольше, а когда это началось, сказать нечем.

Почему так

Автотест, который запускается по расписанию, по сути становится проверкой из мониторинга: он ходит в систему как пользователь и проверяет, что работа делается. Значит, и обращаться с ним нужно как с мониторингом. Результат должен приходить туда же, куда приходят тревоги, а не лежать в CI. Время шагов нужно хранить как ряд, чтобы видеть тренд. И отдельно нужна тревога на случай, когда тест вообще перестал запускаться.

Запуск в CI

У Playwright есть готовый образ со всеми браузерами и зависимостями, ставить ничего не нужно. Версия образа должна совпадать с версией @playwright/test в проекте. Пример для GitLab CI, расписание задаётся в настройках проекта (CI/CD → Schedules), например в 07:00 по будням:

smoke:
  image: mcr.microsoft.com/playwright:v1.55.0-noble
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
  script:
    - npm ci
    - npx playwright test
  after_script:
    - |
      if [ "$CI_JOB_STATUS" != "success" ]; then
        curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
          --data-urlencode chat_id="${TG_CHAT}" \
          --data-urlencode text="Утренняя проверка RX упала: ${CI_JOB_URL}"
      fi
  artifacts:
    when: always
    paths: [playwright-report/, test-results/]
    expire_in: 14 days

В Jenkins то же самое делается триггером cron('H 7 * * 1-5') и шагом post { failure { … } }. В закрытом контуре образ кладут в свой реестр, а уведомление шлют туда, куда у вас приходят тревоги: в почту, в мессенджер или через Alertmanager.

Пароли тестовых учёток задаются секретными переменными CI, в репозитории их нет. Отчёт и трассы сохраняются как артефакты при любом исходе: по трассе упавшего теста причину обычно видно без повторного запуска.

Время шагов в Prometheus

Уведомление скажет, что упало. Чтобы видеть, что медленно становится медленнее, время каждого шага пишется в метрики. Проще всего через textfile-коллектор node_exporter: тест пишет файл в формате Prometheus в каталог коллектора, node_exporter отдаёт его вместе с метриками сервера. Отдельный сервис не нужен.

Свой репортёр Playwright, файл reporters/prom.ts:

import type { Reporter, TestCase, TestResult, TestStep } from '@playwright/test/reporter';
import { writeFileSync, renameSync } from 'node:fs';

const q = (s: string) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ');

export default class PromReporter implements Reporter {
  private ok: string[] = [];
  private dur: string[] = [];
  private steps: string[] = [];

  onTestEnd(test: TestCase, result: TestResult) {
    if (test.parent.project()?.name === 'setup') return;
    const t = q(test.title);
    this.ok.push(`rx_ui_test_success{test="${t}"} ${result.status === 'passed' ? 1 : 0}`);
    this.dur.push(`rx_ui_test_duration_seconds{test="${t}"} ${(result.duration / 1000).toFixed(3)}`);
    // только шаги верхнего уровня: вложенные служебные (например, повторы toPass) Playwright тоже называет test.step
    result.steps.filter((s: TestStep) => s.category === 'test.step').forEach((s) => {
      this.steps.push(`rx_ui_step_duration_seconds{test="${t}",step="${q(s.title)}"} ${(s.duration / 1000).toFixed(3)}`);
    });
  }

  onEnd() {
    const out = process.env.PROM_TEXTFILE ?? 'rx_ui.prom';
    const body = [
      '# HELP rx_ui_test_success 1 если проверка прошла',
      '# TYPE rx_ui_test_success gauge', ...this.ok,
      '# HELP rx_ui_test_duration_seconds Длительность проверки',
      '# TYPE rx_ui_test_duration_seconds gauge', ...this.dur,
      '# HELP rx_ui_step_duration_seconds Длительность шага проверки',
      '# TYPE rx_ui_step_duration_seconds gauge', ...this.steps,
      '# HELP rx_ui_last_run_timestamp_seconds Время последнего прогона',
      '# TYPE rx_ui_last_run_timestamp_seconds gauge',
      `rx_ui_last_run_timestamp_seconds ${Math.floor(Date.now() / 1000)}`, '',
    ].join('\n');
    writeFileSync(out + '.tmp', body);
    renameSync(out + '.tmp', out);   // node_exporter не прочитает файл наполовину записанным
  }
}

Подключается в playwright.config.ts рядом со стандартным отчётом:

reporter: [['html', { open: 'never' }], ['./reporters/prom.ts']],

Если тесты запускаются на сервере, где стоит node_exporter с ключом --collector.textfile.directory=/var/lib/node_exporter/textfile, запуск выглядит так: PROM_TEXTFILE=/var/lib/node_exporter/textfile/rx_ui.prom npx playwright test. Если в CI, файл после прогона копируют на такой сервер или отправляют в Pushgateway.

Каждая метрика описана заголовками HELP и TYPE, строки одной метрики идут подряд. Формат мы проверили promtool check metrics на файле, который репортёр записал после прогона тестов из второй части.

Тревоги

Три правила закрывают основное:

groups:
  - name: rx-ui
    rules:
      - alert: RxUiCheckFailed
        expr: rx_ui_test_success == 0
        labels: { severity: critical }
        annotations:
          summary: "Проверка «{{ $labels.test }}» не прошла"
          description: "Пользователи, скорее всего, не могут выполнить это действие. Отчёт и трасса в последнем прогоне CI."
      - alert: RxUiCheckSlow
        expr: rx_ui_step_duration_seconds > 2 * avg_over_time(rx_ui_step_duration_seconds[7d])
        labels: { severity: warning }
        annotations:
          summary: "Шаг «{{ $labels.step }}» вдвое медленнее, чем за неделю"
          description: "Сейчас {{ $value | humanizeDuration }}. Посмотрите время ответа и очереди за то же время."
      - alert: RxUiCheckStale
        expr: time() - rx_ui_last_run_timestamp_seconds > 26 * 3600
        labels: { severity: warning }
        annotations:
          summary: "Утренняя проверка не запускалась больше суток"
          description: "Расписание в CI выключено, раннер недоступен или тесты падают до записи метрик."

Последнее правило важнее, чем кажется. Без него сломанное расписание выглядит как отсутствие проблем: тревог нет, потому что нет и проверок. Порог 26 часов выбран под ежедневный запуск; если запуск только по будням, его поднимают до 74 часов, иначе тревога будет каждое воскресенье.

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

Чего не делать

  • Не оставлять результат только в CI. Падение должно приходить туда же, куда приходят тревоги мониторинга.
  • Не обходиться без тревоги на устаревание. Тест, который перестал запускаться, хуже, чем упавший.
  • Не запускать каждые пять минут. Сценарии с данными создают задачи. Для частых проверок оставьте только вход и загрузку веб-клиента, а сценарии гоняйте раз в день.
  • Не хранить пароли тестовых учёток в репозитории. Только секретные переменные CI.