Перейти к содержимому

Разрешить сайту считать ваши посещения с помощью Яндекс Метрики? Для этого она сохранит в браузере файлы cookie. Как сайт использует cookie

VC
Кейс 8 из 25 · Интернет-торговля · аналитика

Сквозная атрибуция для двух магазинов на Shopify: от клика до заказа

Бренд товаров для активного отдыха из США платил за рекламу в Meta, Google и YouTube, но не знал, какая реклама приносит заказы. Я связал путь «рекламный клик → заказ → рекламный кабинет» через трекер Keitaro, который учитывает рекламные переходы, и свой промежуточный сервис. Отчёт по выручке сошёлся с реальными продажами. Попутно стало видно, где связь между кликом и заказом теряется.

Отрасль
Интернет-торговля · два магазина на Shopify
Технологии
Python (stdlib) · Shopify API · Keitaro · FB CAPI
Сроки
Основа — 3 недели, доработки — 2 месяца
Итог
+25,7% лишней выручки в отчёте → 0
01 · Проблема

Три источника цифр, и все показывают разное

Бизнес тратил деньги на Meta, Google и YouTube и не мог ответить на первый же вопрос: какая реклама приносит заказы. Shopify записывал большую долю продаж в прямые заходы (Direct). Рекламные кабинеты показывали конверсии, которые не сходились с рабочей таблицей владельца. А таблица не сходилась ни с тем, ни с другим.

Владелец говорил прямо: «данные не совпадают». Аналитика, которой не доверяют, бесполезна. Когда три отчёта дают три разные выручки, дальше начинается спор о цифрах.

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

02 · Решение

Цепочка данных, в которой каждое звено можно проверить

Атрибуция — это привязка заказа к рекламе, которая его принесла. Сломаться она может в четырёх местах. Метка клика не доходит до магазина. Уведомление магазина о заказе (вебхук) приходит дважды. Событие с сервера задваивает событие из браузера. Расход не попадает в отчёт. Поэтому я собрал цепочку без разрывов, и каждое её звено можно проверить отдельно.

01
Клик

Метка клика снимается прямо в шаблоне магазина и дублируется собственным идентификатором

02
Уведомления от магазина

Уведомления о корзине, оформлении и заказе приходят в промежуточный сервис на http.server. Он проверяет подпись каждого магазина и отсеивает повторы

03
Передача в трекер

С сервера на сервер: заявка — на уникальный клик, продажа с суммой — на заказ

04
Связка клика и покупателя

В SQLite сходятся идентификатор клика, хеш почты и номер оформления, и заказ задним числом привязывается к первому клику

05
Отчёты

Весь путь клиента по касаниям, вклад и роль каждого канала, доход с клиента за всё время, заказы, которые Shopify записал как прямые, а трекер привязал к рекламе

Метку клика пришлось снимать в шаблоне магазина

Штатный способ — Web Pixel — не подошёл. Он работает в изолированной среде Shopify и не может записать данные в корзину (cart attributes), поэтому метка клика до заказа не доходит. Пришлось ловить её в шаблоне (теме) магазина и дублировать собственным идентификатором визита (vid). Так связь клика и заказа сохраняется даже там, где штатный механизм её теряет.

Промежуточный сервис проверяет подпись и отсеивает повторы

Свой промежуточный сервис на http.server принимает уведомления Shopify и делает четыре вещи:

  • проверяет подпись HMAC отдельным секретом для каждого из двух магазинов
  • отсеивает повторы: Shopify повторяет отправку уведомлений, и отчёт от этого расти не должен
  • передаёт данные с сервера в трекер: lead — заявка на каждый уникальный клик, sale — продажа с суммой на заказ
  • пишет каждое событие воронки в свой журнал JSONL: подробный путь сохраняется, даже когда трекер хранит только сводные цифры

Conversions API: браузер и сервер не задваивают покупку

Одновременно с передачей в трекер сервис отправляет с сервера событие Purchase в Facebook Conversions API. У него тот же event_id, что у браузерного пикселя. Это ключевая деталь: без общего идентификатора конверсии в кабинете удваиваются, и реклама оптимизируется по завышенным цифрам.

