Інженерні історії · 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) |
| Події товарів на сайті | snippets теми на PDP та лендингах | Amplitude | ця стаття |
| Події checkout | Web Pixel в адмінці Shopify | Amplitude | ця стаття |
lum:analytics (не пайплайн) | кожна подія на сайті | DOM-подія на сторінці | ця стаття |
Ізоляція навмисна. 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 подій, з фільтром за handle, product_id + page_title на кожній |
| Рекламний лендинг | 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. Суть у переносимості, а не в другому виході: майбутній колектор підписується на подію 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 у темі надсилав свої події безумовно. Web Pixel для checkout був налаштований з Permission = Required (Analytics), тож Shopify притримував події пікселя, доки відвідувач не надасть згоду на аналітику. Видимим симптомом були асиметричні дані: для тих самих сесій події на сайті надходили, а події checkout ні.
Я позначив це як питання приватності та політики, а не інженерне. Тема збирала аналітику без бар'єру згоди, поки піксель чекав на згоду, і рішення, як узгодити ці двоє, належало власнику магазину, а не мені. Конфігурацію потім змінили в адмінці, встановивши Permission пікселя на Not required.
Я хочу сформулювати ефект вузько, бо це саме той тип твердження, у якому легко перебільшити. З Not required Shopify доставляє події checkout до пікселя незалежно від сигналу згоди, тож піксель тепер поводиться як трекер на сайті, і події checkout почали надходити. Це спостережуваний ефект. Я не стверджую, що ця конфігурація поважає згоду чи задовольняє якесь конкретне регулювання. Вона узгодила два пайплайни, змусивши обидва збирати дані незалежно від згоди. Чи доречно збирати дані незалежно від згоди, залежить від політики приватності магазину та законної підстави, а це рішення власника, і воно було ухвалене як налаштування в адмінці, а не в коді. Я змінив код подій та його форму; перемикач згоди був під контролем власника.
Мінімізація PII: факт завершення, а не значення
Одне рішення про мінімізацію даних було моїм. Оригінальна карта подій вимагала, щоб піксель checkout надсилав значення імені клієнта, а це більше персональних даних, ніж потребує вимірювання. Тож я звів його до факту: подія фіксує, що поле контакту було заповнене, без прикріпленого імені. Трекери на сайті дотримуються тієї самої лінії й не несуть імен чи email. Воронка може показати, що покупець дійшов до кроку контакту й заповнив його, не фіксуючи, хто він. Це мінімізація даних, вужча й більш захищувана, ніж заява про відповідність нормам.
Прохід виправлень
Після випуску трекерів аналітик повідомив, що частина подій на сайті не спрацьовує. Причина була однією хворобою, а не багатьма багами: карту подій написали під загальні селектори Dawn та одноразові виклики querySelector під час виконання скрипта, а DOM цього магазину значною мірою нестандартний, тож загальні хуки не влучали в реальні елементи, а одноразові пошуки виконувалися до того, як ці елементи існували.
| Елемент | Чому не спрацьовувало | Виправлення |
|---|---|---|
| Хедер (nav / logo / cart) | вказував на загальні селектори Dawn, відсутні на нестандартному хедері | перенацілити на власні селектори lhead |
| Підписка у футері | Klaviyo впроваджує форму на клієнті; немає рідної <form> для прив'язки | зачепитися за подію submit klaviyoForms |
| Карусель відгуків | Swiper на основі transform; слухач scrollLeft ніколи не спрацьовує | зачепитися за slideChange у Swiper, з резервом на MutationObserver |
| Індекс галереї | трекер виконується до розбору галереї; кешований null читав індекс 0 | повторно запитувати трек при кожному виклику |
Кожне виправлення випущено окремим PR. Це той самий урок, який дала робота над кошиком в іншому місці цього магазину: на власній темі запит по всьому документу, написаний під стокову тему, це прихований збій, а пошук, що виконується до того, як існує його DOM, це збій гарантований.
Що я побудував, діагностував, переглянув і рекомендував
Чесна межа сама по собі є частиною роботи, тож я тримаю її чіткою.
Побудував: два трекери на сайті, шину lum:analytics, Web Pixel для checkout, виправлення несправностей та скорочення PII з полем імені.
Діагностував, але не будував: дублювання Purchase. GA4 та соціальні пікселі належать маркетингу; я їх не налаштовував і не змінював. Команда шукала причину періодичних дублів подій Purchase, і я підтвердив, що тема не надсилає жодної (кожен пов'язаний з Purchase рядок у ній це UI-текст). Провідна гіпотеза, передана далі, а не доведена, це дві незалежні події Purchase без спільного event_id, найімовірніше сторонній застосунок-піксель, що спрацьовує паралельно з рідним каналом Facebook, або цей застосунок плюс власний піксель, з серверними подіями, що обходять Shopify, тож ніщо їх не дедуплікує. Я не підтвердив точних відправників. Я залишив шлях діагностики: Meta Test Events для порівняння Browser проти Server і відсотка дедуплікованих, потім список інтеграцій, потім залишити одного відправника Purchase.
Переглянув, але не будував: банер cookie. Банер згоди з'являвся поза регіоном, який я очікував. Я підтвердив, що це власний рідний банер приватності Shopify (consent-tracking-api, privacy-banner, customerPrivacy), а не елемент теми чи сторонній застосунок. Суть була в застереженні: не прибирати його хаком у коді, бо він керує режимом згоди Shopify, який визначає, коли маркетинговим пікселям і GA дозволено спрацьовувати. Видалення його заради охайнішого UI могло б зламати збір даних команди маркетингу. Переглянув і пояснив, не чіпав.
Переглянув і рекомендував: відстеження додавання в кошик у 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 подій), фільтрація за handle, шинаlum:analyticsта хуки виправлення (lheadхедер, KlaviyoklaviyoForms, SwiperslideChange, повторно запитуваний індекс галереї).snippets/lum-amplitude-events-landing.liquid: карта подій рекламного лендингу (page_view,cta_button_clickedз placement), фільтрація за handle сторінки.- Shopify Admin, Settings, Customer events: Web Pixel для checkout (правильно відсутній у репозиторії теми), налаштування
Permission(під контролем власника) та поле імені, зведене до факту завершення. snippets/pdp-web-vitals.liquid: пайплайн RUM, що належить deep dive 03.
Потрібен інженер Shopify, який працює на всьому комерційному шляху?
Я працюю там, де сходяться UX вітрини, логіка товару, обмеження платформи, вимірювання та релізи в продакшн.
Почати розмову