Dilshat Rakhimov
Июль 2026 · 12 мин чтения
Типичная ситуация в перформанс-проекте: сайт - одностраничное приложение (SPA), фронтенд пишет команда клиента или подрядчик, а вам нужно измерять воронку заявок, квалифицировать лиды и передавать корректные конверсии в Google Ads, Meta и Яндекс. Доступа к исходникам фронта нет, dataLayer пустой или неполный, а «событие сабмита формы» само по себе бесполезно: для B2B-продукта важно не «форму отправили», а «отправил её тот, кто нам подходит» - юридическое лицо или ИП нужного профиля, а не физлицо, случайно нажавшее кнопку.
Классический ответ из ТЗ - «дёргать публичный API (например, госреестр) через backend-прокси и обогащать лид». Он рабочий, но требует бэкенда, ключей и согласований. Есть более прямой путь, если заметить одну вещь: сайт уже сам вызывает нужные эндпоинты. Он проверяет БИН/ИИН через свои же govtech-вызовы, чтобы показать пользователю результат. Значит, данные уже летают в браузере - их надо просто перехватить.
Эта статья - про паттерн fetch/XHR-интерсептора, целиком живущего в GTM: как перехватывать собственные API-вызовы SPA, квалифицировать по ним лиды, поднимать value-based bidding из перехваченных бизнес-атрибутов - и почему этот паттерн хрупок, что наглядно показал реальный инцидент.

