Инженерные истории · 06
Инструментирование Shopify через границу темы и checkout
- Web Pixels
- Amplitude
Три пайплайна назначения и одна нейтральная к вендорам шина событий: измерение пути покупки через checkout, до которого код темы не дотягивается, с минимизацией персональных данных и честным изложением того, что проверено, что настроено и что принадлежит владельцу магазина.
Shopify OS 2.0 · Web Pixels / Customer Events · Amplitude · Event bus (lum:analytics) · Data minimization
Каждое остальное решение на этой витрине проверялось данными: какой набор выбирают покупатели, где их теряет buy box, стоит ли чего-нибудь сдвиг макета, который лаборатория не видит. Аналитика была инструментом, под который настраивалось всё остальное. Вот как я инструментировал путь покупки через checkout, куда тема не имеет доступа, держал потоки событий изолированными, а персональные данные минимальными, и оставался точным в вопросе о границе между инженерными решениями и решениями о приватности, которые принимать было не мне.
Граница, которая формирует архитектуру
checkout в Shopify это закрытая, изолированная в песочнице область. checkout.liquid объявлен устаревшим, и JavaScript темы не выполняется на шагах checkout. Один этот факт разбивает воронку надвое. Поведение на сайте (просмотры товаров, выбор набора, добавление в корзину, правки корзины) доступно из кода темы. Всё от checkout_started до checkout_completed недоступно.
Отслеживайте только тему, и вы измеряете вплоть до корзины, а затем уходите в темноту там, где деньги переходят из рук в руки, не способные связать поведение на сайте с завершёнными заказами. Чтобы дотянуться через границу, нужен второй механизм, который работает там, где тема не может: Shopify Web Pixel. Сайт и checkout это две среды выполнения с двумя инструментами, и задача была в том, чтобы они читались как один путь покупки, не притворяясь, будто границы нет.
Архитектура: три пайплайна и внутренняя шина
Я разбил инструментирование на три пайплайна назначения, у каждого один владелец и одна задача, плюс одна внутренняя шина событий, которая вообще не является назначением.
| Пайплайн | Источник | Назначение | Владелец |
|---|---|---|---|
| Производительность реальных пользователей (RUM) | snippet темы на собственных PDP | приватный Google Sheet | работа над производительностью (см. deep dive 03) |
| Товарные события на сайте | snippet-ы темы на PDP и лендингах | Amplitude | эта статья |
| События checkout | Web Pixel в админке Shopify | Amplitude | эта статья |
lum:analytics (не пайплайн) | каждое событие на сайте | DOM event внутри страницы | эта статья |
Изоляция сделана намеренно. RUM пишет в собственный Sheet, поэтому телеметрия производительности никогда не смешивается с потоком товарных событий или отчётностью маркетинга. Пайплайны сайта и checkout держатся в стороне от логики покупки и конверсии, поэтому инструментирование темы никогда не попадает под подозрение, когда команда маркетинга отлаживает собственные цифры Purchase. Три системы это больше поверхности, чем один скрипт, в обмен на радиус поражения в одну систему у каждой.
RUM разбирается в deep dive 03; здесь это первый пайплайн, изолированный от двух других по замыслу.
Товарные события на сайте и шина под ними
Слой сайта это два пассивных трекера, работающих только на прослушивание, которые наблюдают за buy box и никогда не трогают его логику покупки. Оба фильтруют по handle товара или страницы, а не по имени шаблона, потому что контент-команда перетасовывает товары между шаблонами, а handle остаются на месте, так что добавление товара или лендинга это правка в одну строку.
| Поверхность | События (примеры) | Заметки |
|---|---|---|
| PDP | page_view, bundle_selected, purchase_type_selected, add_to_cart, взаимодействия с галереей / вкладками / FAQ / cross-sell / корзиной, подписка в футере | ~18 events, фильтрация по handle, product_id + page_title на каждом |
| Advertorial-лендинг | page_view, cta_button_clicked (с placement) | фильтрация по handle страницы |
Шов lum:analytics: внутренняя шина событий, а не четвёртый пайплайн
Это единственный элемент, который стоит назвать точно, потому что его легко переоценить. Это не назначение и не пайплайн. Это внутренняя, нейтральная к вендорам шина событий внутри страницы. Каждое событие на сайте отправляется как DOM CustomEvent до вызова любого вендорского SDK:
function track(name, props) {
var payload = Object.assign({ product_id: PRODUCT_ID, page_title: document.title }, props || {});
document.dispatchEvent(new CustomEvent('lum:analytics', { detail: { event: name, properties: payload } }));
var fn = ampFn();
if (fn) fn(name, payload); else queue.push([name, payload]);
}
Сегодня единственный потребитель это адаптер Amplitude. Смысл в переносимости, а не во втором выходе: будущий сборщик подписывается на event lum:analytics у document и наследует всю карту событий без единого изменения в трекерах. Amplitude это один потребитель шины, а не сама шина. SDK внедряется приложением и может загрузиться поздно, поэтому события выстраиваются в очередь и сливаются, как только он появляется, в течение сорока секунд, прежде чем сдаться.
Checkout: Web Pixel, а не код темы
Пошаговое инструментирование checkout не может быть кодом темы, поэтому оно живёт в Web Pixel в разделе Admin, Settings, Customer events, встроенной функции Shopify, а не в приложении. Следствие, которое иногда читается как упущение, верно: в репозитории темы нет файла Web Pixel, потому что его место в админке.
Внутри песочницы пикселя нет window.amplitude, который можно вызвать, поэтому пиксель шлёт POST напрямую в HTTP API Amplitude через fetch. Он отправляет payment_info_submitted и checkout_completed, но не checkout_started, который тема уже эмитит, а его дублирование раздуло бы цифры.
Анонимная непрерывность, попытка, а не гарантия. Чтобы события checkout были привязаны к тому же анонимному посетителю, что и события на сайте, пиксель читает Amplitude device id из его cookie и переиспользует его. Это делается по мере возможности, без гарантии. Если cookie отсутствует, очищен, заблокирован блокировщиком рекламы или средством приватности, либо разделён между контекстами, события checkout откатываются к отдельной анонимной личности, и связка ломается. Я не проверял сквозную непрерывность идентичности или полноту событий, так что это попытка непрерывности, а не сшитые сессии.
Инцидент с настройкой согласия, описанный точно
Два пайплайна начинали с несогласованной настройкой приватности, и именно эта несогласованность, а не баг, породила асимметрию, которую заметил аналитик.
Трекер Amplitude в теме отправлял свои события безусловно. Checkout Web Pixel был настроен с Permission = Required (Analytics), поэтому Shopify придерживал события пикселя, пока посетитель не даст согласие на аналитику. Видимым симптомом были асимметричные данные: для одних и тех же сессий события на сайте приходили, а события checkout нет.
Я обозначил это как вопрос приватности и политики, а не инженерный. Тема собирала аналитику без барьера согласия, пока пиксель ждал согласия, и решать, как примирить эти два случая, было делом владельца магазина, а не моим. Затем настройку изменили в админке, выставив Permission пикселя в Not required.
Я хочу сформулировать эффект узко, потому что это ровно тот тип утверждения, где легко перегнуть. При Not required Shopify доставляет события checkout пикселю независимо от сигнала согласия, поэтому пиксель теперь ведёт себя как трекер на сайте, и события checkout начали приходить. Это наблюдаемый эффект. Я не утверждаю, что эта настройка уважает согласие или удовлетворяет какому-либо конкретному регулированию. Она выровняла два пайплайна, заставив оба собирать данные независимо от согласия. Уместен ли сбор независимо от согласия, зависит от политики приватности магазина и законного основания, а это решение владельца, и оно было принято как настройка в админке, а не в коде. Я менял код событий и его форму; переключатель согласия контролировался владельцем.
Минимизация PII: факт заполнения, а не значение
Одно решение о минимизации данных было моим. Исходная карта событий требовала, чтобы checkout-пиксель отправлял значение имени покупателя, а это больше персональных данных, чем нужно для измерения. Поэтому я свёл это к факту: event фиксирует, что поле контакта было заполнено, без привязки имени. Трекеры на сайте держат ту же линию и не несут ни имён, ни email. Воронка может показать, что покупатель дошёл до шага контактов и заполнил его, не записывая, кто он. Это минимизация данных, более узкая и более защитимая, чем заявление о соответствии.
Проход по исправлениям
После того как трекеры вышли, аналитик сообщил, что часть событий на сайте не срабатывает. Причина была одной болезнью, а не множеством багов: карта событий была написана под общие селекторы Dawn и разовые вызовы querySelector во время выполнения скрипта, а DOM этого магазина сильно собственный, поэтому общие зацепки промахивались мимо реальных элементов, а разовые обращения выполнялись раньше, чем эти элементы существовали.
| Элемент | Почему не сработало | Исправление |
|---|---|---|
| Header (nav / логотип / корзина) | указывал на общие селекторы Dawn, отсутствующие на собственном header | перенацелить на собственные селекторы lhead |
| Подписка в футере | Klaviyo вставляет форму на стороне клиента; нет нативного <form> для привязки | подцепиться к событию submit у klaviyoForms |
| Карусель отзывов | Swiper на трансформациях; слушатель scrollLeft никогда не срабатывает | подцепиться к slideChange у Swiper, с запасным вариантом на MutationObserver |
| Индекс галереи | трекер выполняется до того, как галерея разбирается; закэшированный null читал индекс 0 | перезапрашивать элемент track при каждом вызове |
Каждое вышло отдельным PR. Это тот же урок, что преподала работа над корзиной в другом месте этого магазина: на собственной теме запрос по всему документу, написанный под стоковую тему, это скрытый сбой, а обращение, которое выполняется до того, как существует его DOM, это сбой гарантированный.
Что я построил, диагностировал, рассмотрел и рекомендовал
Честная граница сама по себе часть работы, поэтому я держу её чёткой.
Построено: два трекера на сайте, шина lum:analytics, checkout Web Pixel, исправления и сокращение PII с именем.
Диагностировано, но не построено: дублирование Purchase. GA4 и социальные пиксели принадлежат маркетингу; я их не настраивал и не менял. Команда гонялась за периодическими дублирующимися событиями Purchase, и я подтвердил, что тема не эмитит ни одного (каждая связанная с Purchase строка в ней это текст интерфейса). Ведущая гипотеза, переданная дальше, а не доказанная: два независимых события Purchase без общего event_id, скорее всего стороннее приложение-пиксель, срабатывающее параллельно с нативным каналом Facebook, или это приложение плюс собственный пиксель, при этом серверные события идут в обход Shopify, так что ничто их не дедуплицирует. Я не подтвердил точных отправителей. Я оставил диагностический путь: Meta Test Events со сравнением Browser против Server и процента дедупликации, затем список интеграций, затем оставить одного отправителя Purchase.
Рассмотрено, но не построено: баннер cookie. Баннер согласия показывался за пределами региона, который я ожидал. Я подтвердил, что это собственный нативный баннер приватности Shopify (consent-tracking-api, privacy-banner, customerPrivacy), а не элемент темы или стороннее приложение. Смысл был в предупреждении: не убирать его хаком в коде, потому что он управляет режимом согласия Shopify, который определяет, когда маркетинговым пикселям и GA разрешено срабатывать. Удаление его ради наведения порядка в интерфейсе могло сломать сбор данных команды маркетинга. Рассмотрено и объяснено, не тронуто.
Рассмотрено и рекомендовано: отслеживание add-to-cart в Klaviyo. Джуниор предложил глобально переопределить window.fetch, чтобы ловить /cart/add для сценария брошенной корзины. Я отсоветовал это: глобальное переопределение fetch это тот же хрупкий глобальный паттерн, который вызвал здесь три отдельных бага в корзине. Вместо этого я рекомендовал нативный Customer Event Shopify product_added_to_cart, который покрывает каждую точку добавления без кода темы, плюс одно требование: исключить бесплатный подарок, чтобы письма о брошенной корзине никогда не рекламировали товар за $0.
Ограничения
Это инструментирование построено для направленного измерения воронки, а не для точности уровня биллинга, и несёт реальные ограничения, которые стоит назвать.
- Непрерывность device-id зависит от cookie Amplitude. Блокировщики рекламы, средства приватности браузера, очистка cookie и разделение хранилища могут сломать связку сайта с checkout, разделив одного посетителя на две анонимные личности.
- SDK на сайте внедряется приложением и может загрузиться поздно. События выстраиваются в очередь, и механизм сброса их сливает, но всё, что осталось в очереди примерно через сорок секунд, отбрасывается.
- checkout работает в непрозрачной песочнице, которую я не могу инспектировать во время выполнения; пиксель опирается на стандартную поверхность Customer Events, которую предоставляет Shopify.
- То, что собирает пиксель, зависит от настройки разрешения согласия в админке, это контролируемая владельцем конфигурация, которую код не навязывает.
- Ничто из этого не гарантирует полной или точной атрибуции. Полнота событий и непрерывность идентичности не проверялись сквозным образом.
Итог
Проверено инженерное решение, а не бизнес-метрика. Карта событий на сайте срабатывает на PDP и лендингах после прохода по исправлениям, а checkout-пиксель отправляет пошаговые события, не несёт имён покупателей и переиспользует Amplitude device id как связку по мере возможности. Три пайплайна работают независимо, поэтому инструментирование темы ни разу не попало под подозрение в расследовании дублирования Purchase.
Я не назвал бы результат «достоверными данными» в каком-либо абсолютном смысле. Это нечто более узкое и более честное: явно ограниченные по области, изолированные потоки событий с минимизированными персональными данными, измеряющие путь покупки через checkout, где позиция по согласию задана как контролируемая владельцем конфигурация в админке, а не утверждена в коде. Цифры конверсии и выручки магазина принадлежат бренду и находятся под NDA, поэтому здесь я держусь инженерной части.
Уроки
- Изоляция это стратегия аналитики, а не просто code smell. Три пайплайна с тремя доменами отказа держали сломанное товарное событие подальше от телеметрии производительности, а тему в стороне от отладки маркетинга, и шина
lum:analyticsпревратила Amplitude в заменяемого потребителя ценой одной строки на событие. - Говорите, где проходит граница платформы и где заканчиваются ваши полномочия. То, что checkout это песочница, и есть причина, почему пиксель живёт в админке. То, что настройка согласия это решение о приватности, и есть причина, почему я обозначил её и не утверждал соответствие.
- Минимизируйте персональные данные и описывайте ограничения. Факт заполнения вместо имени и раздел с ограничениями вместо значка «соответствует GDPR» это более достоверная позиция.
Технические ссылки
snippets/lum-amplitude-events.liquid: карта событий PDP (~18 events), фильтрация по handle, шинаlum:analyticsи зацепки исправлений (lheadheader, KlaviyoklaviyoForms, SwiperslideChange, перезапрашиваемый индекс галереи).snippets/lum-amplitude-events-landing.liquid: карта событий advertorial (page_view,cta_button_clickedс placement), фильтрация по handle страницы.- Shopify Admin, Settings, Customer events: checkout Web Pixel (правильно отсутствующий в репозитории темы), настройка
Permission(контролируемая владельцем) и поле имени, сведённое к факту заполнения. snippets/pdp-web-vitals.liquid: пайплайн RUM, принадлежащий deep dive 03.
Нужен инженер Shopify, который работает по всему коммерческому пути?
Я работаю там, где сходятся UX витрины, продуктовая логика, ограничения платформы, измерения и доставка в продакшн.
Начать разговор