Stealth-плагин для Puppeteer - что он латает и что не пытался чинить
Если ваш стек автоматизации на Puppeteer, puppeteer-extra-plugin-stealth наверняка уже лежит в вашем package.json. Это разумное значение по умолчанию: набор небольших заплаток на видимые из JS свойства, которые обычный Chromium выдаёт, а браузер человека нет. Системой личности он никогда не был, и обращение с ним как с таковой - то место, где месяцами работавшие скрипты начинают падать в проде без очевидной причины.
Что он на самом деле латает
Плагин поставляется списком именованных модулей обхода, каждый из которых целится в одно свойство: navigator.webdriver, chrome.runtime, chrome.app, chrome.csi, chrome.loadTimes, iframe.contentWindow, media.codecs, navigator.languages, navigator.permissions, navigator.plugins, sourceurl, user-agent-override, webgl.vendor и другие. Каждый модуль выполняется до скриптов страницы и переписывает геттер так, чтобы свойство читалось как в headful-браузере под управлением человека, а не как в автоматизации по умолчанию. Это список заплаток, применяемых при загрузке страницы и нацеленных на конкретные свойства.
Проблема сопровождения
Каждый модуль в этом списке - ставка на то, что считается «нормальным» в текущей сборке Chromium. Значения по умолчанию для headless, поведение permissions API и точная форма chrome.runtime менялись от релиза к релизу, поэтому модуль, написанный под одну версию, к следующей может устареть или стать неверным. Заплатка, которая перестаралась, - подпись сама по себе: детектору не обязательно ловить автоматизацию напрямую, если можно узнать конкретную форму свойства, которую оставляет за собой stealth-плагин. Поддерживать список модулей в порядке означает перепроверять его на каждом релизе Chromium, который плагин объявляет поддерживаемым, а проект с медленным темпом релизов относительно самого Chromium - реальный операционный риск для тех, кто зависит от него, не читая changelog.
Три вещи, которые в его задачи не входили
Личность, сохраняющаяся между запусками. Плагин правит свойства внутри текущего процесса. Он не генерирует и не хранит хеш canvas, строку рендерера WebGL, список шрифтов или аудиоотпечаток, привязанные к каталогу профиля. Направьте его на свежий userDataDir или на тот же после обновления Chromium - и подложечная подпись устройства может измениться, пока пропатченные свойства остаются прежними. Запустите несколько личностей на одной машине - и все они сообщат один и тот же аппаратный отпечаток под разными куками, а это системе оценки риска кластеризовать проще, чем при полном отсутствии stealth-заплаток.
Привязка прокси, тянущая за собой часовой пояс и локаль. page.authenticate() и --proxy-server= маршрутизируют трафик. Ни то, ни другое не трогает Intl.DateTimeFormat().resolvedOptions().timeZone или navigator.language, поэтому сессия может выходить через берлинский IP, а браузер по-прежнему будет сообщать американский часовой пояс и локаль. Именно такое расхождение и ловят проверки согласованности часового пояса с IP, и ни один модуль обхода в stealth-плагине им не занимается.
Аутентификатор WebAuthn для passkey. Плагин не трогает WebAuthn вообще. Домен CDP WebAuthn в Puppeteer умеет добавить виртуальный аутентификатор на сессию, но ничто не сохраняет учётные данные вместе с профилем, поэтому любой аккаунт, перешедший на вход по passkey, становится недостижим при следующем запуске скрипта.
Проверки, которые стоит прогнать на текущей схеме
- Запустите два-три отдельных профиля
userDataDirна одной машине и сравните хеши canvas и WebGL. Одинаковые значения между профилями - это признак, а не доказательство скрытности. - Сравните часовой пояс выходного IP прокси с тем, что страница сообщает в
Intl.DateTimeFormat().resolvedOptions().timeZone. Скрипт, выставляющий одно из них вручную, рано или поздно выставит неверно. - Зарегистрируйте тестовый passkey, закройте браузер, откройте тот же профиль и попробуйте войти снова. Если аутентификатор был виртуальным и жил в пределах сессии, учётных данных уже нет.
Разбор отпечаточной части этой проверки - в чек-листе согласованности отпечатка.
Когда stealth-плагин и есть верный ответ
Одна личность, одна машина, цели, которые не снимают отпечатки агрессивно, аккаунты, не перешедшие на passkey: плагин плюс постоянный userDataDir закрывают этот случай и не стоят ничего сверх установки. Менять его на что-то другое в таком масштабе - накладные расходы на несуществующую задачу.
Где здесь AntiBrow и какова настоящая цена перехода
AntiBrow генерирует отпечаток один раз на профиль и применяет его в движке браузера, ниже слоя JS, поэтому он переживает перезапуски, а не плывёт вместе с машиной. Привязка прокси к профилю выводит часовой пояс, локаль и геолокацию из выходного IP автоматически. У каждого профиля свой виртуальный аутентификатор, поэтому однажды зарегистрированный passkey сохраняется и воспроизводится при следующем входе. Локальных профилей неограниченно на любом тарифе, включая бесплатный, без карты и без срока.
Скажем цену прямо: launch() возвращает стандартные Page и BrowserContext из Playwright, а не из Puppeteer. Код вокруг page.$, page.$eval, ElementHandle или page.setRequestInterception не заработает без изменений на Page из Playwright. Локаторы, автоожидание и перехват запросов отличаются достаточно, чтобы набор тестов и библиотека помощников в стиле Puppeteer столкнулись с настоящим переписыванием, а не с заменой одной строки. Если ваша кодовая база глубоко в идиомах Puppeteer, честно заложите этот переезд до того, как начнёте. Команды, уже сидящие на Playwright, ничего из этого не встретят; те же пробелы с их стороны разобраны в статье когда stealth-плагина для Playwright перестаёт хватать.
Проверьте сами
Создайте бесплатный профиль, привяжите его к любому прокси и повторите проверки canvas и часового пояса на нём, а не на обычном userDataDir. Вызов launch() и опции профиля есть в справочнике SDK.
Попробуйте на бесплатном тарифе.
Неограниченное число локальных профилей, без карты. Проверьте результат детекторами сами.