Сквозная аналитика MAX на Yandex Cloud: как связать рекламный визит с обращением и сделкой

Сквозная аналитика MAX на Yandex Cloud: как связать рекламный визит с обращением и сделкой

⚠ В этой статье я просто делюсь своим практическим опытом и показываю, как решил конкретную задачу.

Ниже описан пошаговый план реализации, но это концептуальный разбор того, что подобная интеграция в принципе возможна, а не универсальная инструкция под любой бизнес.

У каждого проекта свой стек: разные коннекторы мессенджеров (Wazzup, Chat2Desk, Открытые линии), разные CRM и свои кастомные поля.

Обратите внимание: предложенный код универсален — он рассчитан не только на MAX, но и на Telegram и WhatsApp, а помимо metrika_client_id автоматически передает ещё и параметр roistat_visit.

Если какие-то из этих мессенджеров или сервисов аналитики вы не используете — самостоятельно почистите и адаптируйте код под свой проект, чтобы всё работало ровно так, как вы ожидаете.

Недавно я столкнулся с классической задачей: нужно было связать обращения из мессенджера MAX с источниками трафика на сайте UTM-метками, ClientID Яндекс Метрики и Roistat ID и передать эти данные в CRM Битрикс24.

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

Но для моего сценария переход с сайта непосредственно в чат с менеджером в MAX с последующей передачей источника в CRM готового решения, которое закрывало бы задачу целиком, я не нашел.

В других мессенджерах подобные задачи часто решаются за счёт передачи идентификатора прямо в ссылке или первом сообщении. В MAX для ботов есть механизм start-параметров, а для шеринга текста - отдельный диплинк :share?text=...

Но в моём случае требовалось сохранить исходные данные рекламного визита и связать их именно с последующим обращением пользователя в MAX.

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

Почему для этой задачи я выбрал Yandex Cloud?

1. Низкая стоимость. При небольших объёмах такой стек может полностью укладываться в бесплатные лимиты Yandex Cloud. Для Cloud Functions предоставляется 1 млн вызовов и 10 GB×hour вычислений в месяц, а для Object Storage - бесплатные лимиты на хранение и операции. Yandex Message Queue также входит в Free Tier.

2. Минимум инфраструктурного обслуживания. Не нужно поддерживать постоянно работающий сервер, обновлять ОС, следить за Docker-контейнерами и самостоятельно восстанавливать упавшие процессы.

3. Гибкость. Можно реализовать собственную логику сопоставления визита, клика по MAX и сделки в CRM.

Как я решил проблему атрибуции MAX: Логика работы

Раз я не могу передать ID в самом сообщении MAX, я решил связывать визит и сделку по временному окну:

  1. Фиксация визита (при входе на сайт): При загрузке страницы JS-скрипт генерирует уникальный visit_id и сохраняет полный архив сессии (UTM-метки, clientID, roistat_visit, тип устройства, реферер) в S3-бакет в папку visits/visit_id.json.
  2. Логирование клика (при нажатии на MAX): При клике на кнопку перехода в мессенджер скрипт отправляет легкий сигнал, где фиксирует метку времени (ts) и тот самый visit_id этого пользователя. Этот лог сохраняется в S3 в папку clicks/max_timestamp_visitId.json.
  3. Переход в мессенджер: Пользователь переходит в MAX и начинает диалог. Интеграция (Wazzup, Chat2Desk, Открытые линии) создает Сделку в Битрикс24 с источником MAX.
  4. Обработка вебхука и склейка: Битрикс24 отправляет вебхук в Yandex Cloud Function. Функция проверяет папку clicks/ (где хранятся логи только тех, кто реально кликнул на MAX, а не всех посетителей сайта), берет свежий клик, считывает зафиксированный в нем visit_id и по этому ключу забирает из visits/visit_id.json точный файл конкретного визита. После этого обработанный клик сразу удаляется из S3, исключая ошибки и повторные совпадения.

