К содержанию
ActionPulse

Браузер и бэкенд вместе

Как разделить события между браузером и бэкендом, не получить дубли одного факта, склеить одного человека между двумя контурами и не отправлять оплату из браузера.

Кому
Командам, у которых события идут из двух контуров
Проверено
На этой странице

Как только у продукта появляется оплата, события начинают приходить из двух контуров: браузер знает, что человек нажал, а бэкенд знает, что на самом деле произошло. Эта страница — про то, как поделить между ними ответственность так, чтобы отчёты не считали один факт дважды и не считали ложные факты вообще.

Права здесь определяются типом ключа, а не тем, что указал вызывающий: браузерный токен публичен и лежит в разметке страницы, серверный ключ секретен. См. типы ключей приёма.

Кто что отправляет

Что Откуда Почему так
Просмотры, клики, прокрутка, формы браузер, автоматически это и есть поведение в интерфейсе
Шаги пути в интерфейсе, начало оформления браузер бэкенд об этих шагах не знает
Регистрация подтверждена сервер факт создаётся вашей базой
Оплата, заказ, счёт сервер подтверждение приходит от платёжного провайдера
Возврат, отмена, списание сервер часто происходит вообще без участия человека
Выдача и отзыв доступа, смена тарифа сервер решение принимает ваш бэкенд

Часть имён приём принимает только с серверным ключом:

registered, subscription_started, trial_started, trial_activated, trial_converted, trial_expired, payment_succeeded, payment_attached, order_created, first_order_created, order_paid, invoice_paid, refund.

Имена checkout_started и signup_completed в этот список намеренно не входят: это сигналы интерфейса, а не подтверждённые факты, и их отправляют из браузера.

Обратное правило тоже действует: серверный ключ не может отправлять имена из пространства pp.* — их формирует браузерный сбор.

Один факт — один источник

Простое правило: у каждого факта ровно один отправитель. Если канонический order_paid создаёт бэкенд, браузер такого события не отправляет вообще.

Причина техническая. Защита от дублей действует в пределах одного источника: одинаковый event_id, отправленный браузерным токеном и серверным ключом, даёт два разных события. Это сделано специально — иначе публичный браузерный токен мог бы занять event_id заранее и подавить настоящее серверное событие.

Если факт всё-таки виден с двух сторон

Отчёты и список событий читают данные так, что на один логический факт приходится одна запись. Логический факт — это имя события плюс свойство business_event_id (UUID). Когда двух записей с одинаковым логическим фактом несколько, побеждает более достоверный источник: серверная запись выигрывает у браузерной.

Поэтому, если один и тот же факт по каким-то причинам приходит из обоих контуров:

  • имя события должно быть одинаковым — иначе это два разных логических факта;
  • свойство business_event_id должно быть одинаковым и быть корректным UUID; некорректное значение просто игнорируется, и склейки не произойдёт;
  • транспортный event_id у каждой стороны свой — общий не даст ничего и создаст тот самый риск подавления.

Рекомендуемая схема

Штатный способ выглядит иначе и надёжнее: стороны отправляют события с разными именами и связывают их общим business_event_id.

{
  "event": "checkout_started",
  "event_id": "3f1b8c22-6e7a-4a5f-9b0d-1c2d3e4f5a6b",
  "anon_id": "…",
  "session_id": "…",
  "props": {
    "business_event_id": "9c4e77d0-2f31-4a8c-b6d5-0e1f2a3b4c5d",
    "correlation_id": "b1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
  }
}

Бэкенд после подтверждения транзакции отправляет order_paid со своим event_id, тем же business_event_id, тем же user_id и тем же session_id. Два имени — два разных факта, которые не мешают друг другу в воронке, но которые можно сопоставить по свойству. correlation_id — просто ваше свойство для сверки с собственными логами: ActionPulse не придаёт ему особого смысла.

Как склеить одного человека

Между контурами человека связывает user_id — ваш собственный идентификатор из вашей базы. Это не удобство, а обязательное условие: если браузер шлёт один user_id, а бэкенд другой, в отчётах будет два разных актора.

