Серверные и клиентские события: что отправлять откуда
Клиентские события показывают взаимодействие с интерфейсом, серверные — подтверждённые операции системы. Клик по кнопке оплаты не равен полученным деньгам: браузер фиксирует намерение, а платёж подтверждает бэкенд по состоянию заказа или ответу платёжной системы.
Ниже — матрица источников, готовые синтетические сценарии и автономная проверка для Node.js. Материалы не содержат данных клиентов, действующих ключей или обещаний результата.
Скачать JSON со сценариями источников и повторной доставки · Скачать автономный проверяющий скрипт для Node.js
Матрица: какой источник подтверждает событие
У каждого факта должен быть основной источник истины. В ActionPulse источник определяется проверенным типом ключа, а не значением source в JSON или произвольным заголовком запроса.
| Факт | Событие | Основной источник | Что действительно означает |
|---|---|---|---|
| Человек начал оформление | checkout_started |
браузер | начало сценария в интерфейсе, но не успешная оплата |
| Форма регистрации заполнена | signup_completed |
браузер | пользователь дошёл до шага; аккаунт ещё должен подтвердить сервер |
| Аккаунт действительно создан | registered |
сервер | запись создана и подтверждена системой-источником |
| Заказ действительно оплачен | order_paid |
сервер | платёж подтверждён, а не просто инициирован |
| Операция оплаты завершена | payment_succeeded |
сервер | сервер получил подтверждённый результат операции |
| Средства возвращены | refund |
сервер | возврат подтверждён учётной или платёжной системой |
Браузерные пользовательские события дополнительно должны входить в разрешённый список соответствующего браузерного ключа. Критичные имена registered, order_paid, payment_succeeded и refund браузер отправлять не может: элемент будет отклонён с кодом credential_event_forbidden.
Правила именования, полный словарь и версии схемы остаются в материале про разметку событий B2B SaaS. Здесь задача уже другая: определить, кто вправе подтвердить факт и как отличить повтор доставки от нового события.
Один сценарий: браузер начал, сервер подтвердил оплату
Возьмём полностью синтетический заказ. Браузер отправляет checkout_started и фиксирует путь пользователя. После подтверждения оплаты сервер создаёт отдельное событие order_paid.
{
"credential_kind": "browser",
"event": "checkout_started",
"event_id": "11111111-1111-4111-8111-111111111111",
"user_id": "demo-user-001",
"session_id": "demo-session-001",
"time": "2026-08-25T09:00:00Z",
"props": {
"business_event_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"order_id": "demo-order-001"
}
}
Поле credential_kind в файле сценариев показывает, каким типом ключа выполнена отправка. В рабочем запросе это свойство нельзя использовать для назначения прав: ActionPulse определяет доверенный источник по проверенному ключу.
После успешной транзакции сервер отправляет подтверждённый факт:
{
"credential_kind": "server",
"event": "order_paid",
"event_id": "22222222-2222-4222-8222-222222222222",
"user_id": "demo-user-001",
"session_id": "demo-session-001",
"time": "2026-08-25T09:00:04Z",
"props": {
"business_event_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"order_id": "demo-order-001"
}
}
Результат проверки: два транспортных идентификатора, два продуктовых факта, одна подтверждённая оплата. Общий business_event_id связывает одну операцию, но не склеивает checkout_started и order_paid: имена разные, поэтому смысл и шаги воронки остаются раздельными.
Значения event_id и корректного business_event_id должны быть UUID. Формат и варианты идентификаторов определены в RFC 9562: Universally Unique IDentifiers. Идентификаторы в примере придуманы для демонстрации; пользователь, заказ и сессия не относятся к клиенту ActionPulse.
Почему одинаковый event_id не заменяет business_event_id
На практике нужно различать два уровня идентичности.
Транспортная идентичность включает проект, подтверждённый класс отправителя и event_id:
transport = project_id + producer_class + event_id
Повтор того же серверного события с тем же event_id сохраняет одну транспортную идентичность. Браузер и сервер с одинаковым event_id образуют две разные транспортные идентичности: браузер не должен заранее занять идентификатор и подавить доверенный серверный факт.
Продуктовая идентичность включает проект, имя события и либо корректный business_event_id, либо запасной event_id:
logical = project_id + event_name + business_event_id
logical = project_id + event_name + event_id, если business_event_id отсутствует
Отсюда следуют четыре проверяемых случая:
- Один серверный источник, один
event_id, две попытки доставки: один транспортный идентификатор и один продуктовый факт. - Браузер и сервер, одинаковое имя и одинаковый
event_id: два транспортных идентификатора и один продуктовый факт; при чтении канонической версии приоритет у сервера. - Браузер и сервер, разные
event_id, одинаковое имя и корректный общийbusiness_event_id: два транспортных идентификатора и один продуктовый факт; приоритет снова у сервера. checkout_startedиorder_paidс общимbusiness_event_id: два продуктовых факта, потому что шаг интерфейса и подтверждённая оплата отвечают на разные вопросы.
Если у одинакового факта разные event_id, а business_event_id отсутствует или не является UUID, канонического объединения не будет: получится два продуктовых факта вместо одного. Поэтому общий идентификатор бизнес-операции должен быть осознанной частью контракта, а не случайной строкой.
Повторная доставка: что действительно работает
Создавайте серверный event_id один раз в момент подтверждённого факта и используйте его во всех повторных попытках. Новый UUID при каждом retry превращает одну операцию в несколько самостоятельных событий.
Для ActionPulse /v1/track ключ дедупликации находится в теле события: это поле event_id. Заголовок Idempotency-Key не заменяет event_id для /v1/track. У отдельных операций, например отметки релиза из CI/CD, может быть собственный контракт с таким заголовком, но переносить его на приём аналитических событий нельзя.
Серверный ключ остаётся только на бэкенде. Не помещайте его в браузерный код, JSON-сценарий, URL, клиентские логи или текст ошибки. В скачиваемых материалах используются только названия классов browser и server, а не реальные учётные данные.
HTTP 202: почему нужно смотреть rejected
HTTP 202 Accepted не означает, что каждый элемент пакета принят. RFC 9110, раздел 15.3.3 отдельно объясняет, что обработка такого запроса может быть не завершена к моменту ответа.
Для ActionPulse статус нужно читать вместе с телом ответа. Если браузер отправил разрешённый checkout_started и запрещённый order_paid, ответ может выглядеть так:
{
"accepted": 1,
"n": 2,
"rejected": [
{
"i": 1,
"code": "credential_event_forbidden"
}
]
}
Итог: HTTP 202, accepted: 1, rejected: 1. Проверка только HTTP-статуса скрывает потерянную оплату. Обработчик должен разбирать rejected, сохранять безопасный диагностический код и передавать подтверждённый платёж с сервера.
Воспроизводимая проверка на десяти синтетических сценариях
Скачайте оба файла в один каталог:
- JSON-сценарии источников, повторов и канонического выбора.
- Автономный проверяющий скрипт без зависимостей.
Запустите проверку локально:
node actionpulse-event-source-checker.mjs actionpulse-event-source-fixtures.json
Проверка использует только стандартные модули Node.js, сверяет три заранее вычисленных контрольных значения SHA-256, ничего не отправляет по сети и не обращается к действующему проекту. Ожидаемый результат:
PASS 10/10 synthetic event-source scenarios
В набор входят повтор с тем же UUID, одинаковый UUID у браузера и сервера, общий бизнес-идентификатор, два разных шага одной оплаты, отсутствие и неправильный формат business_event_id, новый UUID при retry, запрещённая браузерная оплата, подмена source и частично отклонённый пакет с HTTP 202.
Этот скрипт проверяет контракт идентичности и синтетические ожидания; он не заменяет проверку настоящей интеграции, браузерных разрешений, реестра событий, сетевой доставки или конфигурации конкретного проекта.
Что проверить после внедрения
- У каждого бизнес-факта есть основной источник; подтверждённая оплата приходит только с сервера.
checkout_startedиorder_paidиспользуют разныеevent_id, общий корректныйbusiness_event_idи одногоuser_id.- Повторная отправка серверного факта сохраняет исходный UUID.
- Браузер не может назначить себе серверный источник через
sourceили заголовок. - Обработчик читает
rejected, даже когда HTTP-статус равен202. - Два действия с разным смыслом не объединяются только потому, что относятся к одному заказу.
- В событиях нет токенов, паролей, текста платёжной формы и лишних персональных данных.
Последовательность действий проверяйте в истории пользователя, идентификацию между браузером и сервером — в материале про идентификацию пользователей, а контроль пропусков и дублей — в статье о качестве данных продуктовой аналитики.
Итог
Браузер описывает интерфейсный путь, сервер подтверждает бизнес-результат. Назначьте основной источник, сохраните стабильный event_id для каждого повтора, связывайте этапы корректным business_event_id и проверяйте элементы rejected. Тогда воронка показывает отдельные шаги пользователя и ровно один подтверждённый факт там, где он действительно произошёл.