El panel de WooCommerce dice una cifra, GA4 dice otra más baja, y cada noviembre alguien pregunta cuál es la buena. WooCommerce tiene razón sobre los pedidos. GA4 tiene razón sobre lo que llegó a GA4. La pregunta útil es qué pedidos se perdieron y por qué, y eso lo respondes con dos exportaciones y un cruce por el ID de transacción.
Aquí tienes ese procedimiento, el código para ejecutarlo y una compra enviada desde el servidor que puedes añadir antes de las semanas fuertes, que en España van del Black Friday hasta Reyes. En una tienda española hay un detalle que pesa más que en otros mercados: el TPV virtual de Redsys, con el que trabajan muchos bancos, devuelve al cliente a la tienda de una forma concreta, y eso afecta directamente a GA4. Todo lo que afirmo sobre GA4, WooCommerce y Redsys sale de su propia documentación o de su código fuente, leídos el 30 de octubre de 2026.
¿Qué encontró de verdad la auditoría de Search Engine Journal?
El 28 de octubre de 2026 Search Engine Journal publicó una auditoría de Christian Goodrich, responsable de búsqueda orgánica en la agencia SOZO. Comparó GA4 con los registros de pedidos de ocho marcas de comercio electrónico del Reino Unido; seis le dejaron cruzar los datos. Él mismo declara que su agencia ha trabajado con las ocho. Los minoristas son anónimos y el artículo no dice qué plataforma usan, así que nada de lo que cuenta es un hallazgo sobre WooCommerce como tal.
El “66%” del titular no aparece en el cuerpo. Las cifras del minorista en cuestión son estas:
“GA4 registró el 81% de los pedidos web de la tienda en el cuarto trimestre de 2024, el 41% en el cuarto trimestre de 2025 y solo el 32% en el periodo navideño.”
Search Engine Journal, 28 de octubre de 2026, traducción propia
Captar un 32% significa perder un 68% en Navidad, que el titular redondea a dos tercios. En ese mismo trimestre los pedidos web de la tienda subieron de 5.874 a 7.686, mientras GA4 mostraba una caída de alrededor de un tercio. Si hubieras repartido el presupuesto de enero mirando solo GA4, habrías recortado en un canal que estaba creciendo.
Las causas que señala, con sus palabras o en paráfrase cercana:
- Un hueco de consentimiento constante. Cada semana hay compradores que rechazan el seguimiento, así que GA4 nunca ve todos los pedidos.
- Un banner de consentimiento que se apagó solo. Algunas plataformas de consentimiento venden planes con un cupo mensual de páginas vistas; en diciembre se agotó y el banner dejó de mostrarse hasta la siguiente fecha de facturación.
- Un apagón seguido de una carga tardía. Una tienda registró 2.215 pedidos en cinco semanas mientras GA4 no registró ninguno; unos 1.000 se cargaron más tarde, casi todos como Unassigned.
- Compras duplicadas. En una tienda, 657 de 3.365 números de pedido tenían más de un evento purchase, lo que sumó 735 compras. En el total trimestral los duplicados taparon parte de la pérdida.
- Bots. Bots que ejecutan código parecido al de un navegador se contaron como visitantes.
Hay una frase suya que marca el resto de este artículo:
“El seguimiento del lado del servidor no cambia tus obligaciones de consentimiento, así que el hueco constante se mantiene.”
Search Engine Journal, 28 de octubre de 2026, traducción propia
Estoy de acuerdo, y el código de más abajo está construido alrededor de eso. Lo que el artículo no trata son las pasarelas de pago ni la página de pedido recibido. En una tienda WooCommerce es lo primero que miraría yo, por lo que cuento en la siguiente sección.
¿Por qué WooCommerce pierde compras en GA4?
La extensión gratuita Google Analytics for WooCommerce es la configuración por defecto en muchas tiendas. Su documentación deja clara la dependencia:
“Transacciones de compra: mide las compras en tu sitio. Requiere una pasarela de pago que redirija a la página de agradecimiento/pedido recibido después del pago.”
Documentación de WooCommerce, Google Analytics for WooCommerce, traducción propia
El evento purchase lo envía el navegador del comprador en la página de pedido recibido. Todo lo que impida que esa página cargue, o que la etiqueta se ejecute en ella, saca la compra de GA4 mientras WooCommerce sigue registrando el pedido. Los motivos habituales:
- Consentimiento rechazado. La misma documentación indica que en el EEE, el Reino Unido y Suiza los visitantes tienen que dar su consentimiento antes del seguimiento y que, sin él, GA4 y Google Ads no recogen datos del usuario en esas regiones. La extensión no trae banner propio; lee el consentimiento a través de la WP Consent API desde un plugin de consentimiento aparte.
- Bloqueadores de anuncios y errores de script. La lista de resolución de problemas de WooCommerce incluye extensiones del navegador, bloqueadores, errores de JavaScript y varias instancias de seguimiento a la vez.
- El comprador no vuelve de la página de pago. Las pasarelas con página de pago externa, como el TPV virtual de Redsys, devuelven al comprador con una redirección. Si cierra la pestaña después de pagar, o si la app del banco nunca devuelve el control al navegador, el pedido queda pagado por la notificación del servidor y la compra del navegador nunca se dispara.
- Una capa de caché delante de la página de pedido recibido. WooCommerce envía cabeceras sin caché y define
DONOTCACHEPAGEen el carrito, el pago y la cuenta, y la página de pedido recibido es un endpoint del pago (WC_Cache_Helper::prevent_cachingen el núcleo de WooCommerce). El riesgo está en una regla de CDN o de proxy que cachea HTML sin respetar esas cabeceras. - IDs de transacción erróneos o repetidos. La ayuda de Google sobre eliminación de duplicados por ID de transacción avisa de que, si el ID va vacío, todas esas compras se deduplican juntas, y de que reutilizar IDs hace contar de menos.
La extensión también puede contar de más. En su código fuente en GitHub (trunk, cabecera de versión 2.4.4, leído el 30 de octubre de 2026) la compra se prepara en el hook woocommerce_thankyou para cualquier pedido que no esté fallido ni cancelado y cuya clave de pedido en la URL sea válida. Un pedido por transferencia bancaria en espera, o un pedido pendiente cuyo comprador vio la página de agradecimiento, se envía como compra se llegue a pagar o no. La documentación de estados de pedido define “en espera” como un pedido que espera la confirmación del pago y “pendiente de pago” como un pedido recibido sin ningún pago. Por eso la conciliación se hace solo con estados pagados.
El mismo código te regala un diagnóstico. Cuando la página de pedido recibido se carga con una clave válida, la extensión escribe en el pedido el meta _ga_tracked = 1, en el servidor, antes de que el navegador envíe nada. Eso divide los pedidos que faltan en dos grupos:
- Pedido pagado sin
_ga_tracked: la página de pedido recibido nunca se cargó. Mira el retorno de la pasarela. _ga_trackedpresente, compra ausente en GA4: la página se cargó y el navegador perdió el evento. Mira el consentimiento, los bloqueadores y los errores de script.
La extensión envía transaction_id como $order->get_order_number() por defecto (filtro woocommerce_ga_order_id). Usa ese mismo valor en todo lo que viene a continuación; si no, los plugins de numeración secuencial de pedidos rompen el cruce.
¿Qué hacen Redsys y Bizum cuando el cliente paga?
Muchas tiendas españolas cobran con tarjeta a través de un TPV virtual bancario que funciona sobre Redsys. Su documentación de autorización separa dos direcciones que la tienda envía en cada petición de pago:
DS_MERCHANT_MERCHANTURLes la URL a la que se envía la notificación con el resultado de la operación. Es el servidor de tu tienda hablando con el de Redsys.DS_MERCHANT_URLOKyDS_MERCHANT_URLKOson las URL a las que se manda al cliente cuando termina la operación, una para el pago correcto y otra para el fallido.
La parte que importa para GA4 es el orden. Según la misma página, al terminar el proceso Redsys envía primero de todo una notificación HTTP POST a la URL que el comercio indicó, y posteriormente el cliente vuelve a la tienda de forma automática o cuando pulsa «continuar». La documentación añade que la notificación se envía a la MERCHANTURL se indiquen o no URL de OK y KO, y advierte que no se deben usar los datos que llegan por GET en la redirección para validar el pedido.
Traducido a WooCommerce: el plugin de Redsys marca el pedido como pagado con la notificación, antes de que el cliente vea nada. Si el cliente cierra la ventana en la pantalla de Redsys en lugar de pulsar «continuar», o si el retorno automático no llega a completarse en el móvil, tienes un pedido pagado, sin _ga_tracked y sin compra en GA4. En la conciliación lo verás como un payment_method de Redsys con una captación más baja que el resto.
Bizum entra por el mismo camino. La documentación de Redsys explica que para operar con Bizum en redirección se envía el parámetro Ds_Merchant_PayMethods con el valor z. El comprador confirma el pago fuera de la tienda, la notificación llega igual a la MERCHANTURL, y el regreso a la tienda depende otra vez de que el navegador del móvil retome la pestaña correcta. Si tu plugin ofrece Bizum como método aparte, aparecerá con su propio identificador en payment_method y podrás ver su captación por separado.
El patrón vale para cualquier pasarela con página externa: la notificación al servidor es la señal fiable del pago, y la redirección es una cortesía con el comprador. Los plugins de pasarela de WooCommerce convierten esa notificación en un estado pagado, y de ese evento es del que conviene colgar la compra de GA4.
¿Cómo exporto los pedidos de WooCommerce para la comparación?
Coge solo pedidos web, en estados pagados, en un rango de fechas cerrado. El procedimiento de Search Engine Journal excluye pedidos de marketplaces, de suscripciones y manuales, y WooCommerce te deja hacerlo con created_via: el pago clásico guarda checkout y el pago por bloques (Store API) guarda store-api, según el código del núcleo de WooCommerce. Si vendes también en Amazon o en algún marketplace español conectado a WooCommerce, este filtro es el que evita mezclar esos pedidos.
Este script funciona con HPOS y con el almacenamiento antiguo en posts porque pasa por wc_get_orders(). Ejecútalo con WP-CLI en staging o en producción (solo lee):
<?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 );Tres columnas necesitan explicación. device sale de la atribución de pedidos de WooCommerce, que desde la versión 8.5 guarda _wc_order_attribution_device_type como Mobile, Tablet o Desktop a partir del user agent. thankyou_seen es la marca _ga_tracked de la extensión, descrita arriba. has_ga_cookie se queda en “no” hasta que instales el código de servidor de más abajo; a partir de ahí te dice qué pedidos traían cookie de analítica en el pago.
Los pedidos reembolsados se quedan porque se pagaron, y GA4 debería tener una compra para ellos. Si prefieres SQL en una tienda con HPOS, el mismo filtro queda así (ajusta el prefijo 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');El SQL devuelve el id interno. Si un plugin cambia los números de pedido, conviértelo al número antes de cruzar, o usa la exportación en PHP.
¿Cómo exporto las compras de GA4 por ID de transacción?
Usa la dimensión ID de transacción con la métrica Compras de comercio electrónico. En el esquema de la Data API de GA4, ecommercePurchases cuenta eventos purchase, mientras que transactions cuenta varios tipos de evento, refund incluido. Search Engine Journal da el mismo aviso:
“Evita la métrica de transacciones, que cuenta los reembolsos además de las compras.”
Search Engine Journal, 28 de octubre de 2026, traducción propia
En la interfaz, crea una exploración de forma libre con ID de transacción en filas y Compras de comercio electrónico como valor, pon el mismo rango de fechas que la exportación de pedidos y descarga el CSV. Si tienes GA4 en español, renombra las columnas del fichero a “Transaction ID” y “Ecommerce purchases”, que son los nombres que espera el script. Por la Data API la dimensión es transactionId y la métrica ecommercePurchases. Añade deviceCategory o browser en una segunda exportación si quieres el desglose del propio GA4. Antes de fiarte del fichero, comprueba dos cosas:
- Umbrales de datos. La ayuda de Google sobre umbrales de datos dice que los define el sistema, que no se pueden ajustar y que pueden ocultar filas cuando el informe incluye datos demográficos o Google Signals. Limita la exploración al ID de transacción y al recuento de compras.
- Datos modelados. El modelado de comportamiento del modo de consentimiento solo aparece con la identidad de informes combinada y exige al menos 1.000 eventos diarios con
analytics_storage='denied'durante 7 días, más 1.000 usuarios diarios con consentimiento en 7 de los últimos 28 días. Los usuarios modelados no llevan IDs de transacción de pedidos reales, así que esta comparación va de datos observados.
¿Cómo calculo el hueco por pasarela y por dispositivo?
Este script cruza los dos ficheros e imprime la tasa de captación global y por método de pago, por dispositivo y por la marca de la página de agradecimiento. También cuenta los IDs de transacción con más de un evento purchase, que es la comprobación de duplicados de la auditoría de SEJ, y los IDs de GA4 que no casan con ningún pedido web. Python 3, solo biblioteca estándar:
#!/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])Ejecútalo una vez con noviembre y diciembre del año pasado (y, si puedes, hasta el 6 de enero) y otra con las últimas cuatro semanas. Un porcentaje suelto dice poco; el desglose es el que hace el trabajo.
¿Cómo interpreto el resultado?
| Lo que ves | Causa más probable | Dónde mirar |
|---|---|---|
| Una pasarela muy por debajo de las demás | Los compradores no vuelven de la página de esa pasarela | URL de retorno, página de pedido recibido en el móvil, thankyou_seen de esa pasarela |
Redsys con thankyou_seen = no en pedidos pagados | El cliente no pulsó «continuar» o el retorno automático no terminó | Configuración del retorno en el TPV, prueba de pago en el móvil cerrando la pantalla de Redsys |
thankyou_seen = no en muchos pedidos pagados | La página de pedido recibido no se cargó | Redirecciones de la pasarela, clientes que cierran la pestaña, errores de plugins en esa página |
thankyou_seen = yes pero captación baja | El navegador perdió el evento | Tasa de consentimiento, bloqueadores, errores de consola, el consentimiento por defecto que fija la extensión |
| Móvil muy por debajo del escritorio | Salto a la app del banco o a Bizum, navegadores dentro de apps | Probar el pago en un móvil con cada pasarela |
| Captación buena en octubre que cae en diciembre | Algo se rompió con la carga o se agotó un cupo | Límites del plan de la plataforma de consentimiento, despliegues de esa semana, reglas de la CDN |
| Muchos IDs con más de una compra | Página de pedido recibido recargada o dos etiquetas | Etiquetas duplicadas en GTM y en plugins, comportamiento al recargar |
| IDs de GA4 sin pedido web correspondiente | Pedidos pendientes o en espera contados, o pedidos de prueba | Estado de esos pedidos en WooCommerce |
Un hueco estable que no cambia por pasarela ni por dispositivo es, casi seguro, consentimiento. Esa parte se queda. Son los visitantes que dijeron que no, y GA4 dice la verdad sobre ellos al no contarlos.
¿Cómo envío la compra desde el servidor con el Measurement Protocol?
El Measurement Protocol de GA4 recibe eventos por HTTP desde tu servidor. Google es explícito sobre su papel:
“La finalidad del Measurement Protocol es complementar la recogida automática a través de gtag, Tag Manager y Google Analytics for Firebase, no sustituirla.”
Google Analytics, descripción general del Measurement Protocol, traducción propia
Así que deja la etiqueta del navegador para el comportamiento y usa el servidor solo para la compra. El diseño de abajo sigue la documentación punto por punto:
- client_id. La referencia acepta la cookie de ID de cliente y pide enviar su valor completo. El código guarda el valor de la cookie
_gaen el pedido durante el pago, sin descomponerlo. - session_id. La única forma documentada de leerlo es
gtag('get', '<target>', 'session_id', callback), según la referencia de gtag.js. La descripción general dice que los datos geográficos y de dispositivo de la sesión se aplican si incluyessession_iden el evento y lo envías al Measurement Protocol en las 24 horas siguientes al inicio de la sesión. El código lo lee en la página de pago y lo guarda una hora. - Consentimiento. El objeto
consentde la petición solo cubread_user_datayad_personalization, y si no lo indicas GA4 usa la configuración de consentimiento de las interacciones online correspondientes de ese cliente. El código no se inventa nada: si no hay cookie_gaen el pago no hay ID de cliente, y ese pedido no se envía. - Errores. El Measurement Protocol devuelve 2xx incluso con una carga mal formada, y Google dice que, ante una respuesta que no sea 2xx, corrijas la petición en lugar de repetir la misma. Valida antes contra
/debug/mp/collect, que devuelvevalidationMessagesy no registra nada. - Plazo. Los eventos admiten fecha retroactiva de hasta 72 horas. Una compra enviada al confirmarse el pago no necesita marca de tiempo; con Redsys la notificación llega antes incluso de que el cliente vuelva a la tienda.
- Recogida en la UE. La guía de envío de Google indica
region1.google-analytics.comcomo host si quieres que la recogida se haga en la Unión Europea.
Guárdalo como plugin must-use (wp-content/mu-plugins/ga4-mp-purchase.php) y define el ID de medición y el secreto de la API en wp-config.php. El secreto se crea en la configuración del flujo de datos web de 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 );Por qué estos hooks: woocommerce_payment_complete se dispara cuando una pasarela llama a WC_Order::payment_complete(), normalmente desde el gestor de su notificación de servidor, con el ID del pedido y el ID de transacción de la pasarela. Con Redsys, eso es lo que hace un plugin bien escrito al recibir el POST en la MERCHANTURL, antes de que el cliente vuelva. La transferencia bancaria y el contra reembolso nunca lo llaman; llegan a un estado pagado cuando tú o la pasarela cambiáis el estado, y eso lo recogen woocommerce_order_status_processing y woocommerce_order_status_completed. La marca _wpp_ga_mp_sent evita que la segunda y la tercera llamada vuelvan a enviar.
Los reembolsos salen en woocommerce_order_refunded, que también se dispara en reembolsos parciales. La guía de comercio electrónico de Google pide el transaction_id y los artículos reembolsados con item_id y quantity, así que el código añade las líneas reembolsadas cuando las hay. La extensión Google Analytics for WooCommerce no envía ningún evento de reembolso (su código no gestiona reembolsos), otra razón más por la que la métrica Transacciones y tu lista de pedidos se separan. Los hooks de captura cubren los dos pagos: woocommerce_checkout_order_created en el clásico con shortcode y woocommerce_store_api_checkout_order_processed en el de bloques, ambos llamados durante la propia petición del comprador, así que las cookies están ahí. Ese es el detalle que resuelve el caso de Redsys: la cookie se lee cuando el cliente pulsa el botón de pagar en tu tienda, no cuando vuelve.
El value se calcula como lo comenta el ejemplo de comercio electrónico de Google, la suma de precio por cantidad de todos los artículos, con impuestos y envío en sus propios campos. Si la etiqueta del navegador calcula el valor de otra forma (con IVA incluido, por ejemplo), alinea las dos antes de comparar ingresos.
Un dato más de Google: el Measurement Protocol ha llegado a un estado de producto maduro y cerrado y seguirá funcionando sin planes de retirada, y para nuevas integraciones de servidor a servidor Google recomienda ahora su Data Manager API. Para el evento de compra de una sola tienda, el Measurement Protocol es el trabajo más pequeño, y yo me quedaría con él.
¿Cómo evito contar el mismo pedido dos veces?
La ayuda de Google sobre duplicados describe la eliminación de compras duplicadas del mismo usuario por ID de transacción en flujos web. No dice nada del Measurement Protocol, así que no cuento con que GA4 fusione una compra del navegador y otra del servidor para el mismo pedido. Elige uno de los dos caminos documentados:
- Apagar la compra del navegador. En Google Analytics for WooCommerce, desmarca “Purchase Transactions”. El resto del embudo (vistas de producto, añadir al carrito, inicio del pago) sigue llegando desde el navegador. Pierdes el diagnóstico
_ga_trackeden los pedidos nuevos, así que haz la conciliación una vez antes del cambio. - Enviar la compra del servidor a una segunda propiedad. Es el consejo de Search Engine Journal para quien empieza ahora: mandarla a una propiedad de GA4 aparte durante el cuarto trimestre, para que nada se cuente dos veces. Compara las dos propiedades con la exportación de pedidos en enero, pasado Reyes, y decide entonces.
Dos semanas antes del Black Friday es mal momento para cambiar el evento de compra del que dependen tus pujas y tus informes. El segundo camino mantiene intactas las cifras actuales mientras reúnes pruebas.
¿Qué ajustes del modo de consentimiento cambian las cifras?
Google describe dos implementaciones del modo de consentimiento. En el modo básico las etiquetas no se cargan hasta que el visitante interactúa con el banner, y no se envía nada antes del consentimiento. En el modo avanzado las etiquetas se cargan con valores denegados por defecto y envían pings sin cookies mientras el consentimiento está denegado; es la configuración que exige el modelado de comportamiento. Ningún modo convierte un rechazo en una compra con ID de transacción.
Lo que merece la pena revisar en una tienda WooCommerce antes del pico:
- El valor por defecto que fija la extensión. Google Analytics for WooCommerce fija el consentimiento por defecto al iniciarse y te deja cambiarlo con el filtro
woocommerce_ga_gtag_consent_modes. Si tu plugin de consentimiento fija sus propios valores, WooCommerce documenta ese filtro como la forma de evitar que la extensión los sobrescriba. - La actualización del consentimiento. La documentación de WooCommerce dice que la integración del modo de consentimiento debe actualizar los estados después de que el visitante acepte. Comprueba que el estado pasa a concedido en la misma página vista y no en la siguiente, o la primera página tras aceptar se pierde.
- Los límites del plan de tu plataforma de consentimiento. El banner que se apagó en diciembre en la auditoría de SEJ era un cupo de facturación. Compara el tuyo con las páginas vistas del diciembre pasado.
- Lo que pasa después de “rechazar”. Mi artículo sobre un banner de cookies rechazado en el que el seguimiento sigue cargando trata el fallo contrario, y ese sí es un problema legal.
La configuración general de GA4, la etiqueta en WordPress y el consentimiento por defecto están en mi guía para añadir Google Analytics 4 a WordPress. Los pedidos que hacen agentes de IA sin navegador son un caso aparte, con su propia cuestión de ID de cliente, que trato en analítica del checkout de WooCommerce con agentes de IA. Si además tienes una tienda en Shopify, Shopify muestra checkouts pero no pedidos recorre el mismo tipo de hueco en esa plataforma.
¿Qué exige la ley española para la cookie de analítica?
En España la regla está en el artículo 22.2 de la Ley 34/2002, de servicios de la sociedad de la información y de comercio electrónico (LSSI-CE). La redacción vigente, según el texto consolidado del BOE, es la publicada el 10 de mayo de 2014 tras la Ley 9/2014. Su primera frase dice:
“Los prestadores de servicios podrán utilizar dispositivos de almacenamiento y recuperación de datos en equipos terminales de los destinatarios, a condición de que los mismos hayan dado su consentimiento después de que se les haya facilitado información clara y completa sobre su utilización, en particular, sobre los fines del tratamiento de los datos”
El mismo apartado añade que, cuando sea técnicamente posible y eficaz, el consentimiento puede darse mediante los parámetros adecuados del navegador o de otras aplicaciones, y deja fuera de la exigencia el almacenamiento o acceso técnico con el único fin de transmitir una comunicación por una red de comunicaciones electrónicas, o el estrictamente necesario para prestar un servicio de la sociedad de la información que el destinatario ha pedido expresamente. Una cookie de analítica de GA4 no transmite la comunicación ni hace falta para que el cliente termine su pedido, así que entra en la regla general del consentimiento.
Para este artículo la consecuencia práctica es directa: la compra enviada desde el servidor solo sale para pedidos que llevaban cookie de analítica en el pago, es decir, de compradores que aceptaron. Si el banner de tu tienda o la configuración de GA4 te generan dudas legales, eso es una revisión de protección de datos, no un ajuste técnico.
¿Qué reviso antes del Black Friday?
- Exportar los pedidos y los IDs de transacción de GA4 de noviembre y diciembre del año pasado (hasta Reyes si puedes), ejecutar la conciliación y anotar la captación por pasarela y por dispositivo.
- Repetirlo con las últimas cuatro semanas. Una caída entre los dos periodos es un problema de configuración que todavía puedes arreglar.
- Hacer un pedido real desde un móvil con cada pasarela que ofreces: tarjeta por Redsys, Bizum, PayPal si lo tienes. Una vez, cerrar la pantalla de Redsys sin pulsar «continuar» y comprobar que el pedido pasa a pagado y qué registra GA4.
- Buscar IDs de transacción duplicados y localizar la segunda etiqueta si la hay.
- Confirmar que los pedidos pendientes y en espera (transferencias, sobre todo) no llegan a GA4 como compras, o aceptarlo y dejarlo documentado.
- Comparar el límite del plan de la plataforma de consentimiento con el tráfico del diciembre pasado, y mandar sus avisos a quien pueda pagar la ampliación.
- Confirmar que la CDN no cachea la página de pedido recibido.
- Si añades la compra desde el servidor, validarla en
/debug/mp/collecty enviarla a una propiedad aparte durante el pico. - Poner en el calendario una revisión semanal hasta pasado Reyes: compras de GA4 divididas entre pedidos web de WooCommerce. La auditoría de SEJ muestra que los apagones se detectaron con semanas de retraso.
¿Cuándo merece la pena contar con un desarrollador?
La exportación y la conciliación son una tarde para quien se maneje con WP-CLI. La compra desde el servidor toca el pago, las notificaciones de la pasarela y el consentimiento, tres sitios donde un error cuesta dinero o crea un problema legal, y hay que probarla con cada pasarela que usas, Redsys y Bizum incluidos. Si prefieres tenerlo hecho y probado en staging antes del pico, hago este tipo de trabajo como desarrollador de WooCommerce.





