Назад в блог

Блог

События электронной торговли без платформы магазина: как заставить собственное оформление заказа отправлять данные в GA4

Опубликовано · Обновлено · 5 мин чтения

Платформа магазина даёт вам хотя бы плагин, с которым можно воевать. Оформление заказа, написанное вами самими, — приложение на React, процесс бронирования, регистрация в SaaS со Stripe за ней — не даёт ничего: ни одно событие не сработает, пока его не отправит ваш собственный код. Хорошая новость в том, что контракт невелик: горстка имён, одна форма объекта и несколько правил о том, где место каждой отправки. Здесь он целиком, включая часть, которую опускает большинство руководств, — покупки, которые происходят, когда на вашей странице никого нет.

Контракт: имя и объект ecommerce

Всё, о чём здесь идёт речь, попадает в Analytics одним и тем же путём. Ваша страница отправляет в window.dataLayer событие под его именем в GA4, с объектом ecommerce в форме GA4. Триггер Tag Manager срабатывает на это имя, а тег события GA4 читает объект и передаёт его дальше, в ваш серверный контейнер. У каждого контейнера SignalHost эти триггеры и теги уже есть в веб-контейнере, с префиксом SH в названии, — для событий магазина ниже, а также для регистраций, пробных периодов и заявок. Остаётся только отправка.

Два правила отсекают большинство ошибок. Сначала очищайте предыдущий объект ecommerce, иначе view_item трёхстраничной давности поедет внутри следующего события. И в каждом событии с денежной суммой передавайте value числом, а currency — кодом ISO: value строкой или отсутствующая currency принимаются без единой жалобы и молча не попадают в выручку.

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
  event: 'begin_checkout',
  ecommerce: {
    currency: 'EUR',
    value: 49.00,
    items: [
      { item_id: 'plan-pro', item_name: 'Pro', price: 49.00, quantity: 1 }
    ]
  }
});

Какое событие и где его место

В собственном процессе те же моменты, что и на платформе магазина. Просто искать их придётся в своём коде.

  • view_item_list — страница или компонент, где показано несколько товаров или тарифов. Один раз, когда список показан, а не при каждой прокрутке.
  • view_item — страница одного товара или тарифа.
  • add_to_cart и remove_from_cart — в обработчике кнопки, после того как ваша корзина действительно изменилась.
  • view_cart — когда показана корзина: её страница или открытие выдвижной панели корзины.
  • begin_checkout — момент, когда посетитель входит в оформление заказа: первая отрисовка страницы оформления или клик, открывающий диалог оформления.
  • add_shipping_info — когда выбран способ доставки. Опустите его, если вы ничего не доставляете.
  • add_payment_info — когда платёжные данные приняты, а не когда появилась форма.
  • purchase — один раз, когда заказ подтверждён. Большая часть этой статьи посвящена тому, чтобы не ошибиться именно с ним.
  • sign_up и start_trial — для бизнеса на подписках это конверсии до того, как деньги перейдут из рук в руки. SignalHost создаёт теги для обоих наряду с событиями магазина, а также для заявок: generate_lead, contact, schedule, submit_application и subscribe.

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

Покупка на странице подтверждения: берите заказ со своего сервера

Если после оплаты посетитель возвращается на вашу страницу, именно там место purchase — при двух условиях. Берите заказ из собственного бэкенда, а не из URL: параметры запроса можно отредактировать, редиректы их отбрасывают, а приходят они и при неудавшихся платежах. Stripe, например, возвращает на один и тот же адрес независимо от того, прошёл платёж или нет, а что именно произошло, сообщает в собственном параметре. И отправляйте событие один раз: перезагрузка, кнопка «Назад» или страница чека в закладках — всё это возвращает на неё.

// On your confirmation page. The order comes from YOUR server, by an id the
// page already knows — never from price or status parameters in the URL.
const order = await fetch(`/api/orders/${orderId}`).then((r) => r.json());

const key = `purchase-sent-${order.id}`;
if (order.status === 'paid' && !localStorage.getItem(key)) {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({ ecommerce: null });
  window.dataLayer.push({
    event: 'purchase',
    ecommerce: {
      transaction_id: order.id,
      value: order.total,
      tax: order.tax,
      currency: order.currency,
      items: order.items.map((i) => ({
        item_id: i.sku, item_name: i.name, price: i.price, quantity: i.quantity
      }))
    }
  });
  localStorage.setItem(key, '1');
}

transaction_id — страховочная сетка, а не план. Analytics отбрасывает повторный transaction_id в пределах одной сессии, но не перезагрузку страницы чека на следующее утро.

Оплата на чужой странице: Stripe Checkout, PayPal, перенаправление в банк