Что передавать с сервера:

Поле Когда Зачем
user_id всегда, когда человек известен единственная связь между контурами
anon_id если бэкенд его знает позволяет связать анонимную историю
session_id если фронтенд передал его вам сшивает серверный факт с сессией в браузере

Значение сессии берётся в браузере вызовом pp.getSessionId() и передаётся на бэкенд вместе с бизнес-данными. До получения согласия на сбор этот вызов возвращает undefined — сессии просто ещё нет.

Связывание анонимной истории

Пока человек не вошёл, у него только anon_id. Связать анонимную историю с user_id можно двумя способами:

  • Из браузера — вызовом identify(). Он отправляет запрос на /v1/identify, сохраняет связь и попутно создаёт событие identify.
  • С сервера — запросом на /v1/alias с телом {anon_id, user_id}. Он создаёт только связь и никакого события не порождает.

После создания связи события, у которых есть только anon_id, начинают приходить уже с подставленным user_id. Уже принятая история переписывается отдельной фоновой задачей, а не в момент ответа 202: считайте, что склейка прошлого применяется с задержкой, и не проверяйте её сразу же в отчёте. Подробно — идентификация пользователей.

События, о которых браузер не знает

Часть фактов вообще не имеет браузерного контекста: продление подписки по расписанию, возврат, оформленный поддержкой, заказ по телефону, уведомление от платёжного провайдера, отключение за неоплату. Их отправляет бэкенд, и это нормальная ситуация, а не пробел в разметке.

Что учесть:

  • Актор. Достаточно user_id. Придумывать anon_id и session_id для такого события не нужно: session_id необязателен.
  • Время. Поле time должно попадать в окно ±48 часов от текущего момента. Время вне окна не отклоняется — событию проставляется время приёма, то есть факт «переедет» на сегодня.
  • Старее окна приёма. Загрузить историю за прошлые месяцы обычным приёмом нельзя. Для этого есть загрузка данных.
  • Заголовки о происхождении. X-PP-SDK-Name, X-PP-Source и подобные — диагностика, а не права. Источник события определяется типом ключа. Попытка объявить себя внутренним продюсером приводит к отклонению всех элементов батча.

Типичная ошибка: оплата из браузера

Соблазн понятный: страница «Спасибо за покупку» уже открыта, вызвать оттуда track('order_paid') — одна строка. Так делать нельзя, и вот что произойдёт на практике.

  1. Приём ответит 202 — как и на любой корректно разобранный запрос.
  2. Внутри ответа элемент будет в массиве rejected с кодом credential_event_forbidden.
  3. Если код не проверяет rejected, во фронтенде всё выглядит успешным, а в отчётах оплат нет. Расхождение обнаружится через недели, когда воронка покажет нулевую конверсию последнего шага.

Даже если бы имя не было закрыто, страница благодарности не годится в источник факта оплаты: её перезагружают, открывают из истории браузера, присылают ссылкой. Событие оплаты должно рождаться там, где оплата подтверждена, — в обработчике уведомления от платёжного провайдера или после успешной транзакции в вашей базе.

Правильное разделение того же сценария:

  • браузер отправляет checkout_started при начале оформления;
  • бэкенд отправляет order_paid после подтверждения, с revenue в рублях и общим business_event_id;
  • воронка строится по шагам checkout_startedorder_paid.

Проверка

  1. В коде фронтенда нет ни одного имени из списка серверных событий и нет строки pp_sk_.
  2. Ответы приёма проверяются на непустой rejected — и в браузере, и на бэкенде.
  3. Один и тот же человек в отчётах — один актор: найдите себя в списке событий по user_id и убедитесь, что действия из браузера и с бэкенда лежат под одним актором.
  4. Одна оплата — одно событие: проведите тестовую оплату и убедитесь, что order_paid в списке событий ровно один.
  5. Повторная отправка того же серверного факта не увеличивает счётчик.

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