Blog
E-Commerce-Ereignisse ohne Shop-Plattform: So meldet ein eigener Checkout an GA4
Veröffentlicht · 5 Min. Lesezeit
Eine Shop-Plattform gibt Ihnen wenigstens ein Plugin, mit dem Sie sich herumärgern können. Ein selbst gebauter Checkout – eine React-App, ein Buchungsablauf, eine SaaS-Registrierung mit Stripe dahinter – gibt Ihnen gar nichts: Kein Ereignis feuert, bevor Ihr eigener Code es pusht. Die gute Nachricht: Der Vertrag ist klein – eine Handvoll Namen, eine Objektform und ein paar Regeln dazu, wohin jeder Push gehört. Hier steht alles, einschließlich des Teils, den die meisten Anleitungen weglassen: der Käufe, die passieren, während niemand auf Ihrer Seite ist.
Der Vertrag: ein Name und ein ecommerce-Objekt
Alles hier erreicht Analytics auf demselben Weg. Ihre Seite pusht ein Ereignis unter seinem GA4-Namen in window.dataLayer, mit einem ecommerce-Objekt in der GA4-Form. Ein Tag-Manager-Trigger erkennt den Namen, und ein GA4-Ereignis-Tag liest das Objekt und sendet es weiter an Ihren Server-Container. Wenn Sie SignalHost gesagt haben, dass Ihre Website ein Shop ist – oder auf der Container-Seite unter „Conversion-Events“ auf „Einrichten“ geklickt haben –, gibt es diese Trigger und Tags in Ihrem Web-Container bereits, benannt mit dem Präfix SH. Was bleibt, ist der Push.
Zwei Regeln fangen die meisten Fehler ab. Leeren Sie zuerst das vorherige ecommerce-Objekt, sonst fährt ein view_item von vor drei Seiten im nächsten Ereignis mit. Und senden Sie bei jedem Ereignis, an dem Geld hängt, value als Zahl und currency als ISO-Code: Ein value als String oder eine fehlende currency wird klaglos akzeptiert und stillschweigend beim Umsatz übergangen.
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 }
]
}
});Welches Ereignis, und wohin es gehört
Ein eigener Ablauf hat dieselben Momente wie eine Shop-Plattform. Sie müssen sie nur in Ihrem eigenen Code finden.
- view_item_list – eine Seite oder Komponente, die mehrere Produkte oder Tarife zeigt. Einmal, wenn die Liste angezeigt wird, nicht bei jedem Scrollen.
- view_item – die Seite eines einzelnen Produkts oder Tarifs.
- add_to_cart und remove_from_cart – im Button-Handler, nachdem sich Ihr Warenkorb tatsächlich geändert hat.
- begin_checkout – der Moment, in dem der Besucher den Checkout betritt: das erste Rendern der Checkout-Seite oder der Klick, der einen Checkout-Dialog öffnet.
- add_payment_info – wenn Zahlungsdaten akzeptiert werden, nicht wenn das Formular erscheint.
- purchase – einmal, wenn die Bestellung bestätigt ist. Der größte Teil dieses Artikels handelt davon, genau dieses Ereignis richtig hinzubekommen.
- sign_up und start_trial – bei einem Abo-Geschäft die Conversions, bevor überhaupt Geld fließt. SignalHost baut Tags für beide, neben denen für die Shop-Ereignisse.
Pushen Sie aus dem Handler, der die Änderung ausgelöst hat, nicht aus einem Rendern. Eine Single-Page-App rendert nach Belieben neu, und React führt Effekte in der Entwicklung zweimal aus: Ein Push in einem Effekt verdoppelt sich im Dev-Build und wiederholt sich bei jedem erneuten Mounten einer Komponente.
Der Kauf auf Ihrer Bestätigungsseite: die Bestellung von Ihrem Server lesen
Wenn der Besucher nach dem Bezahlen auf eine Ihrer Seiten zurückkommt, gehört purchase auf diese Seite – unter zwei Bedingungen. Nehmen Sie die Bestellung aus Ihrem eigenen Backend, nicht aus der URL: Query-Parameter lassen sich bearbeiten, gehen bei Weiterleitungen verloren und kommen auch bei fehlgeschlagenen Zahlungen an. Stripe etwa leitet an dieselbe Adresse zurück, ob die Zahlung durchging oder nicht, und teilt in einem eigenen Parameter mit, was davon zutrifft. Und pushen Sie ihn nur einmal: Ein Neuladen, der Zurück-Button oder eine als Lesezeichen gespeicherte Belegseite – alle landen wieder dort.
// 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 ist ein Sicherheitsnetz, kein Plan. Analytics verwirft eine wiederholte transaction_id innerhalb einer Sitzung, aber nicht ein Neuladen der Belegseite am nächsten Morgen.
Zahlung auf einer fremden Seite: Stripe Checkout, PayPal, eine Weiterleitung zur Bank
Eine gehostete Zahlungsseite ist die übliche Wahl bei einem Eigenbau, und sie hat eine scharfe Kante: Der Besucher kommt womöglich nie zurück. Er bezahlt, schließt den Tab, und Ihre Bestätigungsseite wird nie gerendert. Bei Kartenzahlungen sind das ein paar Prozent der Käufe. Bei Überweisungen, Rechnungen und „Später bezahlen“-Verfahren kann es die Mehrheit sein, weil das Geld erst Stunden oder Tage später ankommt.
Entscheiden Sie also pro Zahlungsart, wo die Wahrheit liegt. Erfährt Ihr Backend über einen Webhook von der Zahlung, ist der Webhook der verlässliche Ort, sie zu melden – siehe nächster Abschnitt –, und die Bestätigungsseite darf purchase nicht zusätzlich pushen, sonst wird jede Kartenzahlung doppelt gezählt. Erfahren Sie davon nur auf der Rückkehrseite, melden Sie sie dort und nehmen Sie die Lücke in Kauf.
Käufe, bei denen kein Browser dabei ist: Senden Sie sie von Ihrem Server
Abonnements machen das unvermeidlich. Eine Testphase wird nach sieben Tagen zum Abo, eine Verlängerung passiert jeden Monat, eine Rechnung wird nächste Woche per Überweisung bezahlt. Jedes davon ist ein Kauf, und keiner passiert vor einem Browser. Die Lösung: Senden Sie das Ereignis Server-zu-Server, aus dem Webhook, der von der Zahlung erfährt, an denselben Tagging-Endpunkt, den Ihre Seiten nutzen – Ihren SignalHost-Hostnamen oder Ihre eigene Domain –, sodass es wie jeder andere Treffer durch Ihren Server-Container läuft.
Dafür braucht es eine Sache aus dem Checkout: die Analytics-Client-ID des Besuchers, damit der Kauf bei der Person landet, deren Besuche zu ihm geführt haben. Sie steht im _ga-Cookie, das Ihre Checkout-Anfrage bereits mitschickt, wenn sie an Ihre eigene Domain geht. Speichern Sie sie mit der Bestellung. Kein _ga-Cookie bedeutet, dass der Besucher keine Analytics-Einwilligung erteilt hat: Senden Sie den Treffer mit verweigerter Einwilligung und einer Wegwerf-ID oder gar nicht – erfinden Sie nie eine Identität für ihn.
// 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' });Wählen Sie pro Ereignis eine Quelle und bleiben Sie dabei. Genau so misst signalhost.io seine eigenen Verkäufe: Der Browser pusht sign_up, begin_checkout und start_trial; der Stripe-Webhook sendet purchase, wenn die erste Rechnung bezahlt ist; nichts sendet beides. Googles Measurement Protocol würde ebenfalls funktionieren, aber es geht direkt an Google und umgeht Ihren Server-Container, sodass keines Ihrer serverseitigen Tags den Verkauf je zu sehen bekommt.
Die Einwilligung verlangt Ihnen nichts Zusätzliches ab
Pushen Sie die Ereignisse, egal was der Besucher gewählt hat. Die Tags befolgen den Consent Mode: Mit Einwilligung senden sie normal, ohne sie senden sie cookielose Pings, die Analytics modelliert, statt sie auszuweisen. Halten Sie die Pushes selbst zurück, verbergen Sie Conversions nur vor dieser Modellierung. Worauf es dagegen ankommt, ist die Reihenfolge – das Tag-Manager-Snippet steht nach dem Skript Ihres Consent-Banners, damit die Standardwerte für die Einwilligung existieren, bevor das erste Ereignis gesendet wird.
Prüfen, ob es funktioniert
- Öffnen Sie den Container in SignalHost. Die Karte „Events“ listet nach Namen, was angekommen ist, mit dem Anteil, der eine Einwilligung trug. Ein Name, den Sie gepusht haben und dort nicht sehen, hat Ihren Tagging-Server nie erreicht.
- Gehen Sie Ihren Checkout im Vorschaumodus von Tag Manager durch. Er zeigt jeden Push, welches Tag gefeuert hat und das ecommerce-Objekt, das gesendet wurde.
- In Analytics zeigt DebugView Ereignisse, sobald sie ankommen. Standardberichte können einen Tag dauern.
- Laden Sie die Bestätigungsseite zweimal neu und zählen Sie die Käufe. Es sollte immer noch einer sein.
Die Fehler hier sind alle leise: ein Name in der falschen Groß- und Kleinschreibung, value als String gesendet, items eine Ebene zu tief verschachtelt. Keiner davon erzeugt irgendwo eine Fehlermeldung. Jeder davon erzeugt einen Bericht mit einem Loch darin.
