Jak webhook odzyskał 40% zakupów gubionych przez bramkę płatności

Podsumowanie
Problem: Sklep na WooCommerce gubił około 40% transakcji w Google Analytics 4, mimo działającej warstwy serwerowej w wariancie same-origin. Bramka płatności Tpay robiła długie przekierowanie powrotne, a klient płacił i zamykał kartę, nigdy nie wracając na stronę podziękowania. Bez tego powrotu zdarzenie
purchasenie miało gdzie się odpalić.Rozwiązanie: Drugie źródło zdarzenia zakupu, dołożone do istniejącej warstwy serwerowej. Webhook z WooCommerce wysyła transakcję prosto z backendu sklepu, niezależnie od tego, czy klient wrócił z bramki na stronę podziękowania. Robi to z opóźnieniem i tylko wtedy, gdy przeglądarka nie wysłała zakupu sama, więc duplikaty nie powstają.
Wynik: Pokrycie transakcji w GA4 wzrosło z około 60% do niemal 100%, przy zachowanej atrybucji źródła ruchu i bez duplikatów.
Stack: Google Tag Manager (web i serwerowy), GA4, Stape.io, Stape Store, Cloudflare Workers, WooCommerce, Action Scheduler, Consent Mode v2
01 / 03
Wyzwanie
Bramka płatności, przez którą 40% transakcji nie trafiało do GA4
Standardowy pomiar zakupu w sklepie internetowym opiera się na jednym założeniu. Klient kończy płatność, wraca na stronę podziękowania, a jego przeglądarka zdąży odpalić tam zdarzenie purchase, zanim ktokolwiek zamknie kartę. Gdy to założenie jest spełnione, liczba zdarzeń purchase w GA4 z grubsza odpowiada liczbie opłaconych zamówień w sklepie i nikt nie ma powodu zestawiać jednej liczby z drugą.
W opisywanym wdrożeniu, w sklepie na WooCommerce, założenie zawodziło przy czterech zamówieniach na dziesięć. Bramka płatności robiła długie przekierowanie powrotne, klient miał już potwierdzenie z banku i nie miał żadnego powodu, żeby na cokolwiek czekać. Płacił i zamykał kartę. Zamówienie zostawało w panelu sklepu bez odpowiadającego mu zdarzenia w GA4, bo powstaje ono dopiero na stronie podziękowania, do której taki klient już nie wracał.
Sklep miał już wdrożony server-side tagging, i to w najmocniejszym wariancie. Żądania pomiarowe trafiały na ścieżkę we własnej domenie sklepu, a ciasteczka z długim terminem ważności ustawiał serwer. Blokery i ograniczenia przeglądarek były tu problemem rozwiązanym, a pokrycie transakcji i tak wynosiło około 60%.
Warstwa serwerowa odbiera zdarzenia wysłane przez przeglądarkę i dopiero ona przekazuje je dalej. Jej praca zaczyna się więc w momencie, w którym przeglądarka zdarzenie wyśle. Przy zamkniętej karcie zdarzenie zakupu nie powstaje, warstwa serwerowa nie ma czego odebrać i luka w pomiarze zostaje. Nie jest to przy tym kłopot jednej platformy ani jednej bramki, bo dotyczy każdego sklepu, w którym płatność odbywa się poza witryną, a zdarzenie zakupu czeka na powrót klienta. W dalszej części artykułu pokazuję architekturę, która takie zakupy odzyskuje, czyli drugie źródło zdarzenia zakupu, wysyłane prosto z serwera sklepu. Wyjaśniam przy tym, dlaczego stoi ono obok przeglądarki, a nie zamiast niej, i w których miejscach takie wdrożenie najczęściej się wykłada.
Jak zmierzyłem pokrycie transakcji w GA4
Brakujących zakupów z definicji nie widać w samym GA4, więc nie da się ich tam zliczyć. Potrzebny jest punkt odniesienia poza pomiarem, czyli źródło, które rejestruje sprzedaż niezależnie od tego, co zrobiła przeglądarka klienta.
Takim źródłem jest panel WooCommerce. Zapisuje każde opłacone zamówienie, bo powstaje ono po stronie serwera sklepu i nie zależy od żadnego skryptu w przeglądarce. Zestawiłem liczbę opłaconych zamówień z liczbą zdarzeń purchase w GA4 za ten sam okres, a stosunek tych dwóch liczb dał realne pokrycie transakcji. Wyszło około 60%, przy w pełni działającej warstwie serwerowej. Ten sam pomiar powtórzyłem po wdrożeniu i to on jest podstawą liczb w dalszej części tekstu.
Co niepełne dane o konwersjach robią z budżetem reklamowym
Skutki tej luki nie kończyły się na raporcie. Na tych samych liczbach właściciel sklepu opierał decyzje o podziale budżetu reklamowego.
Te niepełne dane o zakupach, zaniżone o cztery transakcje na dziesięć, trafiały także do systemów reklamowych i to na nich uczyły się algorytmy Google Ads oraz Meta. Sygnał konwersji docierał do nich tylko dla części klientów, więc optymalizacja pracowała na zbiorze mniejszym niż realna sprzedaż sklepu. Razem z brakującymi transakcjami znikał z raportów ich przychód, więc kampania, która realnie zarabiała, mogła wyglądać na ledwie rentowną.
Właściciel przestał po cichu ufać własnym raportom, bo dane z GA4 nie zgadzały się z liczbą zamówień w panelu sklepu, a jedynym punktem odniesienia przy decyzjach budżetowych był raport, któremu nie ufał.
02 / 03
Rozwiązanie
Zakup wysłany z backendu sklepu
Większość opisów server-side taggingu zatrzymuje się na ciasteczkach o dłuższym terminie ważności i na odporności na blokery reklam. Obie te korzyści są prawdziwe i w tym sklepie były już wykorzystane, a mimo to nie zamykały luki, bo obie działają dopiero wtedy, gdy przeglądarka ma co wysłać, czyli gdy klient wrócił na stronę podziękowania. Potrzebna była inna właściwość tego podejścia, o której mówi się rzadziej. Skoro dane analityczne przetwarza własny serwer, a nie przeglądarka, to konwersję można do niego wysłać z dowolnego źródła, również z backendu sklepu w chwili, gdy WooCommerce potwierdza opłacone zamówienie. Zupełnie niezależnie od tego, czy klient wrócił z bramki, czy zamknął kartę pięć sekund wcześniej.
Samej warstwie serwerowej poświęciłem osobny artykuł, czym jest server-side tagging i kiedy naprawdę ma sens. Ekonomiczną stronę niepełnych danych, czyli cenę karmienia algorytmów reklamowych, rozwijam w tekście ile kosztuje Data Blackout.
Architektura odzyskiwania transakcji, w której serwer jest źródłem awaryjnym
Kontener serwerowy stał w tym sklepie od wcześniejszego etapu wdrożenia. To on był miejscem, do którego webhook z WooCommerce mógł wysłać zamówienie, i w którym dało się przechować dane sesji zapisane wcześniej przez przeglądarkę. Na nim stanął mechanizm odzyskiwania i cała jego wartość leży w jednej regule. Przeglądarka ma pierwszeństwo, a serwer wysyła zdarzenie wyłącznie wtedy, gdy zakup nie wyszedł z przeglądarki.
Przeglądarkę i webhook łączy Stape Store, czyli magazyn klucz-wartość wbudowany w serwerowy GTM hostowany na Stape, obsługiwany z kontenera przez dedykowane szablony tagów. W tym wdrożeniu przechowuje odciski sesji, więc dalej nazywam go słownikiem sesji. Dzięki niemu serwer uzupełnia zdarzenie przychodzące z webhooka o dane sesji, które zapisała wcześniej przeglądarka.
Jak działa opóźniony webhook, flaga zamówienia i słownik sesji
Cały przepływ rozgrywa się w trzech aktach i dopiero razem dają one system, który nie liczy tej samej transakcji dwa razy.
W pierwszym akcie przeglądarka zapisuje odcisk sesji, jeszcze w trakcie checkoutu. Zanim klient dojdzie do płatności, na wczesnych krokach lejka (begin_checkout, add_payment_info) przeglądarka zapisuje w słowniku sesji komplet danych potrzebnych później do złożenia poprawnej transakcji: client_id, session_id, status zgody z Consent Mode, prawdziwy adres IP, dane urządzenia oraz parametry UTM z adresu wejścia.
Identyfikator sesji i parametry UTM odpowiadają w tej liście za dwie różne części atrybucji. Sam identyfikator z ciasteczka Google Analytics wystarcza, żeby przypisać zakup do właściwej kampanii, gdy ruch przyszedł ze źródła Google, na przykład z wyników organicznych. Za poprawne przypisanie kampanii z innych źródeł, choćby z Facebooka, odpowiadają parametry UTM.
W drugim akcie przeglądarka zostawia po sobie flagę, ale tylko wtedy, gdy klient wrócił z bramki na stronę podziękowania. Kontener webowy wysyła wtedy zdarzenie purchase tradycyjną ścieżką, a dodatkowo zapisuje w słowniku flagę { "status": "processed" } powiązaną z numerem zamówienia. To jest sygnał dla serwera: tym zamówieniem ktoś już się zajął.
W trzecim akcie webhook sprawdza, czy jest w ogóle potrzebny. Dociera na serwer z WooCommerce celowo opóźniony o trzy minuty, wysyłany przez Action Scheduler. To opóźnienie daje przeglądarce czas, żeby wysłała zakup pierwsza. Serwer odpytuje słownik i podejmuje jedną z dwóch decyzji. Jeśli znajdzie flagę, nie wysyła nic dalej, bo transakcja jest już policzona. Jeśli flagi nie ma, wyciąga ze słownika zapisany wcześniej client_id oraz resztę danych sesji, doszywa je do payloadu z webhooka i wysyła kompletną transakcję do GA4.
Efektem jest zakup zapisany nawet wtedy, gdy klient zamknął kartę na bramce. Trafia do GA4 z prawidłowym identyfikatorem i przypisany do realnego źródła ruchu, więc nadaje się do raportów i do zasilenia algorytmów licytacji.
Prostsze wydaje się wysyłanie zakupów wyłącznie webhookiem, z pominięciem przeglądarki, bo serwer jest pewniejszym źródłem. Skoro serwer dopisuje do transakcji z webhooka dane sesji ze słownika, to ma ona przypisane źródło ruchu i sklejanie sesji działa. Różnica polega na tym, że dane wysłane wprost przez przeglądarkę są zwykle dokładniejsze i mniej zawodne w przypisywaniu ruchu niż sesja sklejona po fakcie. Lepszą praktyką jest więc opierać pomiar w pierwszej kolejności na zdarzeniach z przeglądarki, a webhook trzymać jako źródło awaryjne dla zakupów, których przeglądarka nie wysłała. Taki układ daje przy okazji dodatkowe zabezpieczenie. Awaria webhooka, choćby po aktualizacji wtyczki albo zmianie konfiguracji sklepu, nie pozbawia pomiaru wszystkich zakupów, bo ścieżka przeglądarkowa biegnie niezależnie od niego.
cart_hash jako klucz łączący webhook z sesją przeglądarki
Cały ten mechanizm stoi na jednym warunku, którym jest odnalezienie w słowniku odcisku sesji zapisanego wcześniej przez przeglądarkę. Żądanie webhooka trafia na serwer prosto z backendu sklepu i nie niesie ze sobą niczego, co wiązałoby je z sesją klienta. Potrzebny jest klucz, po którym serwer odszuka właściwy wpis, a musi on powstać w koszyku, przetrwać checkout i wrócić w payloadzie zamówienia. Wybór tej wartości rozstrzyga o tym, czy odzyskane zakupy trafią do GA4 z kompletem danych o sesji i z poprawnie przypisanym źródłem ruchu.
Na początku użyłem w tej roli adresu e-mail klienta. Wybór wygląda naturalnie, bo ten sam adres występuje w obu miejscach, z których korzysta mechanizm, czyli w formularzu checkoutu wypełnianym przez klienta i w payloadzie webhooka. W praktyce dopasowania rozjeżdżały się częściej, niż powinny, a część przyczyn leżała w sposobie zapisu i odczytu klucza, nie w samych adresach wpisywanych przez klientów. Wprowadziłem więc standaryzację, czyli sprowadzenie adresu do małych liter i obcięcie spacji, wykonywane tak samo przy zapisie w przeglądarce i przy odczycie na serwerze, żeby obie strony trafiały dokładnie w ten sam klucz. Pomogło to doraźnie, bo rozjazdy zdarzały się rzadziej, ale nie zniknęły.
Przeszedłem więc na cart_hash, czyli trzydziestodwuznakowy identyfikator koszyka, który WooCommerce nadaje każdemu koszykowi i który wraca w payloadzie zamówienia. Najlepszy klucz łączenia to taki, który tworzy sama platforma, bo nie zależy od tego, co i jak wpisze człowiek.
Consent Mode v2 po stronie serwera, czyli jak odtworzyć zgodę użytkownika
Zdarzenie wysłane z backendu musi nieść informację o zgodzie użytkownika dokładnie tak samo jak to wysłane z przeglądarki, a jedyna różnica między nimi polega na tym, że serwer sklepu nie ma do tej zgody żadnego dostępu. Baner zgód działa w przeglądarce, tam też zapisuje decyzję klienta i nigdy nie kontaktuje się z backendem sklepu, więc w chwili wysyłki webhooka serwer nie wie, czy klient zgodę wyraził, czy jej odmówił. Rozwiązanie leży w tym samym miejscu co reszta układu, czyli w słowniku sesji.
Razem z identyfikatorami i danymi urządzenia przeglądarka zapisuje w słowniku również stan zgody klienta. Po stronie kontenera serwerowego stoi tabela lookup, która ten zapisany stan tłumaczy na parametr gcs wymagany przez protokół Google: G111 przy pełnej zgodzie, G100 przy jej braku. Serwer dopisuje tę wartość do żądania w chwili wysyłki.
Przy odmowie zgody GA4 dostaje ping bezciasteczkowy z G100. Ten sygnał nie niesie identyfikatorów użytkownika, więc sam zakup zostaje zaraportowany, tylko bez przypisania do konkretnej osoby i do źródła ruchu. Bez niego zakup takiego klienta nie zostawiałby po sobie żadnego śladu.
Zgoda zostaje tu odczytana w przeglądarce, a użyta przez serwer kilka minut później, przy zdarzeniu, którego przeglądarka już nie widzi. Sama warstwa przeglądarkowa tego nie odtworzy, bo w chwili wysyłki nie ma jej już czym zapytać o zgodę.
Cztery pułapki wdrożenia webhooka w serwerowym GTM
Opisany wyżej układ wygląda prosto, dopóki patrzy się na sam przepływ danych, bo z takiego opisu znika kolejność zdarzeń w czasie i to, co się dzieje, gdy któryś zapis nie dojdzie do skutku. Zanim system pracował bez zarzutu, musiałem zmierzyć się z czterema wyzwaniami i każde z nich jest klasycznym błędem tego typu wdrożeń.
Odwrócony wyścig danych. W pierwszej wersji webhook potrafił dotrzeć na serwer zanim przeglądarka zdążyła zapisać dane sesji. Serwer pytał wtedy słownik o dokument, którego jeszcze nie było, dostawał odpowiedź o braku rekordu, a transakcja zostawała bez sesji. Problem dotykał przede wszystkim klientów płacących najszybciej, głównie przez BLIK. Rozwiązanie miało dwa kroki. Zapis sesji przeniosłem na wczesne kroki lejka, a webhook świadomie opóźniłem o trzy minuty.
Obiekt `ecommerce` wysyłany jako tekst. Tag GA4 próbował przekazać cały wielopoziomowy obiekt jako jeden ciąg znaków, co kończyło się [object Object] w payloadzie. Wykluczyłem surowy obiekt z wysyłki i zmapowałem pola transakcji ręcznie. Biznesowo był to poważniejszy problem, niż wygląda, bo systemy reklamowe odrzucały wtedy dane o wartości i zawartości koszyka. Wiedziały, że ktoś kupił, ale nie wiedziały co ani za ile, więc licytacja pod wartość konwersji i optymalizacja na poziomie produktów nie miały z czego korzystać.
Transakcja przypisana serwerowi zamiast klientowi. Webhook wysyła backend WooCommerce, a nie przeglądarka klienta, więc bez korekty GA4 przypisałby każdy zakup do serwera sklepu, z User-Agentem WordPress i adresem IP serwerowni. Cała geografia oraz podział na urządzenia byłyby fikcją. Dlatego serwer nadpisuje adres IP i dane urządzenia wartościami klienta, zapisanymi wcześniej w słowniku sesji.
Sam `client_id` nie wystarcza, nawet gdy zdarzenie trafia wyłącznie do GA4. Odnalezienie odcisku sesji w słowniku przypisuje zakup właściwemu użytkownikowi, ale nie zawsze właściwej kampanii. Identyfikator z ciasteczka Google Analytics niesie źródło wtedy, gdy ruch przyszedł ze strony Google, natomiast przy kampaniach prowadzonych gdzie indziej potrzebne są zapisane wcześniej parametry UTM z adresu wejścia. Dlatego słownik przechowuje nie sam identyfikator, tylko komplet danych potrzebnych do złożenia poprawnej sesji, i dlatego zapis odbywa się na wczesnych krokach lejka, a nie dopiero przy płatności. Kiedy którejś z tych wartości zabraknie, nic tego nie sygnalizuje, bo zdarzenie dociera do GA4 zupełnie normalnie. Brak widać dopiero w raportach, w których zakup nie ma przypisanej kampanii albo urządzenia.
03 / 03
Wyniki
Pokrycie transakcji w GA4 wzrosło z 60% do niemal 100%
Zanim przejdę do liczb, oddzielę od siebie dwa wyniki tego wdrożenia, czyli zliczenie transakcji i jakość ich atrybucji.
Zliczenie jest praktycznie pewne. Webhook powstaje w backendzie WooCommerce i trafia do serwera w komunikacji serwer-serwer, więc nie zależy od przeglądarki klienta ani od tego, co ma w niej zainstalowane. Dociera zawsze, a wraz z nim cały payload zakupu: wartość, zawartość koszyka, numer zamówienia. Z perspektywy pytania o to, ile naprawdę sprzedaliśmy, luka praktycznie znika, bo do policzenia transakcji client_id nie jest potrzebny.
Czego nie obiecuję jako matematycznego stu procent, to idealna atrybucja każdej pojedynczej transakcji. Jeśli przeglądarka z jakiegoś powodu nie zdążyła zapisać odcisku sesji, webhook nie znajdzie identyfikatora, który mógłby doszyć do zakupu. Taka transakcja nadal zostaje policzona, a jej wartość wchodzi do przychodu, tyle że żadna kampania nie zostaje wskazana jako jej źródło. Są to jednak przypadki sporadyczne i zdecydowana większość odzyskanych zakupów trafia do GA4 z poprawną atrybucją, więc mówimy tu o jakości danych, a nie o utraconej sprzedaży.
Z tym zastrzeżeniem wdrożenie przyniosło następujące korzyści:
Pokrycie transakcji wzrosło z około 60% do niemal 100%. Ta sama metoda pomiaru co na starcie, czyli opłacone zamówienia w panelu WooCommerce zestawione ze zdarzeniami
purchasew GA4. Pierwsze pomiary po wdrożeniu pokazały komplet zamówień, choć nie zakładam, że tak będzie w każdym oknie, bo zawsze może trafić się pojedynczy przypadek brzegowy. Transakcje, które wcześniej ginęły na przekierowaniu z bramki, trafiają teraz do GA4 przez webhook. Warstwa serwerowa była w obu pomiarach taka sama, więc cały odzysk dołożyło drugie źródło zdarzenia.Zero duplikatów. Kolejność, w której serwer czeka na przeglądarkę i sprawdza jej flagę, sprawia, że transakcja jest liczona dokładnie raz: albo z przeglądarki, albo z serwera, nigdy z obu naraz. Serwer sam blokuje wysyłkę, gdy widzi flagę zostawioną przez przeglądarkę.
Odzyskana atrybucja źródła. W zdecydowanej większości przypadków odzyskane transakcje niosą prawidłowy
client_idisession_id, parametry UTM z adresu wejścia oraz resztę danych potrzebnych do złożenia sesji, więc GA4 przypisuje przychód do realnej kampanii zamiast do ruchu bez źródła.Zgoda odtworzona po stronie serwera. Serwer odtwarza parametr
gcsze stanu, który przeglądarka zapisała w checkoucie, i dokleja go do każdego wysyłanego hitu. Przy odmowie zgody GA4 dostaje ping bezciasteczkowy zamiast niczego.Algorytmy reklamowe dostają komplet sygnałów. Systemy uczą się na wszystkich klientach, a nie na sześciu z dziesięciu, więc budżet pracuje na pełnych danych.
Usługa: Server-Side Tagging →
Chcesz podobnych rezultatów?
Umów bezpłatną konsultację i sprawdź, jak mogę pomóc Twojemu biznesowi.


