Das WooCommerce-Dashboard zeigt eine Zahl, GA4 eine kleinere, und jeden November fragt jemand im Team oder in der Geschäftsführung, welche stimmt. Für Bestellungen hat WooCommerce recht. GA4 hat recht mit dem, was in GA4 angekommen ist. Nützlich ist die Frage, welche Bestellungen fehlen und warum, und die beantworten Sie mit zwei Exporten und einem Join über die Transaktions-ID.
Dieser Beitrag liefert das Verfahren, den Code dafür und einen serverseitigen Kauf, den Sie vor den umsatzstärksten Wochen einbauen können. Alle Aussagen zu GA4 und WooCommerce stammen aus der Dokumentation oder dem Quellcode von Google und WooCommerce, gelesen am 30. Oktober 2026. Die Rechtslage ist für Deutschland, Österreich und die Schweiz aus den amtlichen Gesetzestexten zusammengefasst.
Was hat der Audit im Search Engine Journal tatsächlich gezeigt?
Am 28. Oktober 2026 veröffentlichte Search Engine Journal einen Audit von Christian Goodrich, Head of Organic Search bei der Agentur SOZO. Er hat GA4 bei acht britischen Onlinehändlern mit den eigenen Bestelldaten verglichen; sechs davon ließen einen Abgleich zu. Er legt offen, dass seine Agentur mit allen acht gearbeitet hat. Die Händler bleiben anonym, und der Artikel nennt keine Shop-Plattform. Ein WooCommerce-Befund ist das also nicht, wohl aber ein Muster, das ich auf WooCommerce-Shops im deutschsprachigen Raum genauso prüfen würde.
Die „66 %“ aus der Überschrift tauchen im Text nicht auf. Für den betreffenden Händler stehen dort diese Zahlen:
„GA4 erfasste 81 % der Web-Bestellungen des Shops im vierten Quartal 2024, 41 % im vierten Quartal 2025 und nur 32 % über die Weihnachtszeit.“
Search Engine Journal, 28. Oktober 2026, eigene Übersetzung
32 % erfasst heißt 68 % verloren über Weihnachten, was der Titel auf zwei Drittel rundet. Im selben Quartal stiegen die Web-Bestellungen dieses Shops von 5.874 auf 7.686, während GA4 einen Rückgang um rund ein Drittel zeigte. Wer in so einer Lage Budget nach GA4 verteilt, kürzt bei Kanälen, die gerade mehr verkaufen.
Die Ursachen, die er nennt, hier in enger Paraphrase:
- Eine stetige Einwilligungslücke. Ein Teil der Kundschaft lehnt jede Woche das Tracking ab, also sieht GA4 nie alle Bestellungen.
- Ein Cookie-Banner, das sich selbst abschaltete. Consent-Plattformen mit einem monatlichen Kontingent an Seitenaufrufen liefen im Dezember leer, und das Banner erschien bis zum nächsten Abrechnungstermin nicht mehr.
- Ein Ausfall mit spätem Nachladen. Ein Shop verbuchte 2.215 Bestellungen in fünf Wochen, GA4 keine einzige; rund 1.000 wurden später nachgeladen, fast alle als „Unassigned“.
- Doppelte Käufe. In einem Shop hatten 657 von 3.365 Bestellnummern mehr als ein Kaufereignis, zusammen 735 zusätzliche Käufe. In der Quartalssumme verdeckten die Dubletten einen Teil des Verlusts.
- Bots. Bots, die browserähnlichen Code ausführten, wurden als Besucher gezählt.
Ein Satz aus dem Artikel bestimmt den Rest dieses Beitrags: Serverseitiges Tracking ändert nichts an den Pflichten rund um die Einwilligung, die stetige Lücke bleibt also bestehen. Das sehe ich genauso, und der Code weiter unten ist um genau diese Regel gebaut. Für Shops in Deutschland und Österreich ist das ohnehin keine Stilfrage, dazu mehr im Abschnitt zum Recht.
Was der Artikel nicht behandelt, sind Zahlungsanbieter und die Bestellbestätigungsseite. In einem WooCommerce-Shop schaue ich dort zuerst hin, aus dem Grund im nächsten Abschnitt.
Warum verliert WooCommerce Käufe in GA4?
Die kostenlose Erweiterung Google Analytics for WooCommerce ist in vielen Shops die Standardlösung. Ihre Dokumentation nennt die Abhängigkeit offen:
„Kauftransaktionen: Erfassen Sie Käufe auf Ihrer Website. Dafür ist ein Zahlungsanbieter nötig, der nach der Zahlung auf die Danke-Seite beziehungsweise die Seite ‚Bestellung erhalten‘ weiterleitet.“
WooCommerce-Dokumentation, Google Analytics for WooCommerce, eigene Übersetzung
Das Kaufereignis sendet der Browser der Käuferin auf der Bestellbestätigungsseite. Alles, was das Laden dieser Seite oder das Ausführen des Tags darauf verhindert, entfernt den Kauf aus GA4, während WooCommerce die Bestellung weiter führt. Die typischen Gründe:
- Einwilligung abgelehnt. Dieselbe Dokumentation sagt, dass Besucher im EWR, im Vereinigten Königreich und in der Schweiz dem Tracking zustimmen müssen, und dass Google Analytics und Google Ads ohne Einwilligung dort keine Nutzerdaten erfassen. Die Erweiterung bringt kein eigenes Banner mit; sie liest die Einwilligung über die WP Consent API aus einem separaten Consent-Plugin.
- Werbeblocker und Skriptfehler. Die Fehlerbehebung von WooCommerce nennt Browsererweiterungen, Werbeblocker, JavaScript-Fehler und mehrere Tracking-Instanzen.
- Die Käuferin kehrt nie von der Zahlungsseite zurück. Gehostete Zahlungsseiten schicken die Kundschaft per Weiterleitung zurück. Schließt jemand nach dem Bezahlen den Tab oder gibt die Banking-App die Kontrolle nicht an den Browser zurück, wird die Bestellung über die Server-Benachrichtigung des Zahlungsanbieters trotzdem bezahlt, und der Kauf im Browser wird nie ausgelöst.
- Eine Cache-Schicht vor der Bestellbestätigungsseite. WooCommerce selbst sendet auf Warenkorb, Kasse und Kundenkonto No-Cache-Header und setzt
DONOTCACHEPAGE, und die Bestätigungsseite ist ein Endpunkt der Kasse (WC_Cache_Helper::prevent_cachingim WooCommerce-Kern). Das Risiko ist eine CDN- oder Proxy-Regel, die HTML ungeachtet dieser Header zwischenspeichert. - Falsche oder wiederholte Transaktions-IDs. Googles Hilfeseite zur Deduplizierung über Transaktions-IDs warnt, dass bei leerer Transaktions-ID alle solchen Käufe zusammen dedupliziert werden und dass wiederverwendete IDs zu Unterzählung führen.
Die Erweiterung kann auch zu viel zählen. In ihrem Quellcode auf GitHub (trunk, Versionskopf 2.4.4, gelesen am 30. Oktober 2026) wird der Kauf über den Hook woocommerce_thankyou für jede Bestellung vorbereitet, die weder fehlgeschlagen noch storniert ist und deren Bestellschlüssel in der URL gültig ist. Eine Vorkasse-Bestellung im Status „In Wartestellung“ oder eine ausstehende Bestellung, deren Käufer die Danke-Seite gesehen hat, geht als Kauf raus, ob sie je bezahlt wird oder nicht. Gerade im deutschsprachigen Raum, wo Vorkasse per Überweisung in vielen Shops noch angeboten wird, lohnt der Blick. Die Dokumentation der Bestellstatus beschreibt „In Wartestellung“ als Bestellung, die auf die Zahlungsbestätigung wartet, und „Zahlung ausstehend“ als Bestellung ohne erfolgte Zahlung. Gleichen Sie deshalb nur gegen bezahlte Status ab.
Derselbe Quellcode liefert eine kostenlose Diagnose. Rendert die Bestätigungsseite mit gültigem Schlüssel, schreibt die Erweiterung serverseitig das Meta-Feld _ga_tracked = 1 an die Bestellung, noch bevor der Browser etwas sendet. Damit zerfallen fehlende Bestellungen in zwei Gruppen:
- Kein
_ga_trackedan einer bezahlten Bestellung: Die Bestätigungsseite wurde nie gerendert. Prüfen Sie die Rückleitung des Zahlungsanbieters. _ga_trackedvorhanden, Kauf fehlt in GA4: Die Seite wurde gerendert, und der Browser hat das Ereignis verloren. Prüfen Sie Einwilligung, Blocker und Skriptfehler.
Die Erweiterung sendet transaction_id standardmäßig als $order->get_order_number() (Filter woocommerce_ga_order_id). Verwenden Sie diesen Wert überall in den folgenden Schritten. Sonst scheitert der Join, sobald ein Plugin für eigene Bestellnummernkreise im Spiel ist.
Wie exportiere ich WooCommerce-Bestellungen für den Vergleich?
Nehmen Sie nur Web-Bestellungen in bezahlten Status für einen abgeschlossenen Zeitraum. Das Verfahren aus dem Search Engine Journal schließt Marktplatz-, Abo- und manuell angelegte Bestellungen aus, und WooCommerce erlaubt das über created_via: Die klassische Kasse setzt laut WooCommerce-Kern checkout, die Block-Kasse (Store API) setzt store-api.
Das Skript funktioniert mit HPOS und mit der alten Speicherung in Posts, weil es über wc_get_orders() läuft. Starten Sie es mit WP-CLI auf Staging oder Produktion (es liest nur):
<?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 );Drei Spalten brauchen eine Erklärung. device stammt aus der Bestellzuordnung von WooCommerce, die seit Version 8.5 _wc_order_attribution_device_type als Mobile, Tablet oder Desktop speichert, abgeleitet aus dem User-Agent. thankyou_seen ist das oben beschriebene Feld _ga_tracked der Erweiterung. has_ga_cookie bleibt auf „no“, bis Sie den serverseitigen Code weiter unten installieren; danach zeigt es, welche Bestellungen beim Checkout ein Analyse-Cookie trugen.
Erstattete Bestellungen bleiben drin, weil sie bezahlt wurden und GA4 für sie einen Kauf haben sollte. Wer auf einem HPOS-Shop lieber SQL nutzt, kommt mit diesem Filter zum selben Ergebnis (Präfix wp_ anpassen):
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');Die SQL-Abfrage liefert die interne id. Ändert ein Plugin die Bestellnummern, ordnen Sie die ID vor dem Join der Nummer zu oder nehmen Sie den PHP-Export.
Wie exportiere ich GA4-Käufe nach Transaktions-ID?
Nutzen Sie die Dimension Transaktions-ID mit der Metrik E-Commerce-Käufe. Im Schema der GA4 Data API zählt ecommercePurchases Kaufereignisse, während transactions mehrere Ereignistypen zählt, darunter refund. Search Engine Journal warnt aus demselben Grund vor der Metrik Transaktionen, weil sie Erstattungen wie Käufe mitzählt.
In der Oberfläche bauen Sie eine Exploration im freien Format mit Transaktions-ID als Zeilen und E-Commerce-Käufen als Wert, setzen denselben Zeitraum wie im Bestellexport und exportieren als CSV. Über die Data API heißt die Dimension transactionId und die Metrik ecommercePurchases. Ergänzen Sie deviceCategory oder browser in einem zweiten Export, wenn Sie die Aufteilung aus GA4-Sicht brauchen. Ist die GA4-Oberfläche auf Deutsch gestellt, heißen die Spalten im CSV deutsch; benennen Sie sie vor dem Skriptlauf in „Transaction ID“ und „Ecommerce purchases“ um oder stellen Sie die Sprache für den Export auf Englisch. Zwei Punkte prüfen Sie, bevor Sie der Datei trauen:
- Datenschwellen. Googles Hilfeseite zu Datenschwellen sagt, dass sie systemseitig festgelegt sind, sich nicht anpassen lassen und Zeilen zurückhalten können, wenn ein Bericht demografische Daten oder Google Signals enthält. Beschränken Sie die Exploration auf Transaktions-ID und Kaufanzahl.
- Modellierte Daten. Die verhaltensbasierte Modellierung für den Einwilligungsmodus erscheint nur unter der Berichtsidentität „Gemischt“ und setzt mindestens 1.000 Ereignisse pro Tag mit
analytics_storage='denied'über 7 Tage sowie 1.000 einwilligende Nutzer pro Tag an 7 der letzten 28 Tage voraus. Modellierte Nutzer tragen keine Transaktions-IDs echter Bestellungen, also geht es bei diesem Abgleich um beobachtete Daten.
Wie berechne ich die Lücke je Zahlungsanbieter und Gerät?
Das Skript verbindet beide Dateien und gibt die Erfassungsquote insgesamt sowie je Zahlungsart, je Gerät und je Wert von thankyou_seen aus. Es zählt außerdem Transaktions-IDs mit mehr als einem Kaufereignis, also die Dublettenprüfung aus dem Audit, und GA4-IDs ohne passende Web-Bestellung. Python 3, nur Standardbibliothek:
#!/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])Lassen Sie es einmal für November und Dezember des Vorjahres laufen und einmal für die letzten vier Wochen. Eine einzelne Prozentzahl sagt wenig; die Aufteilung liefert die Antwort. Die Spalte payment_method enthält die interne Kennung des Gateways, wie das Plugin sie setzt, also zum Beispiel einen Wert mit „stripe“ oder „mollie“ im Namen. Damit sehen Sie sofort, welcher Zahlungsweg abfällt.
Wie lese ich das Ergebnis?
| Was Sie sehen | Wahrscheinlichste Ursache | Wo Sie nachsehen |
|---|---|---|
| Ein Zahlungsanbieter deutlich unter den anderen | Kundschaft kehrt von dessen Seite nicht zurück | Rückleitungs-URL, Bestätigungsseite auf dem Smartphone, thankyou_seen für diesen Anbieter |
thankyou_seen = no bei vielen bezahlten Bestellungen | Bestätigungsseite nicht gerendert | Weiterleitungen des Anbieters, geschlossene Tabs, Plugin-Fehler auf dieser Seite |
thankyou_seen = yes, aber niedrige Erfassung | Browser hat das Ereignis verloren | Einwilligungsquote, Werbeblocker, Konsolenfehler, Standardwert der Einwilligung in der Erweiterung |
| Mobil deutlich unter Desktop | Wechsel in Banking- oder Wallet-Apps, In-App-Browser | Testkauf auf dem Smartphone mit jedem Zahlungsanbieter |
| Im Oktober gut, im Dezember eingebrochen | Etwas ist unter Last ausgefallen oder ein Kontingent lief leer | Tariflimits der Consent-Plattform, Deployments in dieser Woche, CDN-Regeln |
| Viele IDs mit mehr als einem Kauf | Bestätigungsseite neu geladen oder zwei Tags | Doppelte Tags in GTM und Plugins, Verhalten beim Neuladen |
| GA4-IDs ohne passende Web-Bestellung | Ausstehende oder wartende Bestellungen gezählt, oder Testbestellungen | Status dieser IDs in WooCommerce |
Eine stetige Lücke, die sich nicht nach Anbieter oder Gerät verschiebt, ist sehr wahrscheinlich die Einwilligung. Diese Lücke soll bleiben. Es sind die Besucher, die Nein gesagt haben, und GA4 sagt über sie die Wahrheit, indem es sie nicht zählt. Wie groß dieser Anteil in Ihrem Shop ist, zeigt die Statistik Ihres Consent-Tools; fremde Durchschnittswerte helfen dabei wenig.
Was machen Stripe und Mollie nach der Zahlung?
Die Dokumentation der Zahlungsanbieter zeigt, warum der Browser nicht der einzige Zeuge sein darf. Zwei Anbieter, die in DACH-Shops verbreitet sind und deren Unterlagen ich für diesen Beitrag gelesen habe:
Mollie trennt in der API-Referenz zum Anlegen einer Zahlung zwei Adressen. redirectUrl ist die URL, auf die die Kundschaft nach dem Zahlungsvorgang weitergeleitet wird. webhookUrl ist die Adresse, an die Mollie Statusänderungen der Zahlung schickt, und Mollie schreibt dazu, dass Ihnen ohne Webhook wichtige Statusänderungen entgehen.
Stripe wird in der Anleitung zur Auftragsabwicklung mit Checkout noch deutlicher:
„Sie können sich nicht darauf verlassen, die Abwicklung nur über Ihre Checkout-Landingpage auszulösen, weil nicht garantiert ist, dass Kundinnen und Kunden diese Seite besuchen. Zum Beispiel kann jemand erfolgreich bezahlen und dann die Internetverbindung verlieren, bevor Ihre Landingpage lädt.“
Stripe-Dokumentation, Fulfill orders, eigene Übersetzung
Stripe beschreibt dort außerdem Zahlungsarten mit verzögerter Erfolgsmeldung, etwa Lastschriften und andere Banküberweisungen. Deren Erfolg kommt als eigenes Ereignis checkout.session.async_payment_succeeded, wenn die Zahlung später durchgeht. Für den Abgleich heißt das: Eine Lastschrift kann Tage nach dem Besuch der Danke-Seite bezahlt werden oder scheitern. Der Browser-Kauf hat dann längst gezählt, egal wie es ausgeht.
Das Muster ist bei gehosteten Zahlungsseiten überall dasselbe. Die Server-Benachrichtigung ist das verlässliche Signal für die Zahlung, die Weiterleitung ist ein Entgegenkommen an die Kundschaft. Gateway-Plugins für WooCommerce machen aus dieser Benachrichtigung einen bezahlten Status, und genau an dieses Ereignis gehört der GA4-Kauf.
Wie sende ich den Kauf über das Measurement Protocol vom Server?
Das GA4 Measurement Protocol nimmt Ereignisse per HTTP von Ihrem Server an. Google ist bei seiner Rolle eindeutig:
„Das Measurement Protocol soll die automatische Erfassung über gtag, Tag Manager und Google Analytics for Firebase ergänzen und sie nicht ersetzen.“
Google Analytics, Überblick Measurement Protocol, eigene Übersetzung
Behalten Sie also das Browser-Tag für das Verhalten und nutzen Sie den Server nur für den Kauf. Der Aufbau unten folgt der Dokumentation Punkt für Punkt:
- client_id. Die Referenz akzeptiert ein Client-ID-Cookie mit seinem vollen Wert. Der Code speichert den Wert des
_ga-Cookies beim Checkout an der Bestellung, ohne ihn zu zerlegen. - session_id. Der einzige dokumentierte Weg, sie zu lesen, ist
gtag('get', '<target>', 'session_id', callback)aus der gtag.js-Referenz. Laut Überblick gelten Geo- und Gerätedaten einer Sitzung, wenn Siesession_idim Ereignis mitsenden, und zwar innerhalb von 24 Stunden nach Sitzungsbeginn. Der Code liest sie auf der Kassenseite und hält sie eine Stunde vor. - Einwilligung. Das Objekt
consentim Request deckt nurad_user_dataundad_personalizationab. Fehlt es, übernimmt Google Analytics laut Referenz die Einwilligungseinstellungen aus den zugehörigen Online-Interaktionen dieses Clients. Der Code erfindet nichts: Kein_ga-Cookie beim Checkout heißt keine Client-ID, und diese Bestellung wird nicht gesendet. - Fehler. Das Measurement Protocol antwortet auch auf fehlerhafte Payloads mit 2xx. Bei einer Antwort außerhalb von 2xx sollen Sie laut Google den Request korrigieren und denselben nicht erneut senden. Validieren Sie vorher gegen
/debug/mp/collect, dasvalidationMessageszurückgibt und nichts speichert. - Zeitpunkt. Ereignisse lassen sich bis zu 72 Stunden rückdatieren. Ein Kauf, der bei Zahlungsbestätigung gesendet wird, braucht keinen Zeitstempel. Weil der Code im Moment der Zahlungsbestätigung sendet, spielt das Fenster für ihn keine Rolle; es betrifft nur Nachlieferungen aus einem Backlog.
- Erfassung in der EU. Googles Anleitung zum Senden empfiehlt den Host
region1.google-analytics.com, wenn die Erfassung in der EU stattfinden soll. Für Shops in Deutschland und Österreich ist das der naheliegende Standard; auch für die Schweiz spricht nichts dagegen.
Speichern Sie den Code als Must-use-Plugin (wp-content/mu-plugins/ga4-mp-purchase.php) und legen Sie Mess-ID und API-Secret in der wp-config.php fest. Das API-Secret erstellen Sie in den Einstellungen des GA4-Web-Datenstreams.
<?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 );Warum diese Hooks: woocommerce_payment_complete wird ausgelöst, wenn ein Gateway WC_Order::payment_complete() aufruft, typischerweise aus seinem Handler für die Server-Benachrichtigung, mit der Bestell-ID und der Transaktions-ID des Anbieters. Vorkasse per Überweisung und Nachnahme rufen das nie auf; diese Bestellungen erreichen einen bezahlten Status, wenn Sie oder der Anbieter ihn ändern, und das fangen woocommerce_order_status_processing und woocommerce_order_status_completed ab. Das Flag _wpp_ga_mp_sent verhindert, dass der zweite und dritte Aufruf erneut sendet.
Erstattungen gehen über woocommerce_order_refunded raus, der auch bei Teilerstattungen feuert. Googles E-Commerce-Anleitung verlangt die transaction_id und die erstatteten Artikel mit item_id und quantity, also hängt der Code die erstatteten Positionen an, wenn es welche gibt. Mit Widerrufen und Retouren wird das im Weihnachtsgeschäft schnell zur Routine. Die Erweiterung Google Analytics for WooCommerce sendet gar kein Erstattungsereignis (ihr Quellcode enthält keine Erstattungslogik), ein weiterer Grund, warum die Metrik Transaktionen und Ihre Bestellliste auseinanderlaufen.
Die Hooks zum Erfassen der IDs decken beide Kassen ab: woocommerce_checkout_order_created in der klassischen Shortcode-Kasse und woocommerce_store_api_checkout_order_processed in der Block-Kasse. Beide laufen im Request der Käuferin selbst, die Cookies sind also da.
Der value wird so berechnet, wie Googles E-Commerce-Beispiel es kommentiert, als Summe aus Preis mal Menge über alle Artikel, mit Steuer und Versand in eigenen Feldern. Rechnet Ihr Browser-Tag den Wert anders, etwa brutto inklusive Mehrwertsteuer, gleichen Sie beides an, bevor Sie Umsätze vergleichen.
Noch ein Hinweis von Google: Das Measurement Protocol ist nach eigener Aussage ein ausgereiftes, abgeschlossenes Produkt, das ohne Abkündigungspläne in Betrieb bleibt, und für neue Server-zu-Server-Integrationen empfiehlt Google inzwischen die Data Manager API. Für das Kaufereignis eines einzelnen Shops ist das Measurement Protocol die kleinere Aufgabe, und ich würde dabei bleiben.
Wie vermeide ich, dieselbe Bestellung doppelt zu zählen?
Googles Hilfeseite zur Deduplizierung beschreibt, dass Transaktions-IDs Käufe desselben Nutzers in Web-Datenstreams deduplizieren. Zum Measurement Protocol steht dort nichts, also verlasse ich mich nicht darauf, dass GA4 einen Browser-Kauf und einen Server-Kauf derselben Bestellung zusammenführt. Wählen Sie einen der beiden dokumentierten Wege:
- Browser-Kauf abschalten. Entfernen Sie in Google Analytics for WooCommerce den Haken bei „Purchase Transactions“. Der restliche Trichter (Produktansichten, In den Warenkorb, Checkout-Beginn) kommt weiter aus dem Browser. Sie verlieren die Diagnose über
_ga_trackedfür neue Bestellungen, also führen Sie den Abgleich einmal durch, bevor Sie umschalten. - Server-Kauf in eine zweite Property schicken. Das ist der Rat aus dem Search Engine Journal für alle, die jetzt anfangen: den Server-Kauf über das vierte Quartal in eine separate GA4-Property senden, damit nichts doppelt gezählt wird. Vergleichen Sie im Januar beide Properties mit dem Bestellexport und entscheiden Sie dann.
Zwei Wochen vor dem Black Friday ist ein schlechter Zeitpunkt, das Kaufereignis zu ändern, von dem Gebotsstrategien und Berichte abhängen. Der zweite Weg lässt die aktuellen Zahlen unangetastet, während Sie Belege sammeln.
Welche Einstellungen im Einwilligungsmodus verändern die Zahlen?
Google beschreibt zwei Varianten des Einwilligungsmodus. In der einfachen Variante laden die Tags erst, wenn der Besucher mit dem Banner interagiert, und vor der Einwilligung wird nichts gesendet. In der erweiterten Variante laden die Tags mit abgelehnten Standardwerten und senden bei abgelehnter Einwilligung cookielose Pings; die verhaltensbasierte Modellierung setzt genau diese Variante voraus. Keine der beiden macht aus einer Ablehnung einen Kauf mit Transaktions-ID.
Was Sie in einem WooCommerce-Shop vor der Hauptsaison prüfen sollten:
- Der Standardwert der Erweiterung. Google Analytics for WooCommerce setzt bei der Initialisierung Standardwerte für die Einwilligung und lässt sie über den Filter
woocommerce_ga_gtag_consent_modesändern. Setzt Ihr Consent-Plugin eigene Standardwerte, dokumentiert WooCommerce diesen Filter als Weg, das Überschreiben durch die Erweiterung zu verhindern. - Die Aktualisierung der Einwilligung. Laut WooCommerce-Dokumentation muss Ihre Integration den Einwilligungsstatus aktualisieren, nachdem Besucher zugestimmt haben. Testen Sie, dass der Status auf derselben Seitenansicht auf „granted“ wechselt und nicht erst auf der nächsten, sonst geht die erste Seite nach der Zustimmung verloren.
- Die Tariflimits Ihrer Consent-Plattform. Das Banner, das sich im Audit im Dezember abschaltete, war ein Abrechnungskontingent. Prüfen Sie Ihren Tarif gegen die Seitenaufrufe des letzten Dezembers. Feuert das Tag weiter, während das Banner fehlt, hat ein Shop in Deutschland oder Österreich zusätzlich ein rechtliches Problem.
- Was nach „Ablehnen“ passiert. Mein Beitrag über ein abgelehntes Cookie-Banner, bei dem das Tracking trotzdem lädt, behandelt den umgekehrten Fehler, und der ist ein rechtliches Problem.
Die allgemeine GA4-Einrichtung, das Tag in WordPress und die Standardwerte der Einwilligung stehen in meiner Anleitung zu GA4 in WordPress. Serverseitiger Tag Manager, der ein anderes Problem löst (Erstanbieter-Erfassung und Daten für Werbeplattformen), steht im Beitrag zu Data Strength in Google Ads und sGTM. Bestellungen, die KI-Agenten ohne Browser aufgeben, sind ein eigener Fall mit einer eigenen Frage zur Client-ID, behandelt in WooCommerce-Analytics beim Agenten-Checkout. Wer zusätzlich einen Shopify-Shop betreibt, findet die gleiche Art Lücke für diese Plattform in Shopify zeigt Checkouts, aber keine Bestellungen.
Was verlangt das Recht in Deutschland, Österreich und der Schweiz für das Analyse-Cookie?
Deutschland. § 25 TDDDG (Telekommunikation-Digitale-Dienste-Datenschutz-Gesetz, früher TTDSG) erlaubt das Speichern von Informationen in der Endeinrichtung und den Zugriff darauf nur, wenn der Endnutzer auf Grundlage klarer und umfassender Informationen eingewilligt hat. Information und Einwilligung richten sich nach der Datenschutz-Grundverordnung. Ausgenommen sind nach Absatz 2 nur die Übertragung einer Nachricht über ein öffentliches Telekommunikationsnetz und Zugriffe, die unbedingt erforderlich sind, damit ein ausdrücklich gewünschter digitaler Dienst bereitgestellt werden kann. Ein Analyse-Cookie für GA4 fällt nicht darunter.
Österreich. § 165 Abs. 3 TKG 2021 verpflichtet Anbieter eines Dienstes der Informationsgesellschaft, über die verarbeiteten Daten, Rechtsgrundlage, Zwecke und Speicherdauer zu informieren. Der Text sagt dazu wörtlich:
„Eine Ermittlung dieser Daten ist nur zulässig, wenn der Nutzer oder Benutzer seine Einwilligung dazu aktiv und auf Grundlage von klaren und umfassenden Informationen erteilt hat.“
Die Ausnahmen sind dieselben zwei wie in Deutschland: Übertragung einer Nachricht und das unbedingt Erforderliche für einen ausdrücklich gewünschten Dienst.
Schweiz. Hier ist die Regel anders gebaut. Art. 45c FMG (Fernmeldegesetz, Fassung vom 1. Juni 2026 gelesen) erlaubt das Bearbeiten von Daten auf fremden Geräten durch fernmeldetechnische Übertragung für die Fernmeldedienste und ihre Abrechnung oder dann, wenn die Benutzerinnen und Benutzer über die Bearbeitung und ihren Zweck informiert und darauf hingewiesen werden, dass sie die Bearbeitung ablehnen können. Das FMG setzt also auf Information und Widerspruch; eine vorherige Einwilligung wie in § 25 TDDDG verlangt dieser Artikel nicht. Liefert ein Schweizer Shop auch nach Deutschland oder Österreich, gelten für diese Besucher allerdings die dortigen Regeln, und die meisten Shops spielen deshalb überall dasselbe Banner aus. Weitere Anforderungen aus dem Schweizer Datenschutzgesetz habe ich für diesen Beitrag nicht geprüft und lasse sie hier weg.
Für diesen Beitrag ist die praktische Folge in allen drei Ländern dieselbe: Der serverseitige Kauf oben wird nur für Bestellungen gesendet, die beim Checkout ein Analyse-Cookie trugen. Wo der Besucher abgelehnt hat oder (in der Schweiz) widersprochen hat, gibt es kein Cookie und damit keinen Versand. Ob Ihre konkrete GA4-Konfiguration samt Datenübermittlung an Google den Anforderungen genügt, klären Sie mit Ihrer Datenschutzberatung; das nehme ich nicht vorweg. Die Aussage aus dem Search Engine Journal gilt auch hier: Serverseitiges Tracking ändert nichts an diesen Pflichten.
Was sollte ich vor dem Black Friday prüfen?
- Bestellungen und GA4-Transaktions-IDs für November und Dezember des Vorjahres exportieren, den Abgleich laufen lassen, die Erfassungsquote je Zahlungsanbieter und Gerät notieren.
- Dasselbe für die letzten vier Wochen. Ein Rückgang zwischen beiden ist ein Einrichtungsproblem, das Sie noch beheben können.
- Mit jedem angebotenen Zahlungsanbieter eine echte Bestellung auf dem Smartphone aufgeben, einmal den Tab auf der Zahlungsseite schließen und prüfen, ob die Bestellung bezahlt wird und was GA4 erfasst.
- Doppelte Transaktions-IDs suchen und das zweite Tag finden, falls es eines gibt.
- Sicherstellen, dass ausstehende und wartende Bestellungen (etwa Vorkasse) nicht als Käufe in GA4 landen, oder es bewusst hinnehmen und dokumentieren.
- Das Tariflimit der Consent-Plattform gegen den Traffic des letzten Dezembers prüfen und die Warnmails an die Person schicken lassen, die ein Upgrade bezahlen kann.
- Bestätigen, dass das CDN die Bestellbestätigungsseite nicht zwischenspeichert.
- Den serverseitigen Kauf, falls Sie ihn einbauen, auf
/debug/mp/collectvalidieren und dann über die Hauptsaison in eine separate Property senden. - Eine wöchentliche Prüfung in den Kalender setzen: GA4-Käufe geteilt durch WooCommerce-Web-Bestellungen. Der Audit zeigt, dass die Ausfälle erst Wochen später bemerkt wurden.
Wann lohnt es sich, einen Entwickler hinzuzuziehen?
Export und Abgleich sind ein Nachmittag für alle, die mit WP-CLI umgehen können. Der serverseitige Kauf berührt Kasse, Zahlungsbenachrichtigungen und Einwilligung, drei Stellen, an denen ein Fehler Geld kostet oder ein rechtliches Problem schafft, und er muss mit jedem Zahlungsanbieter getestet werden, den Sie nutzen. Wenn Sie das lieber vor der Hauptsaison auf Staging umgesetzt und getestet haben möchten: Solche Arbeiten übernehme ich als WooCommerce-Entwickler.





