WooCommerce-panelet viser ett tall, GA4 viser et lavere, og hver november spør noen hvilket som stemmer. WooCommerce har rett om ordrene. GA4 har rett om det som faktisk kom fram til GA4. Det nyttige spørsmålet er hvilke ordrer som forsvant og hvorfor, og det svarer du på med to eksporter og en kobling på transaksjons-ID.
Her får du fremgangsmåten, koden som trengs, og et kjøp sendt fra serveren som du kan legge inn før de travleste ukene. Alt jeg skriver om GA4 og WooCommerce kommer fra dokumentasjonen eller kildekoden til Google og WooCommerce, lest 30. oktober 2026. Delen om betaling bygger på dokumentasjonen til Vipps MobilePay og Stripe, og lovdelen på ekomloven slik den står på Lovdata.
Hva fant revisjonen i Search Engine Journal egentlig?
Search Engine Journal publiserte 28. oktober 2026 en revisjon av Christian Goodrich, Head of Organic Search i byrået SOZO. Han sammenlignet GA4 med butikkenes egne ordredata hos åtte britiske nettbutikker; seks av dem lot ham koble tallene. Han opplyser selv at byrået hans har jobbet med alle åtte. Butikkene er anonyme, og artikkelen sier ikke hvilken plattform de bruker, så dette er ikke et funn om WooCommerce som sådan. Mønsteret er likevel det samme jeg ville sett etter i en norsk WooCommerce-butikk.
«66 %» fra tittelen står ikke i selve teksten. For butikken det gjelder, er tallene disse:
«GA4 registrerte 81 % av butikkens nettordrer i fjerde kvartal 2024, 41 % i fjerde kvartal 2025 og bare 32 % i juleperioden.»
Search Engine Journal, 28. oktober 2026, egen oversettelse
32 % registrert betyr 68 % borte i julen, som tittelen runder av til to tredjedeler. I samme kvartal økte butikkens nettordrer fra 5 874 til 7 686, mens GA4 viste et fall på omtrent en tredjedel. Fordeler du markedsbudsjettet etter GA4 i en slik situasjon, kutter du i kanalene som faktisk selger mer.
Årsakene han peker på, i nær gjengivelse:
- Et jevnt samtykkegap. En del av kundene avslår sporing hver uke, så GA4 ser aldri alle ordrene.
- Et samtykkebanner som slo seg av selv. Samtykkeplattformer solgt med en månedlig kvote av sidevisninger gikk tomme i desember, og banneret forsvant fram til neste fakturadato.
- Et brudd med sen etterfylling. Én butikk registrerte 2 215 ordrer på fem uker mens GA4 registrerte null; rundt 1 000 ble lastet inn senere, nesten alle som «Unassigned».
- Dupliserte kjøp. I én butikk hadde 657 av 3 365 ordrenumre mer enn én kjøpshendelse, til sammen 735 ekstra kjøp. I kvartalssummen skjulte duplikatene en del av tapet.
- Roboter. Roboter som kjørte nettleserlignende kode, ble talt som besøkende.
Én setning i artikkelen styrer resten av dette innlegget: sporing fra serveren endrer ikke pliktene knyttet til samtykke, så det jevne gapet blir værende. Det er jeg enig i, og koden lenger ned er bygget rundt nettopp den regelen. I Norge følger det dessuten direkte av ekomloven, som du finner omtalt mot slutten.
Det artikkelen ikke tar opp, er betalingsleverandører og ordrebekreftelsessiden. I en WooCommerce-butikk er det der jeg ser først, av grunnen i neste avsnitt.
Hvorfor mister WooCommerce kjøp i GA4?
Den gratis utvidelsen Google Analytics for WooCommerce er standardoppsettet i mange butikker. Dokumentasjonen sier rett ut hva den er avhengig av:
«Kjøpstransaksjoner: Spor kjøp på nettstedet ditt. Dette krever en betalingsleverandør som sender kjøperen videre til takkesiden eller siden for mottatt ordre etter betaling.»
WooCommerce-dokumentasjonen, Google Analytics for WooCommerce, egen oversettelse
Kjøpshendelsen sendes av kjøperens nettleser på ordrebekreftelsessiden. Alt som hindrer siden i å laste, eller taggen på den i å kjøre, fjerner kjøpet fra GA4 mens WooCommerce fortsatt registrerer ordren. De vanlige grunnene:
- Samtykke avslått. Den samme dokumentasjonen sier at besøkende i EØS, Storbritannia og Sveits må gi samtykke før sporing, og at Google Analytics og Google Ads ikke fanger opp brukerdata der uten samtykke. Norge er med i EØS og dermed i denne gruppen. Utvidelsen har ikke eget banner; den leser samtykket gjennom WP Consent API fra en separat samtykkeutvidelse.
- Annonseblokkere og skriptfeil. WooCommerce lister selv opp nettleserutvidelser, annonseblokkere, JavaScript-feil og flere sporingsinstanser i feilsøkingen.
- Kjøperen kommer aldri tilbake fra betalingen. Betalingssider og betalingsapper sender kjøperen tilbake med en omdirigering. Lukker kjøperen fanen etter betalingen, eller gir appen aldri kontrollen tilbake til nettleseren, blir ordren likevel betalt gjennom leverandørens servervarsel, og kjøpet i nettleseren blir aldri sendt. Med en appbasert betaling som Vipps MobilePay er dette særlig aktuelt, se avsnittet om betalingsleverandører.
- Et hurtigbufferlag foran ordrebekreftelsessiden. WooCommerce sender selv no-cache-headere og setter
DONOTCACHEPAGEpå handlekurv, kasse og kontosider, og ordrebekreftelsen er et endepunkt under kassen (WC_Cache_Helper::prevent_cachingi WooCommerce-kjernen). Risikoen er en CDN- eller proxyregel som bufrer HTML uansett hva headerne sier. - Feil eller gjentatte transaksjons-ID-er. Googles hjelpeside om duplikatfjerning med transaksjons-ID advarer om at alle kjøp med tom transaksjons-ID blir slått sammen, og at gjenbrukte ID-er gir for lav telling.
Utvidelsen kan også telle for mye. I kildekoden på GitHub (trunk, versjonshode 2.4.4, lest 30. oktober 2026) forberedes kjøpet på kroken woocommerce_thankyou for alle ordrer som verken har feilet eller er kansellert, og der ordrenøkkelen i URL-en er gyldig. En ordre på bankoverføring i status «På vent», eller en ventende ordre der kjøperen så takkesiden, sendes som kjøp enten den blir betalt eller ikke. WooCommerce sin dokumentasjon av ordrestatuser beskriver «På vent» som en ordre som venter på betalingsbekreftelse, og «Venter på betaling» som en ordre der ingen betaling er gjort. Avstem derfor bare mot betalte statuser.
Den samme kildekoden gir deg en gratis diagnose. Når ordrebekreftelsessiden vises med gyldig nøkkel, skriver utvidelsen metafeltet _ga_tracked = 1 på ordren på serveren, før nettleseren har sendt noe. Det deler de manglende ordrene i to grupper:
- Ingen
_ga_trackedpå en betalt ordre: ordrebekreftelsessiden ble aldri vist. Se på tilbakesendingen fra betalingsleverandøren. _ga_trackedfinnes, men kjøpet mangler i GA4: siden ble vist, og nettleseren mistet hendelsen. Se på samtykke, blokkere og skriptfeil.
Utvidelsen sender transaction_id som $order->get_order_number() som standard (filteret woocommerce_ga_order_id). Bruk den samme verdien i alle stegene under. Ellers feiler koblingen så snart en utvidelse for egne ordrenummerserier er i bruk.
Hvordan eksporterer jeg WooCommerce-ordrer til sammenligningen?
Ta bare nettordrer i betalte statuser for en avsluttet periode. Fremgangsmåten i Search Engine Journal utelater markedsplass-, abonnements- og manuelle ordrer, og i WooCommerce gjør du det med created_via: den klassiske kassen setter checkout, og blokkassen (Store API) setter store-api, ifølge kildekoden i WooCommerce-kjernen.
Skriptet virker både med HPOS og med den gamle lagringen i innlegg, fordi det går gjennom wc_get_orders(). Kjør det med WP-CLI på staging eller i produksjon (det leser bare):
<?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 );Tre kolonner trenger en forklaring. device kommer fra ordreattribusjonen i WooCommerce, som siden versjon 8.5 lagrer _wc_order_attribution_device_type som Mobile, Tablet eller Desktop, utledet fra brukeragenten. thankyou_seen er feltet _ga_tracked fra utvidelsen, beskrevet over. has_ga_cookie står på «no» til du installerer serverkoden lenger ned; etter det viser den hvilke ordrer som hadde en analysekapsel da kjøperen gikk til kassen.
Refunderte ordrer blir med, fordi de ble betalt og GA4 burde ha et kjøp for dem. Foretrekker du SQL i en butikk med HPOS, ser det samme filteret slik ut (tilpass prefikset 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-spørringen returnerer den interne id. Endrer en utvidelse ordrenumrene, må du knytte ID-en til nummeret før koblingen, eller bruke PHP-eksporten.
Hvordan eksporterer jeg GA4-kjøp etter transaksjons-ID?
Bruk dimensjonen Transaksjons-ID med metrikken E-handelskjøp. I skjemaet for GA4 Data API teller ecommercePurchases kjøpshendelser, mens transactions teller flere hendelsestyper, blant dem refund. Search Engine Journal advarer mot metrikken Transaksjoner av samme grunn: den teller refusjoner sammen med kjøp.
I grensesnittet lager du en utforskning i fritt format med Transaksjons-ID som rader og E-handelskjøp som verdi, setter samme periode som i ordreeksporten og eksporterer til CSV. Gjennom Data API heter dimensjonen transactionId og metrikken ecommercePurchases. Legg til deviceCategory eller browser i en egen eksport hvis du vil ha GA4 sin egen oppdeling. Står GA4-grensesnittet på norsk, får kolonnene i CSV-filen norske navn; gi dem navnene «Transaction ID» og «Ecommerce purchases» før du kjører skriptet, eller sett språket til engelsk under eksporten. To ting bør du sjekke før du stoler på filen:
- Dataterskler. Googles hjelpeside om dataterskler sier at de settes av systemet, ikke kan justeres og kan holde tilbake rader når en rapport inneholder demografiske data eller Google-signaler. Hold utforskningen til transaksjons-ID og antall kjøp.
- Modellerte data. Atferdsmodellering for samtykkemodus vises bare under rapporteringsidentiteten «Blandet» og krever minst 1 000 hendelser per dag med
analytics_storage='denied'i 7 dager, pluss 1 000 samtykkende brukere per dag på 7 av de siste 28 dagene. Modellerte brukere har ingen transaksjons-ID-er fra ekte ordrer, så denne avstemmingen handler om observerte data.
Hvordan regner jeg ut gapet per betalingsleverandør og enhet?
Dette skriptet kobler de to filene og skriver ut fangstraten totalt og per betalingsmåte, per enhet og per verdi av thankyou_seen. Det teller også transaksjons-ID-er med mer enn én kjøpshendelse, altså duplikatsjekken fra revisjonen, og GA4-ID-er som ikke finnes blant nettordrene. Python 3, bare standardbiblioteket:
#!/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])Kjør det én gang for november og desember i fjor og én gang for de siste fire ukene. Én prosentsats forteller lite; det er oppdelingen som gir svaret. Kolonnen payment_method inneholder den interne ID-en betalingsutvidelsen setter, for eksempel en verdi med «vipps» eller «stripe» i navnet, så du ser med en gang hvilken betalingsmåte som skiller seg ut.
Hvordan leser jeg resultatet?
| Hva du ser | Mest sannsynlig årsak | Hvor du ser |
|---|---|---|
| Én betalingsleverandør langt under de andre | Kjøperne kommer ikke tilbake fra den leverandørens side eller app | Retur-URL, ordrebekreftelsen på mobil, thankyou_seen for den leverandøren |
thankyou_seen = no på mange betalte ordrer | Ordrebekreftelsessiden ble ikke vist | Omdirigeringer fra leverandøren, kjøpere som lukker fanen, feil i utvidelser på siden |
thankyou_seen = yes, men lav fangst | Nettleseren mistet hendelsen | Samtykkeandel, annonseblokkere, konsollfeil, standardverdien for samtykke i utvidelsen |
| Mobil langt under desktop | Bytte til betalingsapp, retur i en annen nettleser enn den kjøpet startet i | Testkjøp på telefon med hver leverandør |
| Fin fangst i oktober, fall i desember | Noe brøt sammen under last, eller en kvote gikk tom | Kvoten i samtykkeplattformen, utrullinger den uken, CDN-regler |
| Mange ID-er med mer enn ett kjøp | Ordrebekreftelsen lastet på nytt, eller to tagger | Doble tagger i GTM og utvidelser, oppførsel ved ny innlasting |
| GA4-ID-er uten tilsvarende nettordre | Ventende ordrer eller ordrer på vent talt med, eller testordrer | Status for disse ID-ene i WooCommerce |
Et jevnt gap som ikke flytter seg med leverandør eller enhet, er mest sannsynlig samtykke. Det gapet skal bli værende. Det er de besøkende som sa nei, og GA4 forteller sannheten om dem ved ikke å telle dem. Hvor stort det er i din butikk, står i statistikken til samtykkeverktøyet ditt.
Hva gjør Vipps MobilePay og Stripe etter betalingen?
Dokumentasjonen til betalingsleverandørene viser hvorfor nettleseren ikke kan være eneste vitne. To leverandører jeg leste dokumentasjonen til for dette innlegget, Vipps MobilePay fordi nesten alle norske kunder kjenner den, og Stripe fordi dokumentasjonen sier det tydeligst:
Vipps MobilePay skriver i hurtigstarten for ePayment API at brukeren sendes til den angitte returnUrl etter en «best effort»-policy. Tilbakesendingen er altså ikke garantert. Resultatet henter du ved å spørre etter betalingens status med GET /epayment/v1/payments/{reference}, og sjekklisten for ePayment API ber deg bruke både webhooks og polling, med polling som reserve hvis webhooks blir forsinket. Den samme sjekklisten sier at returnUrl på mobil åpnes i telefonens standardnettleser, og at løsningen ikke skal være avhengig av en bestemt nettleser, fane, innebygd nettleser eller sesjonstoken.
Det siste punktet er viktig for GA4. Starter kjøperen kjøpet i nettleseren inne i en sosial app, eller i en annen nettleser enn standarden, åpnes ordrebekreftelsen etter Vipps-betalingen i en nettleser som ikke har den samme _ga-kapselen. Slik jeg leser det, kan da to ting skje: kjøpet havner på en ny klient-ID uten kilde, eller samtykkebanneret dukker opp på nytt og kjøpet sendes ikke i det hele tatt. Begge deler ser du som lav mobilfangst for Vipps i skriptet over.
Stripe er enda tydeligere i veiledningen om ordrehåndtering med Checkout:
«Du kan ikke stole på å utløse ordrehåndteringen bare fra landingssiden for kassen, fordi det ikke er garantert at kundene besøker den siden. En kunde kan for eksempel betale og så miste internettforbindelsen før landingssiden din lastes.»
Stripe-dokumentasjonen, Fulfill orders, egen oversettelse
Stripe beskriver også betalingsmåter med forsinket bekreftelse, som direktedebitering og andre bankoverføringer. Når slike betalinger går gjennom senere, kommer det som en egen hendelse, checkout.session.async_payment_succeeded. For avstemmingen betyr det at et kjøp i nettleseren kan være talt lenge før betalingen faktisk er bekreftet, eller avvist.
Mønsteret er det samme hos alle leverandører med betalingsside eller betalingsapp. Servervarselet er det pålitelige signalet om betaling, og omdirigeringen er en tjeneste for kjøperen. Betalingsutvidelser for WooCommerce gjør varselet om til en betalt status, og det er den hendelsen GA4-kjøpet bør henge på.
Hvordan sender jeg kjøpet fra serveren med Measurement Protocol?
GA4 Measurement Protocol tar imot hendelser over HTTP fra serveren din. Google er klar på hvilken rolle det har:
«Hensikten med Measurement Protocol er å utfylle automatisk innsamling gjennom gtag, Tag Manager og Google Analytics for Firebase, ikke å erstatte den.»
Google Analytics, oversikt over Measurement Protocol, egen oversettelse
Behold derfor taggen i nettleseren for atferd, og bruk serveren bare til kjøpet. Oppsettet under følger dokumentasjonen punkt for punkt:
- client_id. Referansen godtar en klient-ID-kapsel med full verdi. Koden lagrer verdien av
_ga-kapselen på ordren i kassen, uten å dele den opp. - session_id. Den eneste dokumenterte måten å lese den på er
gtag('get', '<target>', 'session_id', callback)fra gtag.js-referansen. Ifølge oversikten får hendelsen sesjonens geografi og enhet hvis du tar medsession_idog sender den innen 24 timer etter at sesjonen startet. Koden leser den på kassesiden og tar vare på den i en time. - Samtykke. Objektet
consenti forespørselen dekker baread_user_dataogad_personalization. Mangler det, bruker Google Analytics ifølge referansen samtykkeinnstillingene fra de tilsvarende nettinteraksjonene for klienten. Koden finner ikke på noe: ingen_ga-kapsel i kassen betyr ingen klient-ID, og den ordren sendes ikke. - Feil. Measurement Protocol svarer 2xx også på feil nyttelast. Får du et svar utenfor 2xx, sier Google at du skal rette forespørselen og ikke sende den samme på nytt. Valider først mot
/debug/mp/collect, som returnerervalidationMessagesog ikke lagrer noe. - Tidspunkt. Hendelser kan tilbakedateres opptil 72 timer. Et kjøp som sendes når betalingen bekreftes, trenger ikke tidsstempel, så vinduet betyr noe bare hvis du laster inn etterslep i ettertid.
- Innsamling i EU. Googles veiledning for sending sier at du bruker
region1.google-analytics.comsom vert hvis du vil ha innsamling i EU. Norge er ikke EU-medlem, men er en del av EØS og personvernforordningen gjelder her, så jeg bruker den samme verten for norske butikker.
Lagre koden som en must-use-utvidelse (wp-content/mu-plugins/ga4-mp-purchase.php), og legg målings-ID og API-hemmelighet i wp-config.php. API-hemmeligheten oppretter du i innstillingene for nettdatastrømmen i 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 );Hvorfor disse krokene: woocommerce_payment_complete utløses når en betalingsutvidelse kaller WC_Order::payment_complete(), vanligvis fra håndteringen av servervarselet, med ordre-ID og leverandørens transaksjons-ID. Bankoverføring og betaling ved levering kaller den aldri; de ordrene når en betalt status når du eller leverandøren endrer den, og det fanger woocommerce_order_status_processing og woocommerce_order_status_completed opp. Flagget _wpp_ga_mp_sent hindrer at andre og tredje kall sender på nytt.
Refusjoner sendes på woocommerce_order_refunded, som også utløses ved delvise refusjoner. Googles e-handelsveiledning ber om transaction_id og de refunderte varene med item_id og quantity, så koden tar med de refunderte linjene når det finnes noen. Med angrerett og returer etter jul blir dette fort hverdag. Utvidelsen Google Analytics for WooCommerce sender ingen refusjonshendelse i det hele tatt (kildekoden har ingen håndtering av refusjoner), som er enda en grunn til at metrikken Transaksjoner og ordrelisten din glir fra hverandre.
Krokene som lagrer ID-ene, dekker begge kassene: woocommerce_checkout_order_created i den klassiske kortkodekassen og woocommerce_store_api_checkout_order_processed i blokkassen. Begge kjøres i kjøperens egen forespørsel, så kapslene er der. Det er også grunnen til at Vipps-problemet over ikke rammer serverkjøpet: ID-ene lagres før kjøperen forlater butikken, og kjøpet sendes når servervarselet kommer, uansett hvilken nettleser som åpner ordrebekreftelsen.
value regnes ut slik Googles e-handelseksempel kommenterer det, som summen av pris ganger antall for alle varer, med mva. og frakt i egne felt. Regner taggen i nettleseren verdien annerledes, for eksempel med mva. inkludert, må du få de to til å stemme før du sammenligner omsetning.
Ett punkt til fra Google: Measurement Protocol har ifølge Google nådd en moden og ferdig tilstand og skal fortsette å virke uten planer om utfasing, og for nye integrasjoner mellom servere anbefaler Google nå Data Manager API. For kjøpshendelsen i én enkelt butikk er Measurement Protocol den minste jobben, og jeg ville holdt meg til den.
Hvordan unngår jeg å telle samme ordre to ganger?
Googles hjelpeside om duplikatfjerning beskriver at transaksjons-ID-er fjerner duplikater av kjøp fra samme bruker i nettdatastrømmer. Den sier ingenting om Measurement Protocol, så jeg stoler ikke på at GA4 slår sammen et kjøp fra nettleseren og et fra serveren for samme ordre. Velg en av de to dokumenterte veiene:
- Slå av kjøpet i nettleseren. I Google Analytics for WooCommerce fjerner du haken ved «Purchase Transactions». Resten av trakten (produktvisninger, legg i handlekurv, start på kassen) kommer fortsatt fra nettleseren. Du mister diagnosen via
_ga_trackedfor nye ordrer, så kjør avstemmingen én gang før du bytter. - Send serverkjøpet til en egen property. Dette er rådet i Search Engine Journal for alle som begynner nå: send det til en separat GA4-property gjennom fjerde kvartal, så ingenting telles to ganger. Sammenlign begge med ordreeksporten i januar og bestem deg da.
To uker før Black Friday er et dårlig tidspunkt å endre kjøpshendelsen som budstrategier og rapporter bygger på. Den andre veien lar dagens tall være i fred mens du samler bevis.
Hvilke innstillinger i samtykkemodus endrer tallene?
Google beskriver to varianter av samtykkemodus. I den grunnleggende varianten lastes ikke taggene før den besøkende har svart på banneret, og ingenting sendes før samtykke. I den avanserte varianten lastes taggene med avslåtte standardverdier og sender kapselløse pinger mens samtykket er avslått; det er dette oppsettet atferdsmodellering krever. Ingen av variantene gjør et avslag om til et kjøp med transaksjons-ID.
Dette bør du sjekke i en WooCommerce-butikk før høysesongen:
- Standardverdien utvidelsen setter. Google Analytics for WooCommerce setter standardverdier for samtykke ved oppstart og lar deg endre dem med filteret
woocommerce_ga_gtag_consent_modes. Setter samtykkeutvidelsen din egne standardverdier, dokumenterer WooCommerce dette filteret som måten å hindre at utvidelsen overskriver dem. - Oppdateringen av samtykket. Ifølge WooCommerce-dokumentasjonen må samtykkeintegrasjonen din oppdatere samtykkestatusen når den besøkende har gitt samtykke. Test at statusen skifter til «granted» på samme sidevisning og ikke først på neste, ellers går den første siden etter samtykket tapt.
- Kvoten i samtykkeplattformen. Banneret som slo seg av i desember i revisjonen, var en faktureringskvote. Sjekk planen din mot sidevisningene fra forrige desember. Fortsetter taggen å kjøre mens banneret er borte, har en norsk butikk i tillegg et problem med § 3-15.
- Hva som skjer etter «avvis». Innlegget mitt om et avvist cookie-banner der sporingen lastes likevel tar for seg den motsatte feilen, og den er et juridisk problem.
Det generelle GA4-oppsettet, taggen i WordPress og standardverdiene for samtykke finner du i veiledningen min om GA4 i WordPress. Tag Manager på server, som løser et annet problem (førstepartsinnsamling og data til annonseplattformer), står i innlegget om Data Strength i Google Ads og sGTM. Ordrer lagt inn av KI-agenter uten nettleser er en egen sak med sitt eget spørsmål om klient-ID, omtalt i WooCommerce-analyse ved agentkasse. Driver du også en Shopify-butikk, går Shopify viser kasser, men ingen bestillinger gjennom samme type gap på den plattformen.
Krever norsk lov samtykke til analysekapselen?
Ja. Regelen står i § 3-15 i lov om elektronisk kommunikasjon (ekomloven), LOV-2024-12-13-76, som ifølge Lovdata gjelder fra 1. januar 2025 og erstattet ekomloven fra 2003. Første ledd lyder:
«Det er ikke tillatt å lagre eller å skaffe seg tilgang til opplysninger i sluttbrukers eller brukers kommunikasjonsutstyr uten at den aktuelle sluttbrukeren eller brukeren er informert om blant annet hvilke opplysninger som behandles, formålet med behandlingen og hvem som behandler opplysningene, og uten at den aktuelle sluttbrukeren eller brukeren har gitt samtykke.»
Paragrafen sier videre at samtykket skal oppfylle kravene til samtykke i personvernforordningen. Unntakene i andre ledd gjelder bare teknisk lagring eller tilgang som utelukkende skal overføre kommunikasjon i et elektronisk kommunikasjonsnett, eller som er strengt nødvendig for å levere en tjeneste brukeren uttrykkelig har bedt om. En analysekapsel for GA4 er ingen av delene. Departementet kan gi forskrift om plikten og om unntakene; jeg har ikke gått gjennom forskrifter til paragrafen for dette innlegget.
Den praktiske følgen for dette innlegget er enkel: kjøpet fra serveren over sendes bare for ordrer som hadde en analysekapsel da kjøperen gikk til kassen. Har den besøkende sagt nei, finnes det ingen kapsel og ingen sending. Om akkurat ditt GA4-oppsett, med overføring av data til Google, oppfyller kravene, må du avklare i din egen personvernvurdering; det tar jeg ikke stilling til her. Setningen fra Search Engine Journal gjelder også i Norge: sporing fra serveren endrer ikke samtykkepliktene, og det jevne gapet blir værende.
Hva bør jeg sjekke før Black Friday?
- Eksporter ordrer og GA4-transaksjons-ID-er for november og desember i fjor, kjør avstemmingen og noter fangstraten per betalingsleverandør og enhet.
- Gjør det samme for de siste fire ukene. Et fall mellom de to er et oppsettproblem du fortsatt rekker å rette.
- Legg inn en ekte ordre på telefon med hver betalingsleverandør du tilbyr, også Vipps MobilePay fra nettleseren inne i en sosial app, lukk fanen på betalingssiden én gang, og sjekk at ordren blir betalt og hva GA4 registrerer.
- Se etter dupliserte transaksjons-ID-er, og finn den andre taggen hvis det finnes en.
- Bekreft at ventende ordrer og ordrer på vent (for eksempel bankoverføring) ikke havner i GA4 som kjøp, eller godta det og dokumenter det.
- Sjekk kvoten i samtykkeplattformen mot trafikken fra forrige desember, og send varslene derfra til den som kan betale for en oppgradering.
- Bekreft at CDN-et ikke bufrer ordrebekreftelsessiden.
- Legger du inn kjøpet fra serveren, valider det på
/debug/mp/collectog send det til en egen property gjennom høysesongen. - Sett en ukentlig sjekk i kalenderen: GA4-kjøp delt på WooCommerce-nettordrer. Revisjonen viser at bruddene ble oppdaget uker for sent.
Når lønner det seg å hente inn en utvikler?
Eksporten og avstemmingen er en ettermiddag for alle som er komfortable med WP-CLI. Kjøpet fra serveren berører kassen, betalingsvarsler og samtykke, tre steder der en feil koster penger eller skaper et juridisk problem, og det må testes med hver betalingsleverandør du bruker. Vil du heller ha det gjort og testet på staging før høysesongen, tar jeg denne typen oppdrag som WooCommerce-utvikler.