Клик, покупатель и заказ связаны в одной базе

Отдельная база SQLite связывает клик, покупателя и заказ по ksubid, хешу почты и checkout_token, с привязкой задним числом к первому клику. На этих связях строятся отчёты. Они отвечают на вопросы, на которые не ответить, если учитывать только последний клик:

  • путь покупателя через несколько касаний и распределение заслуг между каналами
  • роль канала: знакомит с брендом, напоминает о нём или приводит к покупке
  • доход с клиента за всё время (LTV) по каждому каналу
  • заказы, которые Shopify записал как прямые заходы (Direct), а трекер привязал к платной рекламе
  • сверка с опросом после покупки «откуда вы о нас узнали» — независимая проверка модели

Расход выгружается каждый день, иначе окупаемость считается без затрат

Атрибуция часто ломается на простом: выручка по каналу есть, а расход остался в рекламном кабинете. Расходы по кампаниям выгружаются по расписанию из Google Ads API и Meta Marketing API прямо в трекер. Поэтому цена продажи по каждому каналу видна в самом отчёте.

Только стандартная библиотека Python

Весь сервис написан на urllib, http.server и sqlite3 — без единой внешней зависимости. Причина простая: код должен работать на рабочем сервере, где нет pip, установщика пакетов Python. Заодно не нужно следить за обновлениями сторонних пакетов.

03 · Технологии

Ни одной внешней зависимости, потому что на сервере нет pip

Python, только стандартная библиотека

http.server, urllib, sqlite3 — ни одной внешней зависимости

Shopify Admin API + вебхуки

Приложение с авторизацией, уведомления о корзине, оформлении и заказе, проверка подписи для каждого магазина

Keitaro

Приём событий с сервера (заявка и продажа), выгрузка кампаний и расходов

Facebook Conversions API

Покупка передаётся с сервера с тем же идентификатором события, что у пикселя в браузере, и не задваивается

Google Ads API · Meta Marketing API

Ежедневная выгрузка расходов по кампаниям в трекер

GA4 Data API

Проверка сессий и источников по данным Google Analytics

SQLite

Связка «клик ↔ клиент ↔ заказ» и привязка задним числом к первому клику

Shopify Liquid / Web Pixel

Метка клика снимается в шаблоне магазина, плюс собственный идентификатор

systemd + nginx

Сервис работает как системная служба, обмен идёт по расписанию, перезапуск без ручных действий

Журнал событий в JSONL

Подробная воронка в своём хранилище, независимо от сводных цифр трекера

Python stdlibShopify APIWebhooks + HMACKeitaro S2SFacebook CAPIGoogle Ads APIMeta Marketing APIGA4 Data APISQLiteLiquidsystemdnginx
04 · Результат

Отчёт сошёлся с выручкой, а слабые места измерений стали видны

Расхождение дневного отчёта
+25,7% 0

лишняя выручка исчезла после исправления расчёта

Ошибки в расчёте
2

отменённые заказы в выручке (+25,7%) и невычтенные промокоды (+5,74%)

Выручка без источника
≈ ⅓

выручки с сайта одного магазина; причину нашли и подтвердили

Две ошибки в расчёте, из-за которых отчёту не верили

Первое, что дала связка, — возможность сверить отчёт с реальностью. Сверка сразу нашла две ошибки. Отменённые заказы попадали в выручку: в один из дней это завышало её на 25,7%. Хуже того, отменённый заказ показывался как успех рекламы. Промокоды не вычитались, и это добавляло ещё 5,74%. Обе ошибки исправлены, и дневной отчёт сошёлся с реальной выручкой.

Ноль отслеженных продаж означал «нечем измерить»

Когда расходы по кампаниям и отслеженные продажи впервые оказались в одном отчёте, разница между каналами выглядела огромной. У одной рекламной сети было ~186 тыс. кликов и ни одной отслеженной продажи. Все отслеженные продажи месяца пришли из другой сети, где было ~12 тыс. кликов. По этим цифрам владелец перераспределил бюджет.

