Panel WooCommerce pokazuje jedną liczbę zamówień, GA4 mniejszą, a w listopadzie ktoś zawsze pyta, której wierzyć. WooCommerce ma rację co do zamówień. GA4 ma rację co do tego, co do niego dotarło. Sensowne pytanie brzmi: które zamówienia zniknęły i dlaczego. Odpowiesz na nie dwoma eksportami i złączeniem po numerze transakcji.
W tym wpisie dostajesz tę procedurę, gotowy kod i zakup wysyłany z serwera, który zdążysz wdrożyć przed Black Friday i grudniowym szczytem. Każde twierdzenie o GA4, WooCommerce, Przelewy24 i PayU pochodzi z ich własnej dokumentacji albo kodu źródłowego, czytanych 30 października 2026.
Co naprawdę wykazał audyt z Search Engine Journal?
28 października 2026 Search Engine Journal opublikował audyt Christiana Goodricha, szefa SEO w brytyjskiej agencji SOZO. Porównał GA4 z rejestrem zamówień w ośmiu sklepach z Wielkiej Brytanii, a w sześciu udało się dopasować dane. Autor ujawnia, że jego agencja pracowała ze wszystkimi ośmioma markami. Sklepy są anonimowe, a artykuł nie podaje, na jakiej platformie działają, więc nie jest to wynik dotyczący WooCommerce jako takiego.
Liczby “66%” z tytułu w treści nie ma. Dla opisanego sklepu autor podaje:
“GA4 zarejestrował 81% zamówień internetowych sklepu w IV kwartale 2024, 41% w IV kwartale 2025 i zaledwie 32% w okresie świątecznym.”
Search Engine Journal, 28 października 2026, tłumaczenie własne
32% zarejestrowanych oznacza 68% brakujących w święta, co tytuł zaokrągla do dwóch trzecich. W tym samym kwartale liczba zamówień internetowych tego sklepu wzrosła z 5874 do 7686, a GA4 pokazywał spadek o mniej więcej jedną trzecią.
Przyczyny wymienione przez autora, w jego ujęciu:
- Stała luka zgód. Co tydzień część klientów odmawia śledzenia, więc GA4 nigdy nie widzi wszystkich zamówień.
- Baner zgód, który sam się wyłączył. Plany platform zgód rozliczane miesięcznym limitem odsłon wyczerpały się w grudniu i baner przestał się wyświetlać do kolejnego okresu rozliczeniowego.
- Przerwa i późne uzupełnienie. Jeden sklep miał 2215 zamówień w pięć tygodni, a GA4 nie zarejestrował żadnego; około 1000 dosypało się później, prawie wszystkie jako Unassigned.
- Zduplikowane zakupy. W jednym sklepie 657 z 3365 numerów zamówień miało więcej niż jedno zdarzenie purchase, co dodało 735 zakupów. W sumie kwartalnej duplikaty przykryły część strat.
- Boty. Boty wykonujące kod jak przeglądarka liczyły się jako odwiedzający.
Jedno zdanie z artykułu ustawia resztę tego wpisu: śledzenie po stronie serwera nie zmienia obowiązków związanych ze zgodą, więc stała luka zostaje. Kod poniżej jest zbudowany właśnie wokół tej zasady.
Audyt nie mówi nic o bramkach płatności i stronie potwierdzenia zamówienia. W sklepie na WooCommerce, zwłaszcza z płatnościami przez przekierowanie do Przelewy24, PayU albo banku, od tego zacząłbym szukanie.
Dlaczego WooCommerce gubi zakupy w GA4?
Darmowe rozszerzenie Google Analytics for WooCommerce to w wielu sklepach konfiguracja domyślna. Dokumentacja wprost opisuje, od czego zależy zliczanie zakupu:
“Transakcje zakupu: śledź zakupy w swoim sklepie. Wymaga to bramki płatności, która po płatności przekierowuje na stronę podziękowania lub potwierdzenia zamówienia.”
Dokumentacja WooCommerce, Google Analytics for WooCommerce, tłumaczenie własne
Zdarzenie purchase wysyła przeglądarka klienta ze strony potwierdzenia zamówienia (order received). Wszystko, co nie pozwala tej stronie się załadować albo tagowi na niej zadziałać, usuwa zakup z GA4, choć WooCommerce zamówienie zapisuje. Najczęstsze powody:
- Brak zgody. Ta sama dokumentacja mówi, że w EOG, Wielkiej Brytanii i Szwajcarii odwiedzający muszą wyrazić zgodę przed śledzeniem i bez niej GA4 oraz Google Ads nie zbierają danych użytkownika. Rozszerzenie nie ma własnego banera; odczytuje zgodę przez WP Consent API z osobnej wtyczki.
- Blokery reklam i błędy skryptów. Lista diagnostyczna WooCommerce wymienia rozszerzenia przeglądarki, blokery reklam, błędy JavaScript i podwójne instancje śledzenia.
- Klient nie wraca z bramki. Strona płatności odsyła klienta przekierowaniem. Jeśli po zapłacie BLIK-iem zamknie kartę albo aplikacja banku nie odda sterowania przeglądarce, zamówienie i tak zostanie opłacone dzięki powiadomieniu z serwera bramki, a zakup w przeglądarce się nie wyśle.
- Cache przed stroną potwierdzenia. Sam WooCommerce wysyła nagłówki no-cache i ustawia
DONOTCACHEPAGEna stronach koszyka, kasy i konta, a strona potwierdzenia jest punktem końcowym kasy (WC_Cache_Helper::prevent_cachingw rdzeniu WooCommerce). Ryzyko pojawia się, gdy CDN albo proxy ma regułę, która cache’uje HTML bez względu na te nagłówki. - Błędne albo powtarzane ID transakcji. Strona pomocy Google o deduplikacji po ID transakcji ostrzega, że pusty identyfikator sprawia, że wszystkie takie zakupy zostaną zdeduplikowane razem, a powtarzane identyfikatory zaniżają liczby.
Rozszerzenie potrafi też zawyżyć liczby. W jego kodzie na GitHubie (gałąź trunk, nagłówek wersji 2.4.4, czytany 30 października 2026) zakup jest przygotowywany na haku woocommerce_thankyou dla każdego zamówienia, które nie jest nieudane ani anulowane i ma poprawny klucz zamówienia w adresie. Zamówienie z przelewem tradycyjnym w statusie wstrzymanym albo zamówienie oczekujące na płatność, którego klient zobaczył stronę podziękowania, trafia do GA4 jako zakup, choćby nigdy nie zostało opłacone. Dokumentacja statusów zamówień określa status wstrzymany jako oczekiwanie na potwierdzenie płatności, a oczekujący na płatność jako zamówienie bez wpłaty. Uzgadniaj więc tylko statusy opłacone.
Ten sam kod daje darmową diagnostykę. Gdy strona potwierdzenia wyrenderuje się z poprawnym kluczem, rozszerzenie zapisuje po stronie serwera metadane zamówienia _ga_tracked = 1, zanim przeglądarka cokolwiek wyśle. Brakujące zamówienia dzielą się więc na dwie grupy:
- Opłacone zamówienie bez
_ga_tracked: strona potwierdzenia się nie wyrenderowała. Szukaj w powrocie z bramki. - Jest
_ga_tracked, brak zakupu w GA4: strona się wyrenderowała, a zdarzenie zginęło w przeglądarce. Szukaj w zgodach, blokerach i błędach skryptów.
Rozszerzenie wysyła transaction_id jako $order->get_order_number() (filtr woocommerce_ga_order_id). Używaj tej samej wartości we wszystkich krokach poniżej, bo przy wtyczkach do numeracji zamówień (np. numeracja z prefiksem pod fakturowanie) złączenie po wewnętrznym ID się rozjedzie.
Jak wyeksportować zamówienia z WooCommerce do porównania?
Bierz tylko zamówienia złożone przez sklep internetowy, w statusach opłaconych, za zamknięty okres. Procedura z Search Engine Journal pomija zamówienia z marketplace’ów, subskrypcji i wprowadzone ręcznie. W WooCommerce zrobisz to przez created_via: klasyczna kasa ustawia checkout, a kasa blokowa (Store API) store-api, co widać w kodzie rdzenia WooCommerce. Zamówienia dodane ręcznie w panelu dostają admin i odpadną same. Jeśli wciągasz zamówienia z marketplace’u przez integrację, sprawdź, jaką wartość created_via ustawia ta wtyczka.
Skrypt działa i z HPOS, i ze starym zapisem w tabeli wpisów, bo korzysta z wc_get_orders(). Uruchom go przez WP-CLI na stagingu albo na produkcji (tylko czyta dane):
<?php
// wp eval-file export-orders.php 2025-11-01 2025-12-31 > orders.csv
list( $from, $to ) = array_slice( $args, 0, 2 );
$out = fopen( 'php://stdout', 'w' );
fputcsv( $out, array( 'transaction_id', 'created', 'status', 'created_via', 'payment_method', 'device', 'thankyou_seen', 'has_ga_cookie', 'total' ) );
$page = 1;
do {
$orders = wc_get_orders( array(
'type' => 'shop_order',
'status' => array( 'processing', 'completed', 'refunded' ),
'date_created' => $from . '...' . $to,
'limit' => 500,
'paged' => $page++,
'orderby' => 'ID',
'order' => 'ASC',
) );
foreach ( $orders as $order ) {
fputcsv( $out, array(
$order->get_order_number(),
$order->get_date_created() ? $order->get_date_created()->date( 'Y-m-d' ) : '',
$order->get_status(),
$order->get_created_via(),
$order->get_payment_method(),
$order->get_meta( '_wc_order_attribution_device_type' ) ?: 'unknown',
'1' === (string) $order->get_meta( '_ga_tracked' ) ? 'yes' : 'no',
$order->get_meta( '_wpp_ga_client_id' ) ? 'yes' : 'no',
$order->get_total(),
) );
}
} while ( count( $orders ) === 500 );Trzy kolumny wymagają komentarza. device pochodzi z atrybucji zamówień WooCommerce, która od wersji 8.5 zapisuje _wc_order_attribution_device_type jako Mobile, Tablet albo Desktop na podstawie user agenta. thankyou_seen to flaga _ga_tracked opisana wyżej. has_ga_cookie będzie mieć wartość “no”, dopóki nie wdrożysz kodu serwerowego z dalszej części wpisu; potem pokaże, które zamówienia miały ciasteczko analityczne w chwili składania.
Zamówienia zwrócone zostają w eksporcie, bo zostały opłacone i GA4 powinien mieć dla nich zakup. Jeśli wolisz SQL w sklepie z HPOS, ten sam filtr wygląda tak (popraw prefiks wp_):
SELECT o.id, o.status, o.payment_method, o.total_amount, o.currency,
o.date_created_gmt, od.created_via
FROM wp_wc_orders o
JOIN wp_wc_order_operational_data od ON od.order_id = o.id
WHERE o.type = 'shop_order'
AND o.status IN ('wc-processing', 'wc-completed', 'wc-refunded')
AND o.date_created_gmt >= '2025-11-01' AND o.date_created_gmt < '2026-01-01'
AND od.created_via IN ('checkout', 'store-api');SQL zwraca wewnętrzne id. Jeśli wtyczka zmienia numery zamówień, zamapuj je przed złączeniem albo użyj eksportu w PHP.
Jak wyeksportować zakupy z GA4 po ID transakcji?
Użyj wymiaru Identyfikator transakcji z metryką Zakupy e-commerce. W schemacie GA4 Data API ecommercePurchases liczy zdarzenia purchase, a transactions liczy kilka typów zdarzeń, w tym refund. Autor z Search Engine Journal ostrzega przed tym samym: metryka Transakcje liczy zwroty razem z zakupami.
W interfejsie zbuduj eksplorację dowolną: Identyfikator transakcji w wierszach, Zakupy e-commerce jako wartość, ten sam zakres dat co eksport zamówień, potem eksport do CSV. W Data API wymiar to transactionId, a metryka ecommercePurchases. Drugi eksport z deviceCategory albo browser pokaże podział po stronie GA4. Zanim zaufasz plikowi, sprawdź dwie rzeczy:
- Progi danych. Strona pomocy o progach danych mówi, że ustala je system, nie da się ich zmienić i mogą ukrywać wiersze, gdy raport zawiera dane demograficzne albo sygnały Google. Ogranicz eksplorację do ID transakcji i liczby zakupów.
- Dane modelowane. Modelowanie behawioralne w trybie zgody widać tylko przy tożsamości raportowania Mieszana i wymaga co najmniej 1000 zdarzeń dziennie z
analytics_storage='denied'przez 7 dni oraz 1000 użytkowników dziennie ze zgodą w 7 z ostatnich 28 dni. Modelowani użytkownicy nie mają numerów prawdziwych zamówień, więc to porównanie dotyczy wyłącznie danych obserwowanych.
Jak policzyć lukę według bramki i urządzenia?
Ten skrypt łączy oba pliki i wypisuje odsetek zarejestrowanych zamówień ogółem oraz dla każdej metody płatności, urządzenia i flagi strony podziękowania. Liczy też ID transakcji z więcej niż jednym zdarzeniem purchase (test duplikatów z audytu SEJ) oraz ID z GA4 bez pasującego zamówienia. Python 3, tylko biblioteka standardowa:
#!/usr/bin/env python3
# python3 reconcile.py orders.csv ga4.csv
# ga4.csv: GA4 export with the columns "Transaction ID" and "Ecommerce purchases".
import csv
import sys
from collections import Counter, defaultdict
def read_ga4(path):
with open(path, encoding="utf-8-sig") as f:
rows = [line for line in f if line.strip() and not line.startswith("#")]
counts = {}
for r in csv.DictReader(rows):
tid = (r.get("Transaction ID") or "").strip()
if tid and tid != "(not set)":
counts[tid] = counts.get(tid, 0) + int(float(r.get("Ecommerce purchases") or 0))
return counts
def main(orders_path, ga4_path):
ga4 = read_ga4(ga4_path)
with open(orders_path, encoding="utf-8") as f:
orders = [o for o in csv.DictReader(f) if o["created_via"] in ("checkout", "store-api")]
groups = defaultdict(Counter)
for o in orders:
seen = ga4.get(o["transaction_id"], 0) > 0
for key in ("payment_method", "device", "thankyou_seen"):
groups[(key, o[key])]["orders"] += 1
groups[(key, o[key])]["in_ga4"] += seen
total = len(orders)
found = sum(1 for o in orders if ga4.get(o["transaction_id"], 0) > 0)
print(f"web orders: {total}, found in GA4: {found}, capture: {found / total:.0%}" if total else "no orders")
for (key, val), c in sorted(groups.items()):
print(f"{key:15} {val:20} orders {c['orders']:6} in GA4 {c['in_ga4']:6} capture {c['in_ga4'] / c['orders']:.0%}")
dupes = {t: n for t, n in ga4.items() if n > 1}
print(f"transaction IDs with more than one purchase event: {len(dupes)} (extra events: {sum(dupes.values()) - len(dupes)})")
unknown = set(ga4) - {o["transaction_id"] for o in orders}
print(f"GA4 transaction IDs with no matching web order: {len(unknown)}")
if __name__ == "__main__":
main(*sys.argv[1:3])Uruchom go raz dla listopada i grudnia zeszłego roku i raz dla ostatnich czterech tygodni. Sama jedna liczba niewiele mówi, robotę robi podział. W polskim sklepie kolumna payment_method zwykle rozdzieli Przelewy24, PayU, Tpay, przelew tradycyjny i pobranie, a każda z tych metod ma inną ścieżkę powrotu klienta.
Jak czytać wynik?
| Co widzisz | Najbardziej prawdopodobna przyczyna | Gdzie szukać |
|---|---|---|
| Jedna bramka wyraźnie niżej od reszty | Klienci nie wracają ze strony tej bramki | Adres powrotu bramki, strona potwierdzenia na telefonie, thankyou_seen dla tej bramki |
Dużo opłaconych zamówień z thankyou_seen = no | Strona potwierdzenia się nie wyrenderowała | Przekierowania bramki, zamykanie karty po BLIK-u, błędy wtyczek na tej stronie |
thankyou_seen = yes, a niski odsetek w GA4 | Przeglądarka zgubiła zdarzenie | Odsetek zgód, blokery, konsola, domyślny stan zgody ustawiany przez rozszerzenie |
| Telefon wyraźnie niżej niż komputer | Przełączanie do aplikacji banku, przeglądarki w aplikacjach | Testowe zamówienie z telefonu każdą bramką |
| W październiku dobrze, w grudniu spadek | Coś pękło pod obciążeniem albo skończył się limit | Limit planu platformy zgód, wdrożenia z tego tygodnia, reguły CDN |
| Wiele ID z więcej niż jednym zakupem | Odświeżanie strony potwierdzenia albo dwa tagi | Podwójne tagi w GTM i wtyczkach |
| ID z GA4 bez zamówienia w eksporcie | Zakupy z zamówień oczekujących lub wstrzymanych albo zamówienia testowe | Statusy tych ID w WooCommerce |
Stała luka, która nie zmienia się między bramkami ani urządzeniami, to najpewniej zgody. Ta część powinna zostać. To ludzie, którzy powiedzieli “nie”, a GA4 mówi o nich prawdę, nie licząc ich.
Co robią Przelewy24, PayU i BLIK po zapłacie?
Dokumentacja polskich bramek dobrze pokazuje, dlaczego przeglądarka nie może być jedynym świadkiem płatności. Specyfikacja API Przelewy24 opisuje dwa osobne adresy: urlReturn, czyli adres powrotu po zakończeniu transakcji, i urlStatus, na który wysyłany jest status transakcji. Dalej czytamy:
“Powiadomienie jest wysyłane niezależnie od tego, czy klient został przekierowany na adres “urlReturn”.”
Przelewy24, dokumentacja REST API, tłumaczenie własne
Ta sama dokumentacja podaje, że transakcja jest potwierdzona dopiero po weryfikacji, a jeśli klient zapłaci i wróci do sklepu, ale sklep transakcji nie zweryfikuje, pieniądze nie trafią do sprzedawcy. Powrót klienta niczego więc nie dowodzi. Przelewy24 opisuje też BLIK “poziomu 0”, w którym klient wpisuje kod bezpośrednio na stronie sklepu, bez przekierowania do panelu P24; to, czy twoja wtyczka go używa, sprawdzisz w jej ustawieniach.
PayU w referencji API opisuje continueUrl jako adres przekierowania klienta po rozpoczęciu płatności. Klient może więc wrócić na stronę sklepu, zanim płatność się rozstrzygnie. Powiadomienia na notifyUrl PayU wysyła asynchronicznie, zaraz po zmianie statusu płatności.
Schemat jest wspólny: powiadomienie serwer do serwera to pewny sygnał zapłaty, a przekierowanie jest uprzejmością wobec klienta. Wtyczki bramek w WooCommerce zamieniają to powiadomienie w status opłacony. Do tego momentu warto przypiąć zakup w GA4.
Jak wysłać zakup z serwera przez Measurement Protocol?
Measurement Protocol GA4 przyjmuje zdarzenia przez HTTP z twojego serwera. Google jasno określa jego rolę:
“Measurement Protocol ma uzupełniać automatyczne zbieranie danych przez gtag, Tag Managera i Google Analytics for Firebase, a nie je zastępować.”
Google Analytics, omówienie Measurement Protocol, tłumaczenie własne
Tag w przeglądarce zostaje więc do zachowań, a serwer wysyła tylko zakup. Konstrukcja kodu idzie za dokumentacją punkt po punkcie:
- client_id. Dokumentacja referencyjna przyjmuje pełną wartość ciasteczka z identyfikatorem klienta. Kod zapisuje wartość
_gaw zamówieniu przy składaniu, bez parsowania. - session_id. Jedyny udokumentowany sposób odczytu to
gtag('get', '<target>', 'session_id', callback)z dokumentacji gtag.js. Według omówienia geolokalizacja i urządzenie z sesji zostaną przypisane, jeśli wyśleszsession_idw ciągu 24 godzin od startu sesji. Kod odczytuje go na stronie kasy i trzyma przez godzinę. - Zgoda. Obiekt
consentw żądaniu obejmuje tylkoad_user_dataiad_personalization, a gdy go nie podasz, GA4 bierze stan zgody z interakcji online tego klienta. Kod niczego nie wymyśla: brak_gaprzy składaniu zamówienia oznacza brak client_id i takie zamówienie nie jest wysyłane. - Błędy. Measurement Protocol zwraca 2xx nawet dla błędnego ładunku, a przy odpowiedzi innej niż 2xx Google zaleca poprawić żądanie i nie ponawiać tego samego. Najpierw sprawdź zdarzenie na
/debug/mp/collect, który zwracavalidationMessagesi niczego nie zapisuje. - Czas. Zdarzenia można cofnąć w czasie najwyżej o 72 godziny. Zakup wysłany w chwili potwierdzenia płatności nie potrzebuje znacznika czasu.
- Zbieranie w UE. Przewodnik Google każe użyć hosta
region1.google-analytics.com, jeśli dane mają być zbierane w Unii.
Zapisz kod jako wtyczkę must-use (wp-content/mu-plugins/ga4-mp-purchase.php), a ID pomiaru i klucz API zdefiniuj w wp-config.php. Klucz API tworzysz w ustawieniach strumienia danych internetowych w GA4.
<?php
/**
* Plugin Name: GA4 purchase via Measurement Protocol
* Description: Sends the GA4 purchase from the server when WooCommerce records the payment.
*/
defined( 'ABSPATH' ) || exit;
// In wp-config.php, never in the theme:
// define( 'WPP_GA4_MEASUREMENT_ID', 'G-XXXXXXXXXX' );
// define( 'WPP_GA4_API_SECRET', 'secret-from-the-data-stream-settings' );
// 1. Checkout page: ask gtag for the GA4 session_id and keep it for an hour.
add_action( 'wp_footer', function () {
if ( ! defined( 'WPP_GA4_MEASUREMENT_ID' ) || ! is_checkout() || is_order_received_page() ) {
return;
}
?>
<script>
(function () {
if (typeof gtag !== 'function' || document.cookie.indexOf('_ga=') === -1) return;
gtag('get', '<?php echo esc_js( WPP_GA4_MEASUREMENT_ID ); ?>', 'session_id', function (sid) {
if (sid) document.cookie = 'wpp_ga_sid=' + encodeURIComponent(sid) + '; path=/; max-age=3600; SameSite=Lax; Secure';
});
})();
</script>
<?php
} );
// 2. Order creation (classic and block checkout): copy the identifiers into order meta.
function wpp_ga4_store_ids( $order ) {
if ( empty( $_COOKIE['_ga'] ) ) {
return; // No analytics cookie: consent was denied or the tag never ran.
}
$order->update_meta_data( '_wpp_ga_client_id', sanitize_text_field( wp_unslash( $_COOKIE['_ga'] ) ) );
$sid = isset( $_COOKIE['wpp_ga_sid'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['wpp_ga_sid'] ) ) : '';
if ( ctype_digit( $sid ) ) {
$order->update_meta_data( '_wpp_ga_session_id', $sid );
}
$order->save_meta_data();
}
add_action( 'woocommerce_checkout_order_created', 'wpp_ga4_store_ids' );
add_action( 'woocommerce_store_api_checkout_order_processed', 'wpp_ga4_store_ids' );
// 3. One POST to /mp/collect. Returns true on 2xx.
function wpp_ga4_mp_send( $order, array $event ) {
if ( ! defined( 'WPP_GA4_MEASUREMENT_ID' ) || ! defined( 'WPP_GA4_API_SECRET' ) ) {
return false;
}
$client_id = $order->get_meta( '_wpp_ga_client_id' );
if ( ! $client_id ) {
return false; // No consent signal stored at checkout: send nothing.
}
$session_id = $order->get_meta( '_wpp_ga_session_id' );
if ( $session_id ) {
$event['params']['session_id'] = $session_id;
}
$event['params']['engagement_time_msec'] = 1;
$url = add_query_arg(
array(
'measurement_id' => WPP_GA4_MEASUREMENT_ID,
'api_secret' => WPP_GA4_API_SECRET,
),
'https://region1.google-analytics.com/mp/collect' // EU collection; www.google-analytics.com otherwise.
);
$response = wp_remote_post( $url, array(
'timeout' => 5,
'headers' => array( 'Content-Type' => 'application/json' ),
'body' => wp_json_encode( array(
'client_id' => $client_id,
'events' => array( $event ),
) ),
) );
$code = wp_remote_retrieve_response_code( $response );
if ( $code < 200 || $code > 299 ) {
// Google's docs: fix the request, do not retry the same one.
wc_get_logger()->warning( 'GA4 MP ' . $event['name'] . ' failed for order ' . $order->get_order_number() . ': ' . ( is_wp_error( $response ) ? $response->get_error_message() : $code ), array( 'source' => 'ga4-mp' ) );
return false;
}
return true;
}
// 4. Purchase: fired when the gateway confirms payment, or when the order reaches a paid status.
function wpp_ga4_send_purchase( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order || $order->get_meta( '_wpp_ga_mp_sent' ) ) {
return;
}
$items = array();
$value = 0.0;
foreach ( $order->get_items() as $item ) {
$qty = (float) $item->get_quantity();
$price = $qty > 0 ? round( (float) $item->get_total() / $qty, 2 ) : 0.0;
$product = $item->get_product();
$items[] = array(
'item_id' => ( $product && $product->get_sku() ) ? $product->get_sku() : (string) $item->get_product_id(),
'item_name' => $item->get_name(),
'price' => $price,
'quantity' => $qty,
);
$value += $price * $qty;
}
$sent = wpp_ga4_mp_send( $order, array(
'name' => 'purchase',
'params' => array(
'transaction_id' => (string) $order->get_order_number(), // Same ID the browser tag and the export use.
'currency' => $order->get_currency(),
'value' => round( $value, 2 ), // Sum of price * quantity, as in Google's example.
'tax' => (float) $order->get_total_tax(),
'shipping' => (float) $order->get_shipping_total(),
'items' => $items,
),
) );
if ( $sent ) {
// ponytail: meta flag, no lock; two webhooks in the same second could both send.
$order->update_meta_data( '_wpp_ga_mp_sent', time() );
$order->save_meta_data();
}
}
add_action( 'woocommerce_payment_complete', 'wpp_ga4_send_purchase' );
add_action( 'woocommerce_order_status_processing', 'wpp_ga4_send_purchase' );
add_action( 'woocommerce_order_status_completed', 'wpp_ga4_send_purchase' );
// 5. Refunds: the same transaction_id, so GA4 nets them against the purchase.
add_action( 'woocommerce_order_refunded', function ( $order_id, $refund_id ) {
$order = wc_get_order( $order_id );
$refund = wc_get_order( $refund_id );
if ( ! $order || ! $refund || ! $order->get_meta( '_wpp_ga_mp_sent' ) ) {
return;
}
$params = array(
'transaction_id' => (string) $order->get_order_number(),
'currency' => $order->get_currency(),
'value' => (float) $refund->get_amount(),
);
// Refunded line items come back with negative quantities; an amount-only refund has none.
foreach ( $refund->get_items() as $item ) {
$product = $item->get_product();
$params['items'][] = array(
'item_id' => ( $product && $product->get_sku() ) ? $product->get_sku() : (string) $item->get_product_id(),
'quantity' => abs( (float) $item->get_quantity() ),
);
}
wpp_ga4_mp_send( $order, array( 'name' => 'refund', 'params' => $params ) );
}, 10, 2 );Dlaczego te haki: woocommerce_payment_complete uruchamia się, gdy bramka wywoła WC_Order::payment_complete(), zwykle z obsługi powiadomienia serwerowego, czyli przy Przelewy24 po odebraniu i zweryfikowaniu urlStatus. Przelew tradycyjny i pobranie nigdy go nie wywołują; dochodzą do statusu opłaconego, gdy zmienisz go ty albo bramka, i to łapią woocommerce_order_status_processing oraz woocommerce_order_status_completed. Flaga _wpp_ga_mp_sent blokuje drugą i trzecią wysyłkę. Zwroty idą na haku woocommerce_order_refunded, który działa też przy zwrotach częściowych. Przewodnik e-commerce Google wymaga przy zdarzeniu refund transaction_id i zwracanych pozycji z item_id oraz quantity, więc kod dokłada zwrócone pozycje, jeśli są. Rozszerzenie Google Analytics for WooCommerce w ogóle nie wysyła zdarzenia refund (w jego kodzie nie ma obsługi zwrotów), co dodatkowo rozjeżdża metrykę Transakcje i listę zamówień. Haki zapisujące identyfikatory obsługują obie kasy: woocommerce_checkout_order_created w klasycznej kasie i woocommerce_store_api_checkout_order_processed w kasie blokowej. Oba działają w żądaniu samego klienta, więc ciasteczka są dostępne.
Wartość value liczę tak, jak opisuje ją komentarz w przykładzie Google: suma ceny razy ilość dla wszystkich pozycji, z podatkiem i wysyłką w osobnych polach. Jeśli tag w przeglądarce liczy wartość inaczej (na przykład brutto z VAT), wyrównaj to, zanim zaczniesz porównywać przychód.
Jeszcze jedna uwaga z dokumentacji: Google określa Measurement Protocol jako produkt dojrzały, utrzymywany bez planów wycofania, a do nowych integracji serwer do serwera poleca dziś Data Manager API. Dla jednego zdarzenia zakupu w jednym sklepie Measurement Protocol to mniej pracy i przy nim bym został.
Jak nie policzyć tego samego zamówienia dwa razy?
Strona pomocy o deduplikacji opisuje usuwanie duplikatów po ID transakcji dla tego samego użytkownika w strumieniach internetowych. O Measurement Protocol nie ma tam ani słowa, więc nie zakładam, że GA4 scali zakup z przeglądarki i z serwera dla tego samego zamówienia. Wybierz jedną z dwóch dróg:
- Wyłącz zakup w przeglądarce. W Google Analytics for WooCommerce odznacz “Purchase Transactions”. Reszta lejka (wyświetlenia produktów, dodanie do koszyka, rozpoczęcie kasy) dalej idzie z przeglądarki. Stracisz diagnostykę
_ga_trackeddla nowych zamówień, więc zrób uzgodnienie przed przełączeniem. - Wysyłaj zakup z serwera do drugiej usługi GA4. Tak radzi autor z Search Engine Journal każdemu, kto zaczyna teraz: osobna usługa przez cały IV kwartał, żeby nic nie policzyło się podwójnie. W styczniu porównasz obie z eksportem zamówień i wtedy zdecydujesz.
Dwa tygodnie przed Black Friday to zły moment na zmianę zdarzenia zakupu, od którego zależą kampanie i raporty. Druga droga zostawia obecne liczby w spokoju, a ty w tym czasie zbierasz dowody.
Które ustawienia trybu zgody zmieniają liczby?
Google opisuje dwa warianty trybu zgody. W podstawowym tagi nie ładują się, dopóki odwiedzający nie kliknie w banerze, i przed zgodą nic nie jest wysyłane. W zaawansowanym tagi ładują się z domyślną odmową i wysyłają pingi bez ciasteczek; tego wymaga modelowanie behawioralne. Żaden z wariantów nie zamieni odmowy w zakup z numerem transakcji.
Co sprawdzić w sklepie na WooCommerce przed szczytem:
- Stan domyślny ustawiany przez rozszerzenie. Google Analytics for WooCommerce ustawia domyślne stany zgody przy inicjalizacji i pozwala je zmienić filtrem
woocommerce_ga_gtag_consent_modes. Jeśli twoja wtyczka zgód ustawia własne wartości domyślne, WooCommerce wskazuje ten filtr jako sposób, żeby rozszerzenie ich nie nadpisywało. - Aktualizację zgody. Dokumentacja WooCommerce mówi, że integracja trybu zgody musi zaktualizować stan po wyrażeniu zgody. Sprawdź, czy stan zmienia się na granted na tej samej odsłonie, bo inaczej pierwsza strona po zgodzie przepada.
- Limity planu platformy zgód. Baner, który wyłączył się w grudniu w audycie SEJ, to był limit rozliczeniowy. Porównaj swój z liczbą odsłon z zeszłego grudnia.
- Co dzieje się po “odrzuć”. Odwrotną awarię, czyli baner odrzucony, a skrypty dalej się ładują, opisałem we wpisie baner cookies odrzucony, a skrypty dalej się ładują. To już problem prawny.
Podstawowa konfiguracja GA4 w WordPressie, tag i domyślne zgody są w poradniku o dodawaniu Google Analytics 4 do WordPressa. Serwerowy Tag Manager, który rozwiązuje inny problem (zbieranie w domenie sklepu i dane dla platform reklamowych), opisuję we wpisie o data strength w Google Ads i server-side GTM. Zamówienia składane przez agentów AI bez przeglądarki to osobny przypadek z własnym problemem client_id: analityka WooCommerce a agenci AI. Jeśli prowadzisz też sklep na Shopify, podobną lukę na tamtej platformie rozbiera wpis klienci dochodzą do kasy, ale nie płacą.
Czy na ciasteczko analityczne potrzebna jest zgoda w Polsce?
Tak. Od 10 listopada 2024 obowiązuje ustawa Prawo komunikacji elektronicznej (Dz.U. 2024 poz. 1221). Jej art. 399 ust. 1 pozwala przechowywać informacje albo uzyskiwać do nich dostęp w urządzeniu końcowym użytkownika pod warunkiem, że użytkownik zostanie uprzednio poinformowany w sposób jednoznaczny, łatwy i zrozumiały o celu i o możliwości określenia warunków przez ustawienia, a potem wyrazi na to zgodę. Wyjątek z ust. 3 obejmuje tylko przypadki, gdy jest to konieczne do transmisji komunikatu albo do dostarczenia usługi, której użytkownik zażądał. Ciasteczko _ga do żadnej z tych kategorii nie należy. Art. 400 odsyła w sprawie samej zgody do przepisów o ochronie danych osobowych, czyli do RODO, które w Polsce nadzoruje Prezes UODO. Tekst czytałem w wersji ogłoszonej w Dzienniku Ustaw (przez ISAP i API Sejmu); tekstu jednolitego ustawy na 30 października 2026 tam nie było.
W praktyce dla tego wpisu: zakup z serwera opisany wyżej wysyła się wyłącznie dla zamówień, które w chwili składania miały ciasteczko analityczne, czyli od osób, które zgodę wyraziły. Kod nie omija zgody i nie powinien.
Co sprawdzić przed Black Friday?
- Wyeksportuj zamówienia i ID transakcji z GA4 za zeszły listopad i grudzień, uruchom uzgodnienie, zapisz odsetek dla każdej bramki i urządzenia.
- Powtórz dla ostatnich czterech tygodni. Spadek między tymi okresami to błąd konfiguracji, który jeszcze zdążysz naprawić.
- Złóż prawdziwe zamówienie z telefonu każdą bramką, jaką masz (BLIK, szybki przelew, karta), raz zamknij kartę na stronie płatności i sprawdź, czy zamówienie dostaje status opłacony i co widzi GA4.
- Sprawdź duplikaty ID transakcji i znajdź drugi tag, jeśli jest.
- Upewnij się, że zamówienia oczekujące i wstrzymane (przelew tradycyjny) nie trafiają do GA4 jako zakupy, albo świadomie to zaakceptuj i zapisz.
- Porównaj limit planu platformy zgód z ruchem z zeszłego grudnia i przekieruj jej ostrzeżenia do osoby, która może zapłacić za wyższy plan.
- Sprawdź, czy CDN nie cache’uje strony potwierdzenia zamówienia.
- Jeśli dodajesz zakup z serwera, zwaliduj go na
/debug/mp/collecti na czas szczytu wysyłaj go do osobnej usługi GA4. - Wpisz do kalendarza cotygodniowy test: zakupy w GA4 podzielone przez zamówienia internetowe w WooCommerce. Audyt SEJ pokazuje, że przerwy zauważano po tygodniach.
Kiedy warto wezwać programistę?
Eksport i uzgodnienie to popołudnie dla kogoś, kto swobodnie używa WP-CLI. Zakup z serwera dotyka kasy, powiadomień z bramek i zgód, czyli trzech miejsc, w których pomyłka kosztuje pieniądze albo rodzi problem prawny, i trzeba go przetestować z każdą bramką w sklepie. Jeśli wolisz, żeby ktoś to wdrożył i sprawdził na stagingu przed szczytem, robię to jako programista WooCommerce.