Размещённая платёжная страница — обычный выбор для собственной разработки, и у неё есть один острый угол: посетитель может так и не вернуться. Он платит, закрывает вкладку, и ваша страница подтверждения так и не отрисовывается. С картами это несколько процентов покупок. С банковскими переводами, счетами и способами «купи сейчас — плати потом» это может быть большинство из них, потому что деньги приходят через часы или дни.

Поэтому для каждого способа оплаты решите, где находится истина. Если ваш бэкенд узнаёт о платеже из вебхука, вебхук и есть надёжное место, чтобы о нём сообщить (см. следующий раздел), — и тогда страница подтверждения не должна отправлять purchase ещё раз, иначе каждый платёж картой будет посчитан дважды. Если узнать о платеже можно только на странице возврата, сообщайте о нём там и смиритесь с пробелом.

Покупки без браузера: отправляйте их со своего сервера

С подписками этого не избежать. Пробный период переходит в платную подписку через семь дней, продление происходит каждый месяц, счёт оплачивают переводом на следующей неделе. Каждый из этих случаев — покупка, и ни один не происходит перед браузером. Решение — отправлять событие с сервера на сервер, из вебхука, который узнаёт о платеже, на ту же конечную точку тегинга, которую используют ваши страницы, — ваше имя хоста SignalHost или ваш собственный домен, — чтобы оно проходило через серверный контейнер, как любое другое обращение.

При отправке покупок с сервера сохраняйте фактические решения посетителя о согласии вместе с заказом. Отсутствие файла cookie `_ga` не доказывает отказ. Этот пример отправляет данные только в Analytics и лишь при наличии сохранённого разрешения и существующего идентификатора клиента; реклама остаётся запрещённой. Для отправки данных рекламным платформам нужно отдельно сохранить соответствующие разрешения.

// At checkout: save the visitor's actual banner choice with the order.
// Cookie presence alone is NOT consent. This example is Analytics-only.
const ga = req.cookies._ga;
order.gaClientId = ga ? ga.split('.').slice(-2).join('.') : null;
order.analyticsGranted = req.body.consent?.analytics === true;

// Later, in the payment webhook: only send with recorded permission and id.
if (order.analyticsGranted === true && order.gaClientId) {
  const params = new URLSearchParams({
    v: '2',
    tid: 'G-XXXXXXXXXX',
    cid: order.gaClientId,
    gcs: 'G101', // Analytics allowed; advertising stays denied.
    en: 'purchase',
    'ep.transaction_id': invoice.id,
    'epn.value': '49.00',
    cu: 'EUR',
    pr1: 'idplan-pro~nmPro~pr49.00~qt1',
  });
  await fetch(`https://metrics.example.com/g/collect?${params}`, { method: 'POST' });
}

Выберите один источник для каждого события и придерживайтесь его. Именно так signalhost.io измеряет собственные продажи: браузер отправляет sign_up, begin_checkout и add_payment_info; наш сервер отправляет start_trial, как только пробный период действительно начался, так что закрытая вкладка не может его потерять; а вебхук Stripe отправляет purchase, когда оплачен первый счёт. Ни у одного события нет двух источников. Measurement Protocol от Google тоже подошёл бы, но он идёт прямо в Google в обход вашего серверного контейнера, так что ни один ваш серверный тег эту продажу так и не увидит.

Поздние покупки — одна из четырёх причин, по которым GA4 насчитывает меньше, чем ваш список заказов: остальные три и какие из них можно исправить.

Настроить cookie-баннер

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

**Basic:** блокируйте код отслеживания до получения необходимого согласия. До этого в SignalHost не ожидаются события отслеживания.

**Advanced:** теги Google загружаются со значениями `denied` по умолчанию. Google может получать измерения без cookie до выбора или после отказа. Такой сигнал не является согласием.

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

[Руководство по настройке баннера cookie](/ru/blog/cookie-banner-consent-mode-server)

Как проверить, что всё работает

  • Откройте контейнер в SignalHost. Карточка событий показывает, что пришло, по именам, вместе с долей обращений, в которых было согласие. Имя, которое вы отправили, но не видите там, так и не дошло до вашего сервера тегинга.
  • Пройдите оформление заказа в режиме предпросмотра Tag Manager. Он показывает каждую отправку, какой тег сработал и какой объект ecommerce был отправлен.
  • В Analytics события по мере поступления показывает DebugView. Стандартные отчёты могут занять сутки.
  • Перезагрузите страницу подтверждения дважды и посчитайте покупки. Покупка по-прежнему должна быть одна.

Все сбои здесь тихие: имя не в том регистре, value, отправленное строкой, items, вложенные на уровень глубже, чем нужно. Ни один из них нигде не выдаёт ошибку. Каждый из них выдаёт отчёт с дырой.

Перестаньте терять данные из-за блокировщиков

Начните бесплатно. Запустите первый контейнер за несколько минут.