
Привет, Хабр! С вами снова Владимир Михайлов, Marketing Analyst в Garage Eight. В прошлой статье я рассказывал, как на одном из своих прошлых проектов мы с командой собрали MVP внутренней платформы конкурентной аналитики: превратили Sensor Tower в источник данных, настроили их обработку и доставку отчетов в Slack. Этот MVP стал фундаментом для следующего этапа. Сегодня расскажу, как мы использовали AI, чтобы отслеживать новые возможности внутри продуктов конкурентов и отделять реальные изменения от технического шума.
Что конкурент добавляет в продукт клиента
Большинство инструментов конкурентного мониторинга следят за тем, что легко увидеть снаружи: рекламой, SEO, ценами, публикациями, обновлениями приложений. Но продуктовой команде часто интереснее, что именно конкурент добавил в продукт: может, это новый AI-инструмент, способ работы с данными или расширение существующей функции. И вот как раз такие изменения часто не анонсируются вообще — их просто тихо выкатывают пользователям.
Узнавать об изменениях других компаний важно, поэтому я решил автоматизировать именно этот процесс. В результате получилась система, которая авторизуется в продуктах конкурентов, исследует доступный интерфейс, строит семантический снимок функциональности, сравнивает его с предыдущим состоянием и отправляет в Slack только реальные продуктовые изменения.
Не:
у кнопки поменялся testid
и не:
раздел переехал на другой URL
а:
NEW FEATURE — Smart Recommendations
Пользователь теперь получает персонализированные рекомендации непосредственно внутри продукта.
Вот только построить такой мониторинг значительно сложнее, чем просто сравнивать две версии сайта.
Почему обычный website monitoring здесь не работает
Первая версия идеи была очевидной: снимаем DOM сегодня, завтра снимаем еще раз — и сравниваем.
old_html = load_snapshot("yesterday.html")
new_html = load_snapshot("today.html")
diff = compare(old_html, new_html)
Проблема в том, что современный SPA может изменить тысячи элементов без единого изменения продукта:
class changed— поменялось название CSS-класса;testid changed— изменился идентификатор для тестов;URL changed— раздел переехал на другой адрес;component moved— компонент переместился внутри страницы;label changed— поменялся текст на кнопке;experiment variant changed— пользователю показали другой вариант A/B-теста;DOM structure changed— изменилась структура разметки.
В результате получается отличный детектор фронтенд-деплоев и довольно плохой детектор продуктовых изменений — я бы каждый день получал сотни уведомлений «что-то поменялось», но почти все они были бы мусором.
Я довольно быстро понял, что задаю неправильный вопрос. Нужно выяснить, не что изменилось на странице, а что пользователь теперь может делать такого, чего не мог делать раньше, — это и стало основной идеей проекта.
Как выглядит архитектура
В упрощенном виде система выглядит так:
Competitor
↓
Authenticated Browser
↓
AI Discovery
↓
Feature Model
↓
Semantic Snapshot
↓
Compare
↓
Business Significance Filter
↓
Slack
где:
Competitor — продукт конкурента, изменения которого мы отслеживаем;
Authenticated Browser — браузер с сохраненной авторизованной сессией, который позволяет видеть интерфейс так же, как его видит реальный пользователь;
AI Discovery — AI, который исследует интерфейс и определяет доступные пользователю функции и действия;
Feature Model — здесь найденные возможности преобразуются в структурированную модель продукта;
Semantic Snapshot — сохраняет текущее состояние продукта в виде набора его возможностей, а не HTML-страницы;
Compare — новое состояние сравнивается с предыдущим, и определяются добавленные, удаленные или измененные возможности;
Business Significance Filter — на этом этапе отбрасываются технические и несущественные изменения, остаются изменения, важные с точки зрения продукта или бизнеса;
Slack — тут значимые изменения отправляются команде в виде уведомления с кратким описанием.
Важный архитектурный принцип заключается в том, что AI не получает два HTML-файла с просьбой: «Скажи, что изменилось». LLM используется раньше — для преобразования интерфейса в структурированную модель продукта. А сравнение двух состояний после выполняется детерминированным кодом. Проще говоря, обычный Python сравнивает два структурированных объекта, за счет чего мы получаем стабильный и воспроизводимый результат.
При этом одних автоматических уведомлений оказалось недостаточно: Slack хорошо отвечает на вопрос «Что изменилось сегодня?», но для аналитической работы нужны более сложные запросы — что нового появилось у конкурентов за месяц, какие AI-функции уже есть на рынке, какие возможности есть у одного конкурента и отсутствуют у другого.
Именно поэтому я добавил отдельный аналитический слой и подключил его к Claude через MCP. Claude получил набор read-only инструментов для работы с накопленной историей:
получение текущих функций каждого конкурента;
поиск NEW/UPDATED/REMOVED за выбранный период;
фильтрацию по категориям;
просмотр конкретной функции и ее capabilities;
проверку покрытия мониторингом и доступности исторических данных.
Claude работает уже с подготовленными структурированными данными. В результате вместо SQL, JSON и ручного просмотра истории можно спросить обычным языком: какие новые функции и возможности есть или появились у конкурентов за последний месяц, но пока не обнаружены у остальных?
Дополнительно история мониторинга синхронизируется с Google Drive. Это позволяет использовать накопленные результаты как дополнительную базу знаний внутри отдельного Claude Project для конкурентной разведки.
В итоге получилось два интерфейса к одной системе:
Slack → push-мониторинг: система сама сообщает команде о новых и измененных функциях;
Claude → pull-аналитика: сотрудник сам задает вопрос о конкурентах, периодах, функциях и трендах и получает ответ на основе накопленной истории.
То есть Competitor Feature Monitor постепенно превратился из обычного site-monitoring скрипта в небольшой AI Competitive Intelligence Assistant: браузерные агенты собирают изменения, детерминированный слой хранит и сравнивает факты, а Claude становится разговорным интерфейсом к этой базе. В итоге мы получаем:
Competitor websites → Playwright/MCP → Feature extraction → Semantic comparison → History → Slack + Google Drive → Claude Analytics MCP → Analyst
Зачем вообще заходить внутрь продукта
Публичный сайт показывает далеко не всю функциональность. Например, снаружи мы видим то, что продукт показывает всем — на лендинге, в маркетинговых материалах, в документации:
Analytics
Automation
Reports
Integrations
А после авторизации:
Saved Views
Custom Alerts
Recommendations
Advanced Analytics
Automation Rules
Notifications
Account Tools
...
Многие из этих функций нельзя найти ни на публичном сайте, ни в сторе, ни в пресс-релизах: они доступны только тем, кто залогинился. Именно поэтому нужно было научить систему работать с той частью продукта, которую видит обычный авторизованный пользователь.
Persistent Chrome profiles
Для каждого конкурента я создал persistent Chrome profile — отдельную папку, в которой Chrome хранит куки, localStorage, сессии, историю:
.browser-profile/
competitor-a/
competitor-b/
competitor-c/
...
И отдельный Playwright MCP — способ, которым AI управляет браузером:
playwright-competitor-a
playwright-competitor-b
playwright-competitor-c
После первого полноценного входа следующий запуск во многих случаях выглядит так:
[pre-login] auth_check -> AUTH_OK
LOGIN_NOT_NEEDED
Монитор открывает существующий профиль, проверяет авторизацию и начинает discovery. Но именно здесь мы столкнулись с первым серьезным набором edge cases.
AUTH_OK
Самый простой вариант определить AUTH_OK:
if "/login" not in page.url:
return AUTH_OK
ненадежен. SPA может сначала показать login, затем восстановить session, или наоборот: на секунду показать authenticated UI, а потом отправить пользователя на login. Именно поэтому я выбрал вариант со state machine, где система проходит через несколько состояний:
class AuthState(Enum):
AUTH_CHECKING = "AUTH_CHECKING"
AUTH_OK = "AUTH_OK"
AUTH_EXPIRED = "AUTH_EXPIRED"
AUTH_2FA_REQUIRED = "AUTH_2FA_REQUIRED"
AUTH_CAPTCHA_REQUIRED = "AUTH_CAPTCHA_REQUIRED"
AUTH_UNKNOWN = "AUTH_UNKNOWN"
Для каждого продукта я использовал несколько независимых authenticated markers. Условно:
{
"authenticated_markers": [
"navigation-user-menu",
"account-switcher",
"logout-control"
],
"authenticated_threshold": 2
}
Есть три признака того, что мы авторизованы: меню пользователя, переключатель аккаунта и кнопка выхода. Но система считает нас авторизованными, только если найдены минимум два признака из трех, потому что один элемент может случайно совпасть или подгрузиться с задержкой, а вот два-три независимых уже надежнее.
SPA race conditions
Даже несколько markers не спасают от навигационных гонок. Во время одного реального login Playwright получил:
Execution context was destroyed,
most likely because of a navigation
При этом login фактически прошел успешно. Если воспринимать такое исключение как authentication failure, монитор может начать повторно отправлять форму, поэтому проверка состояния выполняется серией наблюдений:
def poll_until_stable(reader, classifier, max_wait):
samples = []
while time.time() < deadline:
try:
snapshot = reader.snapshot()
except Exception:
time.sleep(POLL_INTERVAL)
continue
state = classifier(snapshot)
samples.append(state)
if stable(samples):
return state
time.sleep(POLL_INTERVAL)
return AUTH_UNKNOWN
Transient navigation error означает:
skip sample → continue polling
а не:
LOGIN_FAILED
Мелкая деталь, но без нее auth automation оказывается очень хрупкой.
AI не должен видеть credentials
Это было одним из принципиальных решений. Credentials — это логины и пароли, и если AI управляет браузером, возникает соблазн просто передать ему логин и пароль, чтобы он сам всё ввел. Но тогда секрет окажется в истории запросов и ответов модели, а это уже риск утечки.
Claude управляет браузером через MCP, но логин и пароль я не передаю через MCP tool calls.
Credentials хранятся в системном credential store:
username, password = get_credentials("competitor-a")
И получает их отдельный standalone Playwright process:
OS Credential Store
↓
run_auto_login.py
↓
Chrome
Recovery выглядит так:
MCP auth check
↓
AUTH_EXPIRED
↓
close MCP browser
↓
standalone auto-login
↓
reopen profile
↓
AUTH_OK
↓
Discovery
AI проверяет авторизацию, видит, что сессия истекла, закрывает браузер, запускает отдельный процесс автологина, снова открывает профиль, проверяет, что всё хорошо, и продолжает исследование. При этом AI не знает ни логина, ни пароля — он просто получает сигнал, что можно работать.
Как работать с двухфакторной аутентификацией
На одном из сервисов первый реальный auto-login закончился так:
[pre-login] AUTH_EXPIRED
[post-login] AUTH_UNKNOWN
LOGIN_FAILED
Но в этот момент на телефон пришел СМС-код, и на самом деле произошло:
credentials accepted
↓
SMS 2FA
↓
detector doesn't know this screen
↓
AUTH_UNKNOWN
Логин и пароль подошли, просто из-за СМС-кода система не узнала этот экран. Тогда я добавил отдельное состояние:
AUTH_2FA_REQUIRED
Так система не пытается обходить 2FA — она останавливается и требует подтверждения от человека. После ввода кода сессия может сохраняться, и последующие запуски снова будут работать автоматически:
AUTH_OK
LOGIN_NOT_NEEDED
Получилась вполне практичная схема:
Автоматизация по умолчанию + human-in-the-loop только тогда, когда его действительно требует сервис.
Что делать с CAPTCHA
Сначала логика была примерно такой:
if iframe.title == "reCAPTCHA":
return AUTH_CAPTCHA_REQUIRED
И довольно быстро дала false positive.
На странице может присутствовать invisible reCAPTCHA или обычный CAPTCHA badge, хотя никакого challenge пользователь сейчас не проходит.
Именно поэтому пришлось разделить:
CAPTCHA infrastructure exists
и:
CAPTCHA is actively blocking authentication
Так можно различить два разных состояния: «на сайте вообще есть CAPTCHA» и «нас прямо сейчас просят ее пройти». Это частая ловушка browser automation: элемент на странице есть, а состояния, которое он обычно означает, нет.
Persistent profile не равен persistent session
Самый интересный auth-кейс встретился на другом сервисе.
Auto-login успешно завершается:
[pre-login] AUTH_EXPIRED
[post-login] AUTH_OK
LOGIN_SUCCESS
Закрываем Chrome.
Открываем тот же physical profile:
AUTH_EXPIRED
Система логинится, всё хорошо, но стоит закрыть браузер и снова открыть профиль — опять нужно авторизоваться. Хотя профиль тот же, куки те же, всё должно сохраниться.
Причиной оказалась auth-critical session cookie:
expires: -1
session: true
Пока живет Chrome:
Chrome process
↓
session cookie
↓
AUTH_OK
Закрываем Chrome:
Chrome process dies
↓
session cookie dies
↓
AUTH_EXPIRED
Отсюда один из важных практических выводов проекта:
Persistent browser profile ≠ persistent authenticated session.
Для таких сервисов нужна другая архитектура.
Что делает Browser handoff
Как раз на этом этапе нужно передать браузер из рук в руки: один процесс логинился, а другой подхватывал уже авторизованную сессию и продолжал работу.
Вместо:
login
↓
close Chrome
↓
reopen Chrome
↓
discovery
нужно:
start Chrome
↓
auto-login
↓
AUTH_OK
↓
keep Chrome alive
↓
AI/Playwright attaches
↓
Discovery
↓
Snapshot
↓
Compare
В первом случае мы логинимся, закрываем браузер, открываем заново — и сессия умирает. Во втором случае логинимся и не закрываем браузер, а просто подключаемся к нему другим процессом — сессия остается живой, потому что Chrome не закрывался.
Один из вариантов — CDP:
Python Playwright
↓
Chrome --remote-debugging-port
↓
authenticated context
↓
Playwright MCP connectOverCDP()
↓
AI discovery
Если сервис решил, что сессия не должна сохраняться, не надо с этим бороться— надо просто не закрывать браузер.
Главный вопрос: что считать изменением продукта
Изначально feature выглядела примерно так:
{
"id": "competitor-a:smart_recommendations",
"name": "Smart Recommendations",
"url": "...",
"description": "...",
"selectors": [],
"labels": []
}
Мы считали:
label_hash(feature)
structure_hash(feature)
Это позволяло обнаруживать, когда появлялась новая функция, когда она менялась, была временно недоступна и появлялась снова. Но представим два сценария.
Сценарий №1
Функция переехала с:
/analytics
на:
/insights/analytics
DOM полностью переделали, но для пользователя ничего не изменилось.
Сценарий №2
URL, DOM и название функции остались прежними. Но раньше пользователь мог выполнять действие только вручную, а теперь может настроить автоматическое выполнение— это ощутимое изменение.
Обычный structural diff в первом случае говорит:
MAJOR CHANGE!
А второй может вообще не заметить.
Для продуктовой разведки всё должно работать наоборот.
Почему важны Semantic capabilities
Это ключевой слой всей системы. Нужно было описать не только feature, но и что конкретно пользователь может с ее помощью делать:
{
"feature_key": "automated_action",
"capabilities": [
"perform_action",
"schedule_action"
]
}
capabilities— это стабильные смысловые единицы:
schedule_action
export_report
create_custom_alert
compare_multiple_items
weekly_frequency
Для них считается отдельный hash:
def capability_hash(feature: dict) -> str:
payload = {
"capabilities": sorted(
feature.get("capabilities", [])
)
}
return stable_hash(payload)
И теперь:
business_significant = (
new_capability_hash != old_capability_hash
)
Итого: если набор возможностей изменился— это бизнес-значимое изменение, остальное— технический шум.
После этого в системе фактически появились два независимых мира.
Technical changes
URL changed
DOM changed
testid changed
label changed
navigation changed
Они полезны для диагностики и аудита.
Product changes
user can do something new
Именно они отправляются людям.
Например:
Событие | Product alert |
Функция переехала на другой URL | Нет |
Переделали DOM | Нет |
Переименовали кнопку | Нет |
Добавили автоматическое выполнение | Да |
Теперь можно работать с несколькими объектами вместо одного | Да |
Добавили новый вариант аналитики | Да |
Это оказалось главным архитектурным решением проекта.
Как быть с semantic diff, если он тоже может шуметь
Казалось бы, если мы сравниваем смысл, а не технические детали, шума быть не должно, но и тут есть подвох. Представим, что вчера AI назвал capability:
schedule_action
а сегодня:
automate_action
Продукт не изменился, а вот hash— да, поэтому capabilities нельзя генерировать как свободный текст. Если позволить AI придумывать названия самому, каждый день будет новый набор слов, и сравнение потеряет смысл.
Я ввел правило:
Capability — стабильный атомарный идентификатор подтвержденной пользовательской возможности.
То есть AI выбирает значения из установленного мной списка.
По мере роста системы для них нужен нормальный справочник категорий:
{
"automation": [
"schedule_action",
"pause_automation",
"resume_automation"
],
"analytics": [
"compare_multiple_items",
"view_historical_data",
"export_report"
]
}
Это уже не столько техническая, сколько data governance задача.
Еще одна ловушка — migration storm
Иногда после изменения схемы система вдруг решает, что изменилось всё сразу. Именно поэтому я добавил capability_hash, когда baseline уже существовал. Получилось:
Старый snapshot:
{
"feature": "Smart Recommendations"
}
Новый:
{
"feature": "Smart Recommendations",
"capability_hash": "abc123"
}
Формально:
None != abc123
И первый запуск новой версии системы мог объявить все функции обновленными.
Поэтому:
if "capability_hash" not in old_feature:
business_significant = False
Первое появление semantic metadata— миграция схемы, а не изменение продукта.
Что важно запомнить
Baseline: первый запуск ничего не «обнаруживает»
Допустим, первое исследование продукта нашло:
23 features
15 confirmed
8 partial
Нельзя отправлять:
23 NEW FEATURES!
Это не новые функции— просто система увидела продукт впервые. Именно поэтому первый успешный run создает baseline:
data/competitor-a/
current.json
baseline/
first_run.json
И только следующие snapshots сравниваются с ним.
Принцип простой:
first observation = known state
next observation = possible change
Исчезновение функции еще не означает ее удаление
Функция может временно пропасть:
из-за A/B тестов— разным пользователям показывают разные варианты, и в момент снимка мог попасться вариант без этой функции;
lazy loading— раздел подгружался с задержкой, и AI просто не успел его увидеть;
временной ошибки— что-то на стороне сервиса сломалось;
feature flag— функцию временно отключили переключателем;
rollout— функцию выкатывают постепенно, и не все пользователи ее еще видят;
изменения навигации— функция на месте, но до нее теперь надо добираться другим путем;
неполного discovery— AI просто не дошел до нужного раздела.
Именно поэтому одного missing observation недостаточно. Missing observation— это ситуация, когда функция не найдена в одном конкретном снимке. Но один пропуск ничего не доказывает.
У нас есть lifecycle:
present
↓
missing
↓
temporarily unavailable
↓
missing again
↓
...
↓
removal threshold
↓
removed
Например:
{
"removal_threshold": 3
}
Временное исчезновение и повторное появление при этом не летят в продуктовый Slack.
Safety: AI нельзя разрешать «исследовать всё»
Это особенно важно для авторизованных продуктов.
Discovery разрешено:
читать
переходить по безопасным разделам
анализировать интерфейс
открывать подтвержденные navigation links
Но запрещено выполнять действия с side effects:
покупки
оплаты
заказы
изменение аккаунта
изменение персональных данных
удаление
публикация
verification
Если эффект действия непонятен:
SKIP
Иногда система знает только, что функция существует, но не может безопасно исследовать ее полностью.
Тогда мы сохраняем:
partial
Лучше получить неполный подтвержденный snapshot, чем полный, но выдуманный.
Как выглядит результат для человека
Первая версия Slack notifier отправляла технические CHANGED.
Например:
CHANGED — Advanced Analytics
Navigation link changed.
Для продуктовой команды это не особо полезно, поэтому Slack полностью отвязан от:
structure_hash
label_hash
technical severity
В него попадает только:
NEW
или:
business_significant is True
Сообщение выглядит примерно так:
🔔 Competitor A
✨ NEW
Smart Recommendations
Пользователь теперь получает персонализированные
рекомендации внутри продукта.
[View feature]
🆙 UPDATED
Custom Alerts
Теперь одно уведомление может содержать
несколько пользовательских условий.
[View feature]
Detected: 2026-09-10 · 1 new · 1 updated
Без:
DOM
testid
selector
evidence hash
navigation path
URL changed
internal IDs
Техническая информация остается внутри monitoring data— человек получает только бизнес-смысл.
Почему здесь вообще нужен AI
Можно было написать обычные Playwright scrapers, но тогда для каждого продукта пришлось бы заранее знать:
какие страницы существуют
где находится каждая функция
какие selectors использовать
как устроена навигация
что считать одной feature
Любой редизайн превращался бы в новую задачу разработчика, поэтому я решил разделить ответственность. Python / deterministic layer отвечает за:
authentication
storage
hashes
comparison
lifecycle
safety
notifications
AI отвечает за:
interface understanding
discovery
semantic classification
feature descriptions
capability detection
В итоге LLM не решает, изменился ли hash, а Python не пытается понять, является ли новый элемент интерфейса новой пользовательской возможностью.
Итог: что всё это значит для бизнеса
После нескольких месяцев monitoring появляется база: Competitor × Feature × Capability × Time. И Slack alerts становятся только первым уровнем ее использования.
Можно считать features launched / competitor / quarter, видеть, кто первым внедрил capability, и измерять, сколько времени проходит между появлением возможности у одного продукта и появлением аналога у других.
Для удобства можно вообще построить Product Feature Radar:
Capability | Product A | Product B | Product C | Product D |
Smart Recommendations | ✓ | ✓ | ||
Scheduled Actions | ✓ | ✓ | ||
Advanced Analytics | ✓ | ✓ | ✓ | |
Custom Alerts | ✓ | ✓ | ✓ |
И таблица будет обновляться автоматически.
Система постепенно проходит путь от website monitoring к market intelligence, поэтому бизнесу открываются новые метрики для аналитики:
Feature Velocity — сколько значимых функций выпускает продукт за период;
Category Velocity — какие продуктовые категории развиваются быстрее остальных;
Time-to-Follow — как быстро рынок копирует новую возможность;
Feature Convergence — какие возможности постепенно становятся стандартом рынка;
Differentiation Map — какие capabilities пока остаются уникальными.
Так конкурентная аналитика превратится в полноценную разведку рынка.
Как вам такое решение? Пользовались бы или считаете, что это уже нарушает границы цифровой этики?

Комментарии 0