Продолжение оказалось важнее самого решения. Ноль в колонке продаж означал, что покупка приходила без идентификатора кампании. По той же причине 31 кампания, на которую приходилось 74% рекламного расхода, стояла с подписью «0 продаж · 0,0×». Экран подсказывал выключить то, что работало.

Теперь, если сопоставить продажи не с чем, окупаемость не считается и в отчёте стоит прочерк. Сбой рекламной площадки, о котором она сама не сообщает, отчёт называет прямо. Однажды такой сбой рекламного API вдвое сократил число связанных покупок — со 101 до 47, и в журнале не было ни одного предупреждения.

Тот же урок повторился с типом кампании. Определять тип по названию оказалось ошибкой: тринадцать кампаний со словом Video в названии к видеоканалу не относились. Настоящих видеокампаний за тот же период нашлось 17, и на всех вместе у них было восемь кликов. Теперь тип берётся у самой площадки.

Треть выручки без источника: причина нашлась вне системы учёта

Около трети выручки с сайта одного из магазинов приходило без источника. Причина оказалась внешней. Баннер согласия на cookie блокировал сессию Shopify, пока посетитель не давал согласие. Источник терялся раньше, чем аналитика успевала его записать. Диагноз подтвердили сравнение двух магазинов и такой же случай, который раньше был во втором магазине.

Система пережила смену источника данных

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

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

Восстановление задним числом: собрано 247 кликов, опознано 229, просроченных ноль, всё за 35 секунд. Связанных заказов стало 97, было 46. Доля выручки без привязки к кампании снизилась с 82% до 57%. Рядом на экране показано прежнее значение и пометка, что связь восстановлена.

Основная часть бюджета уходила в канал, которого в отчёте почти не было

Основная часть рекламных денег уходила блогерам, а система показывала по этому каналу почти ноль. Настоящий след нашёлся в теге заказа. Ник блогера читается за тот же проход, который уже определяет тип продажи, так что новых запросов к магазину не понадобилось. За 50 дней канал дал 96 заказов — 19,7% всей выручки — по 17 разным ссылкам. Ссылки одного человека я намеренно не объединяю: отдача двух ссылок одного блогера различается в 22 раза, и объединение спрятало бы именно то, ради чего всё считают.

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

05 · Применимость

Где ещё работает тот же подход

За кейсом стоит типовая задача: «связать действие на сайте с деньгами в кассе и с расходом в рекламном кабинете». Этот подход переносится почти без изменений везде, где есть платная реклама и заказы:

  • → Любой магазин на Shopify, WooCommerce или Tilda, где подозрительно много продаж записано как прямые заходы («Direct»)
  • → Несколько магазинов или доменов с общим рекламным бюджетом: одна модель атрибуции для всех кабинетов
  • → Бизнес с заявками и долгой сделкой: заявка, звонок, сделка в CRM — та же цепочка «клик → идентификатор → передача данных с сервера»
  • → События с сервера для Meta, Google и TikTok, когда браузерный учёт теряет данные, — с обязательным отсевом повторов по event_id
  • → Проверка готовой аналитики, когда отчёты уже есть, но расходятся между собой. Часто причина в самом расчёте отчёта
Что переиспользуется на следующих проектах
  • Шаблон промежуточного сервиса на стандартной библиотеке: приём уведомлений, проверка подписи для каждого магазина, отсев повторов, передача событий с сервера
  • Общий идентификатор события для пикселя в браузере и Conversions API, чтобы конверсии не задваивались
  • Связка на SQLite: клик ↔ клиент ↔ заказ, с привязкой задним числом к первому касанию
  • Ежедневная выгрузка расходов из рекламных кабинетов, чтобы окупаемость считалась вместе с затратами
  • Набор отчётов поверх трекера: весь путь по касаниям, вклад и роль канала, доход с клиента за всё время, заказы от рекламы, записанные как прямые заходы
Похожая задача?

Если магазин записывает продажи в «Direct», а отчёты не сходятся между собой, это можно исправить

Начать стоит с проверки всей цепочки: где теряется метка, где задваивается событие и где отчёт считает не то, что показывает. Основа атрибуции собирается за 2–3 недели, дальше — отчёты и отладка на реальных данных.