Почему GA4 врет на 30% при оплате через Kaspi? Чиним атрибуцию транзакций с помощью Server-Side GTM и Cloud Functions
Главная боль казахстанского e-commerce
Если вы делаете e-commerce в Казахстане, у вас 100% прикручена оплата через Kaspi. Это стандарт де-факто, монополия и просто удобно для юзера. Но для маркетологов и веб-аналитиков Kaspi Pay — это персональный филиал ада на земле.
Представьте картину: вы заливаете бюджет в Google Ads, таргетолог настраивает идеальные кампании, люди кликают, покупают. Вы заходите в Google Analytics 4 (GA4), чтобы посмотреть ROAS (окупаемость рекламы) и выписать таргетологу премию. А там — сюрприз! 30% всех транзакций имеют источник Direct / (none) или, что еще смешнее, Referral / pay.kaspi.kz. Google Ads показывает, что он принес 3 копейки, хотя в реальности продажи идут именно оттуда.
Таргетолог плачет, маркетолог пьет валерьянку, бизнес режет косты на рекламу (которая на самом деле работала). Почему так происходит? И главное — как это починить, не переписывая половину бэкенда?
Анатомия разрыва сессии: Куда пропадает атрибуция?
Давайте по шагам разберем типичный User Journey (путь пользователя) при покупке с мобильного телефона:
- Пользователь видит рекламу в Google (у него есть
gclidи рекламный источник). Кликает, переходит на сайт. В GA4 стартует сессия, фиксируетсяclient_id(уникальный идентификатор браузера) иsession_id. - Юзер кладет товар в корзину, заполняет данные доставки и нажимает «Оплатить через Kaspi».
- Точка невозврата: Сайт редиректит пользователя в мобильное приложение Kaspi. Браузер сворачивается (или закрывается).
- Пользователь подтверждает оплату Face ID, видит зеленую галочку в приложении Kaspi.
- А дальше развилка:
- Сценарий А (Оптимистичный, 70% случаев): Юзер нажимает «Вернуться в магазин». Приложение Kaspi открывает браузер, юзер попадает на страницу
/checkout/success. Но браузер может открыть новую вкладку, или контекст теряется, и GA4 засчитывает этот переход как новую сессию с источникомpay.kaspi.kz(реферал). Да, можно добавитьkaspi.kzв Referral Exclusion List, но это спасает не всегда. - Сценарий Б (Пессимистичный, 30% случаев): Юзер просто смахивает приложение Kaspi и кладет телефон в карман. Товар-то уже куплен, зачем возвращаться на сайт? В итоге транзакция есть в вашей базе данных, деньги на счету, а в GA4 события
purchaseпросто нет.
- Сценарий А (Оптимистичный, 70% случаев): Юзер нажимает «Вернуться в магазин». Приложение Kaspi открывает браузер, юзер попадает на страницу
Именно из-за Сценария Б ваша аналитика врет на треть. Вы теряете самые горячие лиды и убиваете алгоритмы машинного обучения Google Ads, потому что они не получают сигналы об успешных конверсиях.
Костыли, которые не работают
Обычно «сеньоры» из диджитал-агентств предлагают два решения:
- Measurement Protocol (MP) напрямую с бэкенда. Вроде логично: после успешной оплаты бэкенд шлет POST-запрос в GA4. Но проблема в том, что бэкенд обычно не знает
client_idиsession_id. В итоге он шлет транзакцию от «нового» пользователя, и она падает в Direct. - Webhooks + Zapier/Make. Дорого, медленно и не секьюрно. Передавать данные о транзакциях (с суммами и товарами) через сторонние no-code сервисы — так себе идея для энтерпрайза.
Идеальная архитектура: Webhooks + Cloud Functions + sGTM
Чтобы раз и навсегда решить эту проблему, нам нужно построить асинхронный пайплайн, который не зависит от того, вернулся пользователь на сайт или нет. Транзакция должна отправляться в GA4 сервером в момент фактического поступления денег (по вебхуку от Kaspi).
Архитектура выглядит так:
- Фронтенд (Создание заказа): Когда юзер жмет «Оформить заказ», фронтенд отправляет на бэкенд не только товары, но и куки аналитики:
_ga(содержит client_id) иsession_id. Бэкенд сохраняет их в таблице заказа (Orders) в базе данных. - Бэкенд (Ожидание): Бэкенд создает заказ со статусом
pendingи отдает ссылку на оплату. - Kaspi (Оплата): Юзер платит в приложении. Kaspi отправляет защищенный HTTP Webhook на наш сервер: «Заказ #12345 успешно оплачен».
- Cloud Function (Обогащение): Чтобы не грузить основной монолит бэкенда аналитикой, мы поднимаем микросервис на Google Cloud Functions. Он принимает вебхук, лезет в БД, находит заказ #12345, достает оттуда
client_idиsession_id, формирует JSON с данными о товарах. - Server-Side GTM (Отправка): Cloud Function отправляет этот JSON в наш серверный Google Tag Manager (sGTM). sGTM уже заботливо формирует правильный запрос Measurement Protocol и пушит его в GA4.
Шаг 1: Захват Client ID и Session ID на фронтенде
Без этих двух параметров GA4 не сможет склеить транзакцию с кликом по рекламе. Достаем их из куки и localStorage (в зависимости от настроек GA4) перед отправкой заказа на бэкенд.
BigQuery ML против маркетплейсов: Как собрать свою рекомендательную систему для e-commerce в Казахстане за выходные
// Функция для извлечения client_id из куки _ga
function getGAClientId() {
const match = document.cookie.match(/(?:^|;)s*_ga=([^;]*)/);
if (match) {
// Кука выглядит так: GA1.1.123456789.1600000000
// Нам нужны последние две части
const parts = match[1].split('.');
return parts.length === 4 ? `${parts[2]}.${parts[3]}` : null;
}
return null;
}
// Отправляем заказ на бэкенд
async function createOrder(cartItems) {
const payload = {
items: cartItems,
ga_client_id: getGAClientId(),
ga_session_id: sessionStorage.getItem('ga_session_id') // Или достаем из gtag('get', ...)
};
await api.post('/orders', payload);
}Совет: Если вы используете свежий gtag.js, надежнее всего получать параметры через асинхронный метод gtag('get', 'G-XXXXXXX', 'client_id', (id) => {...}).
Шаг 2: Google Cloud Function для обработки вебхука
Мы используем Serverless-функцию. Она масштабируется в ноль, стоит копейки и не падает, если Kaspi решит прислать нам 1000 вебхуков в секунду (такое бывает после сбоев).
// index.js для Google Cloud Functions (Node.js)
const fetch = require('node-fetch');
const { Firestore } = require('@google-cloud/firestore');
const db = new Firestore();
const SGTM_URL = 'https://sgtm.yourdomain.kz/kaspi-webhook'; // URL вашего Server-Side GTM
exports.handleKaspiWebhook = async (req, res) => {
try {
// 1. Проверяем подпись вебхука от Kaspi (Security First!)
if (!verifyKaspiSignature(req)) {
return res.status(403).send('Forbidden');
}
const { order_id, status, amount } = req.body;
// 2. Обрабатываем только успешные оплаты
if (status !== 'PAID') {
return res.status(200).send('OK');
}
// 3. Достаем заказ из базы данных
const orderDoc = await db.collection('orders').doc(order_id).get();
if (!orderDoc.exists) return res.status(404).send('Order not found');
const orderData = orderDoc.data();
// 4. Защита от дублей (чтобы не отправить purchase дважды)
if (orderData.ga_purchase_sent) {
return res.status(200).send('Already processed');
}
// 5. Формируем Payload для sGTM
const sgtmPayload = {
event_name: 'purchase',
client_id: orderData.ga_client_id,
session_id: orderData.ga_session_id,
transaction_id: order_id,
value: amount,
currency: 'KZT',
items: orderData.items // Массив товаров по стандарту GA4 e-commerce
};
// 6. Отправляем в sGTM
await fetch(SGTM_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sgtmPayload)
});
// 7. Помечаем заказ, чтобы избежать дублей
await db.collection('orders').doc(order_id).update({ ga_purchase_sent: true });
res.status(200).send('Success');
} catch (error) {
console.error('Webhook error:', error);
res.status(500).send('Internal Error');
}
};Шаг 3: Магия в Server-Side GTM
Почему мы не шлем запрос в GA4 напрямую из Cloud Function? Потому что Measurement Protocol в GA4 — та еще штука. Там постоянно меняются требования к заголовкам (User-Agent, IP-адрес), плюс sGTM позволяет нам из одной точки рассылать эту транзакцию не только в GA4, но и в Facebook CAPI, TikTok API, Яндекс.Метрику и т.д.
В sGTM мы создаем кастомный Клиент (Client), который слушает эндпоинт /kaspi-webhook. Он парсит входящий JSON и генерирует Event Data Object.
Далее мы настраиваем стандартный тег GA4 (Google Analytics 4), который триггерится на событие purchase, созданное нашим кастомным клиентом. Тег сам правильно сформирует HTTP-запрос к серверам Google, добавит нужные секретные ключи (API Secret) и прокинет client_id.
Распределение источников транзакций (До и После)
Как изменилась атрибуция после внедрения Server-Side GTM.
Результаты: Воскрешение ROAS
Что произошло после деплоя этой архитектуры?
- Конверсии выросли на 28% (на самом деле они не выросли, просто мы перестали их терять).
- Доля Direct трафика упала. Транзакции, которые раньше падали с неба, теперь корректно приклеиваются к кликам из Google Ads (CPC), органики (Organic Search) и соцсетей.
- Обучение рекламных кампаний взлетело. Google Ads начал получать в 1.5 раза больше сигналов о конверсиях. Алгоритмы Target ROAS и Maximize Conversions наконец-то поняли, какие аудитории реально приносят деньги, а не просто кликают. CPO (Cost Per Order) снизился на 15% за две недели.
Обучение алгоритмов Google Ads
Динамика CPO (Cost Per Order) в тенге по неделям.
Резюме для инженеров и маркетологов
Не пытайтесь решить системные проблемы аналитики костылями на фронтенде. Если в вашем флоу есть редирект в стороннее приложение (Kaspi, банк-клиент, платежный шлюз) — фронтенд-аналитика всегда будет терять данные.
Единственный надежный источник истины о деньгах — это ваш бэкенд и база данных. Связка Cloud Functions + Server-Side GTM дает вам Enterprise-уровень аналитики. Вы контролируете каждый байт данных, не зависите от AdBlocker-ов, браузерных ограничений (ITP от Apple) и закрытых вкладок.
Собирайте client_id, любите серверную аналитику, и пусть ваш ROAS всегда будет зеленым!

Рустам Шарафутдинов
Эксперт в области архитектуры Google Cloud и Senior Full-Stack разработчик с более чем 15-летним опытом. Специализируется на отказоустойчивых архитектурах, оптимизации высоконагруженных проектов и интеграции AI (Vertex AI).