Разворачиваем бэкенд в Yandex Cloud

Шаг 1. Регистрация и активация платежного аккаунта

  1. Перейдите на официальный сайт https://yandex.cloud/ru и войдите под своим Яндекс ID.
  2. Перейдите в Консоль управления (кнопка в правом верхнем углу).
  3. Создайте Платежный аккаунт (Billing Account) в разделе «Биллинг».

Шаг 2. Создаем сервисный аккаунт

Для того, чтобы все функции работали без вашего участия, необходимо создать сервисный аккаунт: Перейдите в «Identity and Access Management» и создайте аккаунт

Задайте имя для сервисного аккаунта, например analytics-sa и назначьте для него следующие роли:

  • storage.editor (для записи и чтения файлов S3)
  • ymq.writer и ymq.reader (для работы с очередью)
  • functions.functionInvoker (для вызова функций)

Создайте статический ключ доступа (Access Key / Secret Key) для этого аккаунта - он понадобится для переменных окружения.

❗ Сохраните ключ доступа в удобном месте, он пригодится на этапе создания переменных окружения Cloud Functions.

Шаг 3. Создаем очередь в Message Queue

  • В левом меню консоли Yandex Cloud перейдите в раздел Message Queue.
  • Нажмите «Создать очередь».
  • Имя: bitrix-lookup-queue
  • Тип очереди: Стандартная.
  • Стандартный таймаут видимости: 30 секунд (важно: это время должна отрабатывать ваша фоновая функция №2 до того, как сообщение снова станет видимым в очереди).
  • Срок хранения сообщений: 4 дня (по умолчанию).
  • Время ожидания при получении: 20 секунд (по умолчанию).

Остальные параметры оставьте по умолчанию и нажмите Создать