Почему обычный GTM здесь не справляется
GTM хорошо видит клики, отправки форм и history-навигацию SPA. Чего он из коробки не видит - это тела ответов внутренних API-вызовов. А именно там лежит ответ на вопрос «квалифицирован ли лид»: сайт спрашивает у госреестра статус компании, форму собственности, ОКЭД, и по этому ответу решает, что показать пользователю. Для маркетинга этот же ответ - основа квалификации и ценности лида, но в DOM он не всплывает, а в dataLayer его никто не кладёт, потому что фронтенд писали не под аналитику.
Можно было бы повторить govtech-запрос самому из GTM. Но это лишний внешний вызов, лишние лимиты и риск рассинхрона с тем, что реально увидел сайт. Раз сайт уже сделал запрос и получил ответ - правильнее прочитать этот ответ, а не делать второй.
Механизм: monkey-patch fetch и XMLHttpRequest
Интерсептор - это Custom HTML тег в GTM, который на триггере DOM Ready подменяет window.fetch и методы XMLHttpRequest.prototype своими обёртками. Обёртка вызывает оригинал, а по пути смотрит на URL: если это один из интересующих эндпоинтов - копирует ответ (через response.clone() для fetch, через перехват onreadystatechange/load для XHR) и достаёт нужные поля.
Перехватываются, как правило, три типа вызовов:
| Эндпоинт (обобщённо) | Назначение |
|---|---|
/govtech/entity-check (параметр iinBin) | проверка ИП / физлица |
/govtech/org-info (параметр bin) | данные юрлица из госреестра |
/gateway/submit | сам сабмит заявки |
Поток такой:
Решение о квалификации - это несколько строк в самом теге, на основе перехваченного ответа:
Таймаут в 2000 мс на сбор обоих ответов - обязательная защита от гонки: entity-check и org-info приходят асинхронно и в разном порядке, и решение о квалификации нельзя принимать, пока не пришли оба (или не истёк дедлайн).
Дедуп заложен с первого дня
Даже если Meta и Google сейчас работают только клиентским пикселем, eventID для Meta и orderId для Google Ads сразу проставляются равными request_id заявки. Это делает переход на server-side CAPI и Enhanced Conversions бесшовным: серверное событие с тем же request_id дедуплицируется с браузерным автоматически. Заложить это в первой версии стоит примерно ноль усилий, а позже экономит переделку.
Из квалификации - в value-based bidding
Самая недооценённая часть паттерна: перехваченный ответ org-info содержит гораздо больше, чем нужно для бинарного «квалифицирован / нет». Тег обычно использует из него три поля (regStatus.code, orgForm.code, status.code), а доступны, например:
| Поле | Что даёт для перформанса |
|---|---|
размер организации (orgSize) | главный сигнал ценности лида → value-based bidding |
отрасль (oked, основной вид деятельности) | аудитории, lookalike, роутинг на нужный продукт |
| адрес (регион, город, КАТО) | гео-корректировки ставок и гео-отчётность |
| дата регистрации | возраст компании → отсечь неподходящих до конверсии |
| статус регистрации / активности | отсев ликвидированных и неактивных |
Отсюда - переход от «лид/не лид» к градуированной ценности. Из размера компании считается lead_value, обогащается dataLayer бизнес-атрибутами, и во все каналы уходит не value: 0, а расчётная ценность:
Дальше Google Ads переводится на Maximize Conversion Value / tROAS, в Meta событие Lead уходит с value, в Яндексе - order_price. Прокси-ценности по сегментам (микро / малый / средний / крупный) на старте берутся оценочно, а после первой выгрузки реальных сделок из CRM заменяются на фактическую ожидаемую маржу. Смысл в том, что интерсептор бесплатно отдаёт вам сигнал ценности, который иначе пришлось бы тянуть из бэкенда.
Инцидент: пассивный захват молча сломался
Теперь честно о хрупкости. Интерсептор пассивен - он читает те вызовы, которые делает сам сайт. Это его сила (не нужен свой бэкенд) и его слабость: вы жёстко связаны с фронтенд-контрактом чужой команды.
На одном из проектов разработчики клиента перевыпустили фронт: убрали govtech-вызовы со страницы и заменили сабмит на новую цепочку (OTP + другой gateway). Формально ничего не «упало» - сайт работал. Но интерсептор перехватывать стало нечего: квалификация перестала срабатывать, и конверсии полетели с value: 0. Хуже всего, что это произошло тихо - ни ошибки в консоли, ни падения тега, просто медленная деградация качества оптимизации, которую замечаешь по метрикам спустя время.
Выводы, которые стоит зашить в паттерн с самого начала:
- Мониторинг перехвата. Нужен алерт на аномалию: доля лидов с непустым
qualifiedрезко упала → фронтенд, скорее всего, изменился. - Переход от пассивного к активному захвату. Если сайт перестал сам дёргать нужный эндпоинт - тег может вызвать его сам (та самая backend-прокси / прямой запрос из ТЗ). Пассивный режим удобнее, но активный устойчивее к правкам фронта.
- Договорённость с командой фронтенда. Любой рефакторинг сабмита или govtech-логики должен проходить через уведомление аналитики. Технически интерсептор от вас не зависит - организационно он зависит от чужого релиз-процесса.
Ещё три вещи, которые легко упустить
Согласованность гейтинга по каналам. Классическая ошибка: часть тегов (например, Meta и Яндекс) на триггере сабмита сами перечитывают последний lead_iin_result и шлют событие только при qualified === true, а Google Ads и GA4 гейтятся только по form.status === success. Если фронтовый гейт для физлиц однажды даст сбой, Google получит неквалифицированные конверсии и начнёт оптимизироваться на мусор. Гейт qualified === true должен стоять на всех каналах одинаково.
Сырой ИИН/БИН в dataLayer - это PII. Перехваченный идентификатор удобно держать переменной, но для физлица ИИН - персональные данные. В конверсионные теги его передавать не нужно, а в dataLayer сырым лучше не хранить: если нужен для матчинга - хешировать (SHA-256) до отправки.
Consent Mode. У перехваченных тегов легко забыть про согласие (consentStatus: NOT_SET). Для финансовых и других регулируемых вертикалей стоит сразу закладывать Consent Mode v2 - хотя бы в формате готовности.
Когда применять, а когда нет
Fetch/XHR-интерсептор - правильный инструмент, когда выполнены два условия: вы не можете трогать фронтенд, и сайт уже сам вызывает эндпоинты с нужными вам данными. Тогда это самый короткий путь к квалификации и value-based bidding без бэкенда.
Но если вы контролируете код - не городите интерсептор поверх собственного приложения. Честный dataLayer.push из фронтенда или серверное событие из бэкенда надёжнее и не ломается от рефакторинга. Интерсептор - это тактическое решение для чужой поверхности, а не архитектурный дефолт. Его сила в том, что он даёт измеримую воронку там, где её иначе не было бы вовсе; его цена - постоянная связанность с чужим фронтенд-контрактом, которую нужно мониторить.
Частые вопросы
Почему обычный GTM не может отследить воронку в SPA?
Нет ни загрузки страницы, ни submit формы, ни DOM-события, которое соответствует нужному шагу. Сигнал квалификации живет в API-ответе, который получает приложение, а встроенные триггеры GTM его не видят.
Как работает перехват fetch/XHR?
Из GTM подменяются `fetch` и `XMLHttpRequest`, читается ответ, который приложение и так получает, а найденное отправляется в dataLayer. Дедупликацию нужно закладывать сразу: `eventID` для Meta и `orderId` для Google Ads ставятся равными `request_id` самого приложения, чтобы будущий переход на server-side CAPI не удвоил конверсии.
Когда так делать не стоит?
Когда push в dataLayer от фронтенд-команды — это одна задача в бэклоге, когда в payload есть чувствительные данные или когда мониторинг некому вести. Пассивный перехват ломается молча — изменение на фронтенде однажды сломало всю схему без единой ошибки, — поэтому нужен алерт на объем перехваченного потока.
