Blog
Zdarzenia e-commerce bez platformy sklepowej: jak sprawić, by własna kasa raportowała do GA4
Opublikowano · 5 min czytania
Platforma sklepowa daje Ci przynajmniej wtyczkę, z którą można się spierać. Kasa zbudowana samodzielnie — aplikacja React, ścieżka rezerwacji, rejestracja w SaaS ze Stripe w tle — nie daje Ci nic: żadne zdarzenie się nie uruchomi, dopóki Twój własny kod go nie wypchnie. Dobra wiadomość jest taka, że kontrakt jest niewielki: garść nazw, jeden kształt obiektu i kilka zasad, gdzie należy każde wypchnięcie. Oto całość, łącznie z częścią, którą większość poradników pomija — zakupami, które dzieją się, gdy nikogo nie ma na Twojej stronie.
Kontrakt: nazwa i obiekt ecommerce
Wszystko tutaj trafia do Analytics w ten sam sposób. Twoja strona wypycha do window.dataLayer zdarzenie pod jego nazwą z GA4, z obiektem ecommerce w kształcie GA4. Reguła Tag Managera dopasowuje nazwę, a tag zdarzenia GA4 odczytuje obiekt i przekazuje go dalej do Twojego kontenera serwerowego. Jeśli Twoja strona jest w SignalHost oznaczona jako sklep — albo na stronie kontenera kliknięto „Skonfiguruj” w sekcji „Zdarzenia konwersji” — te reguły i tagi już istnieją w Twoim kontenerze internetowym, z prefiksem SH w nazwie. Zostaje samo wypchnięcie.
Dwie zasady wyłapują większość błędów. Najpierw wyczyść poprzedni obiekt ecommerce, inaczej view_item sprzed trzech stron pojedzie na gapę w następnym zdarzeniu. A w każdym zdarzeniu, które niesie pieniądze, wysyłaj value jako liczbę, a currency jako kod ISO: value w postaci ciągu znaków albo brak currency zostaną przyjęte bez słowa skargi i po cichu pominięte w przychodach.
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'begin_checkout',
ecommerce: {
currency: 'EUR',
value: 49.00,
items: [
{ item_id: 'plan-pro', item_name: 'Pro', price: 49.00, quantity: 1 }
]
}
});Które zdarzenie i gdzie jest jego miejsce
Własna ścieżka ma te same momenty co platforma sklepowa. Musisz je tylko znaleźć we własnym kodzie.
- view_item_list — strona lub komponent pokazujący kilka produktów albo planów. Raz, gdy lista się wyświetli, a nie przy każdym przewinięciu.
- view_item — strona pojedynczego produktu lub planu.
- add_to_cart i remove_from_cart — w handlerze przycisku, po tym, jak koszyk faktycznie się zmienił.
- begin_checkout — moment, w którym odwiedzający wchodzi do kasy: pierwsze renderowanie strony kasy albo kliknięcie, które otwiera okno kasy.
- add_payment_info — gdy dane płatności zostaną przyjęte, a nie gdy pojawi się formularz.
- purchase — raz, gdy zamówienie zostanie potwierdzone. Większość tego artykułu dotyczy tego, jak zrobić dobrze właśnie to zdarzenie.
- sign_up i start_trial — w biznesie subskrypcyjnym konwersje, które następują, zanim jakiekolwiek pieniądze zmienią właściciela. SignalHost buduje tagi dla obu, obok zdarzeń sklepowych.
Wypychaj z handlera, który spowodował zmianę, a nie z renderowania. Aplikacja jednostronicowa renderuje się ponownie, kiedy tylko chce, a React w trybie deweloperskim uruchamia efekty dwukrotnie: wypchnięcie wewnątrz efektu podwaja się w buildzie deweloperskim i powtarza przy każdym ponownym zamontowaniu komponentu.
Zakup na Twojej stronie potwierdzenia: odczytaj zamówienie ze swojego serwera
Gdy odwiedzający po zapłacie wraca na Twoją stronę, to właśnie tam należy purchase — pod dwoma warunkami. Pobierz zamówienie z własnego backendu, nie z adresu URL: parametry zapytania można edytować, przekierowania je gubią, a do tego pojawiają się też przy nieudanych płatnościach. Stripe na przykład odsyła na ten sam adres niezależnie od tego, czy płatność przeszła, czy nie, a który to przypadek, podaje we własnym parametrze. I wypchnij zakup tylko raz: odświeżenie, przycisk wstecz czy strona z paragonem zapisana w zakładkach — wszystko to prowadzi z powrotem na nią.
// On your confirmation page. The order comes from YOUR server, by an id the
// page already knows — never from price or status parameters in the URL.
const order = await fetch(`/api/orders/${orderId}`).then((r) => r.json());
const key = `purchase-sent-${order.id}`;
if (order.status === 'paid' && !localStorage.getItem(key)) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: order.id,
value: order.total,
tax: order.tax,
currency: order.currency,
items: order.items.map((i) => ({
item_id: i.sku, item_name: i.name, price: i.price, quantity: i.quantity
}))
}
});
localStorage.setItem(key, '1');
}transaction_id to siatka bezpieczeństwa, a nie plan. Analytics odrzuca powtórzony transaction_id w obrębie jednej sesji, ale nie odświeżenie strony z paragonem następnego ranka.
Płatność na cudzej stronie: Stripe Checkout, PayPal, przekierowanie do banku
Hostowana strona płatności to typowy wybór przy własnej implementacji i ma jedną ostrą krawędź: odwiedzający może nigdy nie wrócić. Płaci, zamyka kartę przeglądarki, a Twoja strona potwierdzenia nigdy się nie wyrenderuje. Przy płatnościach kartą to kilka procent zakupów. Przy przelewach bankowych, fakturach i metodach „kup teraz, zapłać później” może to być większość, bo pieniądze docierają kilka godzin albo dni później.
Zdecyduj więc, dla każdej metody płatności z osobna, gdzie leży prawda. Jeśli Twój backend dowiaduje się o płatności z webhooka, to webhook jest niezawodnym miejscem, by ją zgłosić — patrz następna sekcja — a strona potwierdzenia nie może dodatkowo wypychać purchase, bo inaczej każda płatność kartą zostanie policzona dwa razy. Jeśli jedynym miejscem, w którym się o niej dowiadujesz, jest strona powrotu, zgłoś ją tam i pogódź się z luką.
Zakupy, przy których nie ma przeglądarki: wysyłaj je ze swojego serwera
Przy subskrypcjach nie da się tego uniknąć. Okres próbny przechodzi w płatny po siedmiu dniach, odnowienie następuje co miesiąc, faktura zostaje opłacona przelewem w przyszłym tygodniu. Każde z nich to zakup i żadne nie dzieje się przed przeglądarką. Rozwiązaniem jest wysłanie zdarzenia z serwera do serwera — z webhooka, który dowiaduje się o płatności — do tego samego punktu końcowego tagowania, którego używają Twoje strony: Twojej nazwy hosta SignalHost albo Twojej własnej domeny, żeby przeszło przez kontener serwerowy jak każde inne trafienie.
Potrzeba do tego jednej rzeczy z kasy: identyfikatora klienta Analytics odwiedzającego, żeby zakup trafił do osoby, której wizyty do niego doprowadziły. Znajduje się w cookie _ga, które Twoje żądanie kasy i tak niesie, gdy trafia do Twojej własnej domeny. Zapisz go razem z zamówieniem. Brak cookie _ga oznacza, że odwiedzający nie wyraził zgody na analitykę: wyślij trafienie ze stanem zgody denied i jednorazowym identyfikatorem albo nie wysyłaj go wcale — nigdy nie wymyślaj mu tożsamości.
// In your checkout request handler: keep the visitor's Analytics id with the order.
const ga = req.cookies._ga; // "GA1.1.1234567890.1700000000"
order.gaClientId = ga ? ga.split('.').slice(-2).join('.') : null;
// Later, in your payment webhook — a trial converting, an invoice paid:
const params = new URLSearchParams({
v: '2',
tid: 'G-XXXXXXXXXX', // your GA4 measurement ID
cid: order.gaClientId ?? `${Date.now()}.${Math.floor(Math.random() * 1e9)}`,
gcs: order.gaClientId ? 'G101' : 'G100', // no _ga cookie = no consent
en: 'purchase',
'ep.transaction_id': invoice.id,
'epn.value': '49.00',
cu: 'EUR',
pr1: 'idplan-pro~nmPro~pr49.00~qt1',
});
await fetch(`https://metrics.example.com/g/collect?${params}`, { method: 'POST' });Wybierz jedno źródło dla każdego zdarzenia i trzymaj się go. Dokładnie tak signalhost.io mierzy własną sprzedaż: przeglądarka wypycha sign_up, begin_checkout i start_trial; webhook Stripe wysyła purchase, gdy opłacona zostanie pierwsza faktura; nic nie wysyła obu. Measurement Protocol od Google też by zadziałał, ale trafia prosto do Google i omija Twój kontener serwerowy, więc żaden z Twoich tagów po stronie serwera nigdy nie zobaczy tej sprzedaży.
Zgoda nie wymaga od Ciebie niczego dodatkowego
Wypychaj zdarzenia bez względu na to, co wybrał odwiedzający. Tagi słuchają Consent Mode: przy zgodzie wysyłają normalnie, bez niej wysyłają pingi bez ciasteczek, które Analytics modeluje, zamiast je raportować. Wstrzymywanie wypchnięć na własną rękę jedynie ukrywa konwersje przed tym modelowaniem. Liczy się natomiast kolejność — fragment kodu Tag Managera umieszczasz po skrypcie banera zgody, żeby domyślne ustawienia zgody istniały, zanim zostanie wysłane pierwsze zdarzenie.
Sprawdzenie, czy to działa
- Otwórz kontener w SignalHost. Karta zdarzeń pokazuje, co dotarło, według nazwy, wraz z odsetkiem objętym zgodą. Wypchnięta nazwa, której tam nie widzisz, nigdy nie dotarła do Twojego serwera tagowania.
- Przejdź przez swoją kasę w trybie podglądu Tag Managera. Pokazuje on każde wypchnięcie, który tag się uruchomił i jaki obiekt ecommerce został wysłany.
- W Analytics DebugView pokazuje zdarzenia na bieżąco, w miarę jak docierają. Raporty standardowe mogą potrzebować doby.
- Odśwież stronę potwierdzenia dwa razy i policz zakupy. Nadal powinien być jeden.
Wszystkie usterki są tu ciche: nazwa zapisana z niewłaściwą wielkością liter, value wysłane jako ciąg znaków, items zagnieżdżone o poziom za głęboko. Żadna z nich nigdzie nie generuje błędu. Każda z nich daje raport z dziurą.