❗ После создания откройте очередь и скопируйте её URL (он выглядит так: https://message-queue.api.cloud.yandex.net/b1g.../dj6.../bitrix-lookup-queue).

Этот URL вы подставите в переменные окружения Cloud Functions под именем YMQ_QUEUE_URL

Шаг 4. Создаем бакет в Object Storage

Для того чтобы сохранять JSON-файлы с визитами пользователей и кликами по кнопкам, нужно создать бакет в сервисе Object Storage.

Как настроить бакет:

  1. В консоли Yandex Cloud в левом меню перейдите в Object Storage и нажмите «Создать бакет».
  2. Имя бакета: Задайте уникальное имя латинскими буквами (например, analytics-data-bucket).
  3. Максимальный размер: Можно ограничить (например, 10 ГБ). JSON-файлы логов весят килобайты, поэтому 10 ГБ хватит на сотни тысяч визитов.
  4. Класс хранилища: Выберите Стандартное.
  5. Для раздела «Доступ» в настройках создания бакета для всех операций (чтение объектов, чтение списка объектов, чтение настроек) выберите «с авторизацией», чтобы защитить данные пользователей и UTM-метки от публичного доступа в интернете. Cloud Function будет безопасно читать и записывать файлы через секретные ключи сервисного аккаунта.

Шаг 4.1. Загружаем скрипт для будущих функций Cloud Functions в Object Storage

Yandex Cloud Functions позволяет загружать код функции напрямую из файла ZIP-архива, сохраненного в вашем бакете.

Чтобы скачать архив нажмите тут

⚠ Важно: Внутри файла index.js указаны стандартные текстовые заглушки.

Чтобы скрипт корректно записывал данные в вашу CRM, вам нужно указать ID пользовательских полей из вашего Битрикс24.

Создаем поле для Client ID Яндекс Метрики в Битрикс24: Перейдите в Битрикс24: CRM → Настройки → Настройки CRM → Настройки полей → Сделка (список общих полей).

Нажмите Добавить поле: Название: metrika_client_id (или любое удобное вам).

Показывать в фильтре: Да.

Сохраните поле, а затем нажмите на него, чтобы открыть окно редактирования.

Посмотрите на адресную строку браузера (URL). Ссылка будет выглядеть примерно так: https://project.bitrix24.ru/crm/configs/fields/CRM_DEAL/edit/UF_CRM_1773915910/

Значение UF_CRM_1773913410 — это уникальный код вашего поля в CRM.

В файле index.js найдите строку: const ymField = process.env.BITRIX_YM_FIELD || 'UF_CRM_YM_CLIENT_ID'; И замените заглушку 'UF_CRM_YM_CLIENT_ID' на полученный код поля: const ymField = process.env.BITRIX_YM_FIELD || 'UF_CRM_1773913410';

Если вам нужно передавать параметр roistat_visit, выполните аналогичную операцию: создайте поле в Битрикс24, скопируйте его ID и подставьте в строку: const roistatField = process.env.BITRIX_ROISTAT_FIELD || 'UF_CRM_ROISTAT_VISIT';

Если сервис Roistat вы не используете — просто оставьте эту строку как есть или удалите блок кода, связанный с Roistat.

После того, как вы внесли изменения в файл index.js, необходимо заархивировать файлы index.js и package.json обратно и загрузить архив в Object Storage

  1. Зайдите созданный бакет.
  2. Нажмите кнопку «Загрузить».
  3. Выберите ваш файл function.zip и загрузите его.

Шаг 5. Создаем функции в Cloud Functions

Теперь, когда у нас подготовлены хранилище Object Storage с архивом function.zip, очередь сообщений YMQ и сервисный аккаунт, переходим к разворачиванию бэкенда.

В нашей архитектуре используются две функции (обе поднимаются из одного и того же архива function.zip, но имеют разные точки входа и настройки):

  1. HTTP-приемник (index.handler): Общедоступная функция. Принимает логи визитов/кликов с сайта и быстрые исходящие вебхуки от Битрикс24.
  2. Обработчик очереди (index.queueHandler): Приватная фоновая функция. Запускается триггером очереди сообщений, делает паузы, ищет визиты в CRM и обогащает сделки.

Шаг 5.1. Создаем Функцию №1: HTTP-приемник (analytics-http-handler)

В левом меню Yandex Cloud перейдите в Cloud Functions и нажмите «Создать функцию».

Имя: analytics-http-handler

Среда выполнения: Node.js 22.

Способ добавления кода: выберите «Из Object Storage».

Бакет: выберите ваш бакет (созданный на шаге 4).

Объект: укажите function.zip (загруженный на шаге 4.1).

Точка входа: index.handler. Таймаут: 3–5 секунд (эта функция должна отвечать мгновенно).

Память: 128 МБ. Сервисный аккаунт: выберите созданный analytics-sa.

Важно: Переключите тумблер «Публичная функция» в положение Включено.

❗ После того как вы создадите функцию - зайдите в нее, найдите поле «Ссылка для вызова» и сохраните его значение, оно будет в формате https://functions.yandexcloud.net/YOUR_FUNCTION_ID

Эта ссылка потребуется на следующих этапах: создание исходящего вебхука в битрикс24 и использование его в скрипте который будет установлен на сайте

Сквозная аналитика MAX на Yandex Cloud: как связать рекламный визит с обращением и сделкой

Шаг 5.2. Создаем функцию №2: Обработчик очереди (analytics-queue-processor)

Снова нажмите «Создать функцию».

Имя: analytics-queue-processor.

Среда выполнения: Node.js 22.

Способ добавления кода: «Из Object Storage» (укажите ваш бакет и function.zip).

Точка входа: index.queueHandler.

Таймаут: 30 секунд (функция выполняет повторные запросы в CRM, ей нужно время).

Память: 256 МБ.

Сервисный аккаунт: analytics-sa.

Важно: Тумблер «Публичная функция» оставьте Выключенным (вызывать эту функцию напрямую из интернета не нужно).

Шаг 5.3. Создаем исходящий вебхук в Битрикс24

  • В Битрикс24 перейдите: Разработчикам → Другое → Исходящий вебхук.
  • Адрес обработчика: укажите публичный URL вашей Функции №1 (HTTP-приемника) с добавлением /bitrix-hook на конце:https://functions.yandexcloud.net/YOUR_HTTP_FUNCTION_ID/bitrix-hook
  • События: выберите из списка: Создание лида (ONCRMLEADADD), Создание сделки (ONCRMDEALADD), Обновление лида (ONCRMLEADUPDATE), Обновление сделки (ONCRMDEALUPDATE)
  • Нажмите Сохранить

Шаг 5.4. Настройка переменных окружения

В настройках каждой из двух функций прокрутите вниз до блока «Переменные окружения» и добавьте следующие ключи:

Название: BITRIX_WEBHOOK_URL / Значение переменной: URL входящего вебхука из Битрикс24 (получен на шаге 5.3)

Название: BUCKET_NAME / Значение переменной: Имя вашего бакета в Object Storage

Название: YMQ_QUEUE_URL / Значение переменной: URL очереди Yandex Message Queue (получен на шаге 5.1)

Название: YC_S3_KEY_ID / Значение переменной: Key ID статического ключа сервисного аккаунта (получен на шаге 2)

Название: YC_S3_SECRET_KEY / Значение переменной: Secret Key статического ключа сервисного аккаунта (получен на шаге 2)

Название: BITRIX_YM_FIELD / Значение переменной: Код поля Битрикс24 для Client ID (например, UF_CRM_1773915910)

Название: BITRIX_ROISTAT_FIELD / Значение переменной: Код поля Битрикс24 для Roistat ID

Название: ENABLE_DEBUG_LOGS / Значение переменной: true (включает подробные логи в консоли Yandex Cloud для отладки)

Шаг 5.5. Настройка триггера для функции 2

Чтобы сообщения из очереди YMQ автоматически отправлялись на обработку в функции №2:

В консоли Yandex Cloud в левом меню перейдите в Cloud Functions → раздел «Триггеры».

Нажмите «Создать триггер».

Имя: analytics-queue-trigger

Очередь: выберите созданную ранее очередь bitrix-lookup-queue.

Сервисный аккаунт: выберите ваш сервисный аккаунт (analytics-sa).

Время ожидания: 0 минут (сообщения должны уходить на обработку без задержек).

Размер группы сообщений: 1 (каждая сделка обрабатывается отдельно сразу при появлении).

Функция: выберите Функцию №2 (analytics-queue-processor).

Сервисный аккаунт: выберите analytics-sa.

Нажмите «Создать триггер».

Шаг 6. Установка скрипта на сайт

Финальный этап — подсоединение нашего сайта к созданной экосистеме Yandex Cloud.

Скрипт устанавливается в код всех страниц сайта перед закрывающим тегом : </head>

<script> /** * Сквозная аналитика для мессенджеров (WhatsApp, Telegram, MAX) */ (function () { // Укажите здесь публичный URL вашей Функция №1 из Yandex Cloud var BACKEND = 'https://functions.yandexcloud.net/YOUR_FUNCTION_ID'; var STORAGE_KEY = 'analytics_visit_id'; var GREETING = 'Здравствуйте! Хочу задать вопрос, мой номер обращения ID-'; function uuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (Math.random() * 16) | 0; var v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } function getParam(name) { var m = new RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search); return m ? decodeURIComponent(m[1].replace(/\+/g, ' ')) : ''; } function getCookie(name) { var matches = document.cookie.match(new RegExp( '(?:^|; )' + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)' )); return matches ? decodeURIComponent(matches[1]) : ''; } function getVisitId() { var id = localStorage.getItem(STORAGE_KEY); if (!id) { id = uuid(); localStorage.setItem(STORAGE_KEY, id); } return id; } function sendVisit(id) { var payload = { id: id, roistat_visit: getCookie('roistat_visit'), ym_client_id: getCookie('_ym_uid'), utm_source: getParam('utm_source'), utm_medium: getParam('utm_medium'), utm_campaign: getParam('utm_campaign'), utm_content: getParam('utm_content'), utm_term: getParam('utm_term'), referrer: document.referrer || '', landing: window.location.href, device: /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) ? 'mobile' : 'desktop', ts: Date.now() }; var blob = new Blob([JSON.stringify(payload)], { type: 'text/plain' }); if (navigator.sendBeacon) { navigator.sendBeacon(BACKEND, blob); } else { fetch(BACKEND, { method: 'POST', mode: 'cors', credentials: 'omit', headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify(payload), keepalive: true }).catch(function () {}); } } function sendMaxClick(id) { var payload = { type: 'max', visit_id: id, ts: Date.now(), roistat_visit: getCookie('roistat_visit'), ym_client_id: getCookie('_ym_uid'), utm_source: getParam('utm_source'), utm_medium: getParam('utm_medium'), utm_campaign: getParam('utm_campaign'), utm_content: getParam('utm_content'), utm_term: getParam('utm_term'), landing: window.location.href }; var blob = new Blob([JSON.stringify(payload)], { type: 'text/plain' }); if (navigator.sendBeacon) { navigator.sendBeacon(BACKEND, blob); } else { fetch(BACKEND, { method: 'POST', mode: 'cors', credentials: 'omit', headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify(payload), keepalive: true }).catch(function () {}); } } function patchMessengerLinks(id) { document.addEventListener('click', function (e) { var a = e.target.closest ? e.target.closest('a') : null; if (!a || !a.href) return; var isWhatsApp = /wa\.me|api\.whatsapp\.com|whatsapp\.com/i.test(a.href); var isTelegram = /t\.me|telegram\.me/i.test(a.href); var isMax = /max\.ru/i.test(a.href); if (!isWhatsApp && !isTelegram && !isMax) return; if (isMax) { // MAX не поддерживает ?text= -> фиксируем клик на бэкенд sendMaxClick(id); return; } // Для WA и TG подставляем ID в текст try { var url = new URL(a.href); var existing = url.searchParams.get('text') || ''; var marker = 'номер обращения ID-'; if (existing.indexOf(marker) === -1) { var msg = GREETING + id; url.searchParams.set('text', msg); a.href = url.toString(); } } catch (err) {} }, true); } var visitId = getVisitId(); sendVisit(visitId); patchMessengerLinks(visitId); })(); </script>
❗️ var BACKEND = 'https://functions.yandexcloud.net/YOUR_FUNCTION_ID'; Замените на значение полученное на шаге 5.1.

Проверяем работу

После того как все компоненты настроены, проведите итоговое тестирование цепочки:

  1. Зайдите на ваш сайт в режиме Инкогнито (желательно добавить тестовую метку к ссылке, например: ?utm_source=test_yandex).
  2. Откройте панель разработчика браузера (клавиша F12) и перейдите во вкладку Сеть (Network).
  3. В списке запросов вы должны увидеть успешное обращение к вашей Yandex Cloud Function вида functions.yandexcloud.net/YOUR_FUNCTION_ID со статусом 200 OK.
  4. Перейдите в консоль Yandex Cloud → Object Storage → ваш бакет → папку visits/. Там должен появиться новый .json файл с сохраненной сессией и вашими UTM-метками.
  5. Вернитесь на сайт, нажмите кнопку перехода в MAX и напишите первое сообщение в чат компании.
  6. Зайдите в Битрикс24 и откройте созданную Сделку. Через пару секунд в карточке автоматически заполнятся поля UTM-меток, Client ID Метрики, а в комментариях появится системный отчет сквозной аналитики