Сквозная атрибуция для двух Shopify-магазинов: от клика до заказа
Бренд товаров для активного отдыха из США платил за Meta, Google и YouTube, но не знал, какая реклама приносит заказы. Собрал контур «рекламный клик → заказ → рекламный кабинет» на трекере Keitaro и собственном промежуточном сервисе. Отчёты по выручке перестали расходиться с реальностью, а владелец получил основание перераспределить рекламный бюджет.
Три источника цифр, и ни один не сходится с другими
Бизнес тратил деньги на Meta, Google и YouTube — и не мог ответить на первый же вопрос:
какая реклама приносит заказы. Витрина Shopify списывала большую долю продаж на
Direct,
рекламные кабинеты рапортовали конверсии, которые не сходились с оперативной таблицей
владельца, а сама таблица не бьётся ни с тем, ни с другим.
Владелец бизнеса формулировал это прямо: «данные не совпадают». И это худший вариант из возможных — аналитика, которой не доверяют. Когда три отчёта дают три разные выручки, дальше начинается спор о цифрах.
Практическое следствие: решения по бюджету принимались на ощущениях. Никто не мог сказать, сколько стоит одна продажа по каждому каналу, — а значит, нельзя было ни выключить убыточное плечо, ни осознанно долить в прибыльное. Деньги распределялись по привычке.
Непрерывный путь данных: клик не теряется нигде
Атрибуция ломается сразу в четырёх местах: метка клика не долетает до витрины, вебхук приходит дважды, серверное событие задваивает браузерное, расход не попадает в отчёт. Поэтому строился сплошной контур — каждое звено проверяемо отдельно.
Метка клика снимается прямо в шаблоне магазина и дублируется собственным идентификатором
Корзина, оформление и заказ уходят в промежуточный сервис на http.server: проверка подписи по каждому магазину, отсев повторов
С сервера на сервер: обращение — на уникальный клик, продажа с суммой — на заказ
В SQLite сходятся идентификатор клика, хеш почты и номер оформления — заказ привязывается к первому клику задним числом
Весь путь клиента по касаниям, вклад и роль каждого канала, доход с клиента за всё время, «победы атрибуции»
Метку клика пришлось снимать в самой теме
Штатный путь — Web Pixel — не сработал: в песочнице Shopify он не пишет
cart attributes,
то есть метка клика просто не доезжает до заказа. Пришлось ловить её в самой теме магазина
и дублировать собственным идентификатором визита
(vid),
чтобы связь клик↔заказ держалась даже там, где штатный механизм её теряет.
Middleware: HMAC по каждому магазину, дедупликация на входе
Собственный промежуточный сервис на
http.server
принимает уведомления Shopify и делает четыре вещи:
- проверяет
HMAC-подпись — отдельным секретом для каждого из двух магазинов - дедуплицирует события: Shopify повторяет доставку, отчёт от этого не должен расти
- передаёт данные с сервера в трекер:
lead— обращение на каждый уникальный клик,sale— продажа с суммой на заказ - пишет каждое событие воронки в собственный
JSONL— гранулярный путь сохраняется там, где трекер уже только агрегирует
Conversions API: события схлопываются в одно
Одновременно с передачей в трекер сервис отправляет с сервера
Purchase
в Facebook Conversions API — с тем же
event_id,
что и браузерный пиксель. Это ключевая деталь: без общего идентификатора вы получаете удвоенные конверсии в кабинете и оптимизацию по выдуманным цифрам.
Identity-граф вместо last-click
Отдельная база SQLite склеивает клик, покупателя и заказ по
ksubid,
хешу e-mail и
checkout_token,
с обратной привязкой к первому клику. Поверх этого графа — слой отчётов, который отвечает
на вопросы, на которые нельзя ответить, если учитывать только последний клик:
- мультитач-путь покупателя и распределение заслуги между каналами
- роль канала: открывает / догоняет / закрывает
- LTV по каналу — доход с клиента за всё время
- «победы атрибуции»: витрина сказала
Direct, трекер поймал платный источник - сшивка с post-purchase опросом «откуда узнали» — независимая проверка модели
Расход выгружается ежедневно — иначе окупаемость считается без затрат
Половина проектов по атрибуции ломается на банальном: выручка по каналу есть, а расход остался в кабинете. Расходы по кампаниям тянутся по расписанию из Google Ads API и Meta Marketing API прямо в трекер, поэтому цена продажи по каналу — поле в отчёте.
Только стандартная библиотека
Весь контур написан на
urllib,
http.server
и
sqlite3
— без единой внешней зависимости. Причина прозаическая: код обязан работать на боевом
сервере, где нет pip.
Побочный эффект — нечему устаревать и нечего обновлять по сообщениям об уязвимостях.
Ноль зависимостей — потому что боевой сервер без pip
http.server, urllib, sqlite3 — ни одной внешней зависимости
Приложение с авторизацией, уведомления о корзине, оформлении и заказе, проверка подписи по каждому магазину
Приём событий с сервера (обращение и продажа) и выгрузка кампаний и расходов
Покупка передаётся с сервера с тем же идентификатором события — задвоения с пикселем в браузере исключены
Ежедневная синхронизация расхода по кампаниям в трекер
Перекрёстная проверка сессий и источников против собственных данных
Связка «клик ↔ клиент ↔ заказ» и привязка к первому клику задним числом
Метка клика снимается в шаблоне магазина, плюс собственный идентификатор
Сервис работает как служба, обмен данными по расписанию, перезапуск без ручных действий
Подробная воронка в собственном хранилище, независимо от сводных цифр трекера
Отчёт перестал врать — и сразу изменил решения
фантомная выручка ушла после починки расчёта
отменённые заказы в выручке (+25,7%) и невычтенные промокоды (+5,74%)
веб-выручки одного магазина — причина найдена и подтверждена
Два дефекта расчёта, которые ломали доверие к отчёту
Первое, что дал контур, — возможность сверить отчёт с реальностью, и сверка сразу нашла две ошибки. Отменённые заказы попадали в выручку: на одном дне это давало завышение на 25,7% и, что хуже, выставляло отменённый заказ «победой атрибуции». Промокоды не вычитались — ещё 5,74% сверху. Оба дефекта закрыты; дневной отчёт перестал расходиться с реальной выручкой.
Ноль отслеженных продаж означал «нечем измерить»
Когда расход по кампаниям и отслеженные продажи сошлись в одном отчёте, разрыв между каналами выглядел разительным: при ~186 тыс. кликов из одной сети — ни одной отслеженной продажи, тогда как все отслеженные продажи месяца пришли из другой при ~12 тыс. кликов. На этих цифрах владелец перераспределил бюджет.
Продолжение оказалось важнее самого решения. Ноль в колонке продаж означал, что покупка доходила без идентификатора кампании. По той же причине 31 кампания, на которую приходится 74% рекламного расхода, стояла с подписью «0 продаж · 0,0×» — экран подсказывал выключить то, что работало. Теперь пустой знаменатель даёт прочерк, а молчаливый отказ площадки называется поимённо: отказ рекламного API однажды срезал привязку вдвое, со 101 связанной покупки до 47, и в журнале не было ни одного предупреждения.
Тот же урок повторился на типе кампании. Разбирать его по названию оказалось негодным приёмом: тринадцать кампаний со словом Video в имени видеоканалом не были, а настоящих видеокампаний за то же окно нашлось 17 — с восемью кликами на всех. Тип теперь берётся у самой площадки.
Треть выручки без источника оказалась не багом трекинга
Около трети веб-выручки одного из магазинов приходила без источника. Причина оказалась внешней: баннер согласия на куки блокировал сессию Shopify до нажатия кнопки согласия, то есть источник терялся ещё до того, как до него доходила очередь у аналитики. Диагноз подтверждён A/B-сравнением двух витрин и историческим прецедентом второго магазина.
Контур пережил смену источника данных
Позже внешний трекер отключили, и отчётность переехала на собственные источники. Переезд дал свой урок: два таймера восемь суток подряд лили расходы в отключённый приёмник и рапортовали об успехе, причём в журнале не было ни одной ошибки. Сейчас каждый шаг называет, с чем именно он разговаривал и чем это кончилось.
История кликов у рекламной площадки живёт ровно 90 дней, и достижимые связи истекали примерно по полторы в сутки. Поэтому собран свой кэш «клик → кампания», который пополняется дважды в сутки, а разрешение идёт на страницах без обращения в сеть — отдельный тест валит прогон при попытке сходить за связью в интернет. Пересборка задним числом: собрано 247 кликов, опознано 229, просрочено ноль, за 35 секунд. Связанных заказов стало 97 вместо 46, доля непривязанной выручки опустилась с 82% до 57%. Рядом на экране печатается прежнее значение и пометка, что связь восстановленная.
Три четверти бюджета уходили в канал, которого в отчёте почти не было
Основная часть рекламных денег уходит блогерам, а контур показывал по этому каналу почти ноль. Настоящий след лежал в теге заказа: ник читается в том же проходе, который уже размечает вид продажи, ни одного нового обращения к магазину не понадобилось. За 50 дней канал дал 96 заказов — 19,7% всей выручки по 17 разным ссылкам. Ссылки одного человека намеренно не склеиваются: отдача двух ссылок одного блогера различается в 22 раза, и склейка спрятала бы именно то, ради чего считают.
Общий итог, если убрать детали: у бизнеса появился один источник правды, который выдерживает проверку. Отчёту снова можно задавать вопросы — и получать ответ, который не приходится перепроверять вручную.
Где ещё ложится та же методология
За кейсом стоит типовая задача «склеить событие на сайте с деньгами в кассе и с расходом в рекламном кабинете». Контур переносится почти без изменений везде, где есть платный трафик и заказы:
- → Любой магазин на Shopify / WooCommerce / Tilda, где витрина показывает подозрительно много «Direct»
- → Несколько витрин / доменов на общем рекламном бюджете — единая модель атрибуции вместо разрозненных кабинетов
- → Бизнесы с заявками и длинной сделкой — заявка, звонок, сделка в CRM: та же цепочка «клик → идентификатор → передача данных с сервера»
- → Server-side события для Meta / Google / TikTok после потерь браузерного трекинга — с обязательной дедупликацией по event_id
- → Аудит существующей аналитики, когда отчёты уже есть, но расходятся между собой — часто причина сидит в расчёте отчёта
- Шаблон промежуточного сервиса на стандартной библиотеке: приём уведомлений, проверка подписи по магазину, отсев повторов, передача события с сервера
- Общий идентификатор события для пикселя в браузере и Conversions API — конверсии не задваиваются
- Связка на SQLite: клик ↔ клиент ↔ заказ, с привязкой к первому касанию задним числом
- Ежедневная выгрузка расходов из рекламных кабинетов — окупаемость считается вместе с затратами
- Набор отчётов поверх трекера: весь путь по касаниям, вклад и роль канала, доход с клиента за всё время, «победы атрибуции»
Если витрина показывает «Direct», а отчёты не сходятся между собой — это чинится
Начинать имеет смысл с аудита контура: где именно теряется метка, где задваивается событие и где отчёт считает не то, что показывает. Ядро атрибуции собирается за 2–3 недели, дальше — отчёты и отладка на реальных данных.
Похожие кейсы
Оплаченные заказы Shopify → рабочие таблицы склада
Заказы, номера отправлений, статусы и отмены проставляются сами каждые 30 минут. Приёмник на Advanced Sheets…
Обмен с партнёром по отгрузке: файл дня туда, стоимость и треки обратно
Файл дня собирается сам по дате в книге и повторяет её раскраску настоящим условным форматированием. Ответ…
Сверка выручки из трёх источников: разрыв 8,4% объяснён до 0,33%
Книга владельца, складская книга товароведа и CRM давали три разные выручки. Разовая тройная сверка объяснила…
Аудит за 9 900 ₽ — с конкретным отчётом и сметой
Расскажу что внедрить в вашем бизнесе в первую очередь, какая будет окупаемость, и нужен ли вообще AI для вашей задачи (иногда — нет).
Или просто напишите свой вопрос — отвечу в течение 2 часов