Blog
Eventos de comercio electrónico sin plataforma de tienda: cómo hacer que un checkout propio informe a GA4
Publicado · Actualizado · 6 min de lectura
Una plataforma de tienda al menos te da un plugin con el que pelearte. Un checkout que has construido tú —una aplicación React, un flujo de reservas, un registro de SaaS con Stripe detrás— no te da nada: ningún evento se dispara hasta que tu propio código lo envía. La buena noticia es que el contrato es pequeño: un puñado de nombres, la forma de un objeto y unas pocas reglas sobre dónde va cada envío. Aquí está todo, incluida la parte que la mayoría de las guías se salta: las compras que ocurren cuando no hay nadie en tu página.
El contrato: un nombre y un objeto ecommerce
Todo lo que aparece aquí llega a Analytics por el mismo camino. Tu página envía un evento a window.dataLayer con su nombre de GA4 y un objeto ecommerce en la forma de GA4. Un activador de Tag Manager reconoce el nombre, y una etiqueta de evento de GA4 lee el objeto y lo reenvía a tu contenedor de servidor. Cada contenedor de SignalHost ya tiene esos activadores y etiquetas en su contenedor web, con el prefijo SH en el nombre: para los eventos de tienda de abajo, y para registros, pruebas gratuitas y leads. Lo que falta es el envío.
Dos reglas evitan la mayoría de los errores. Vacía primero el objeto ecommerce anterior, o un view_item de hace tres páginas se colará dentro del siguiente evento. Y en cada evento que lleve dinero, envía value como número y currency como código ISO: un value en forma de cadena o la falta de currency se aceptan sin una sola queja y quedan fuera de los ingresos sin que nadie lo note.
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 }
]
}
});Qué evento, y dónde va
Un flujo a medida tiene los mismos momentos que una plataforma de tienda. Solo tienes que encontrarlos en tu propio código.
- view_item_list: una página o un componente que muestra varios productos o planes. Una vez, cuando se muestra la lista, no con cada desplazamiento.
- view_item: la página de un solo producto o plan.
- add_to_cart y remove_from_cart: en el manejador del botón, después de que tu carrito haya cambiado de verdad.
- view_cart: cuando se muestra el carrito, ya sea en su página o al abrirse el panel lateral.
- begin_checkout: el momento en que el visitante entra en el checkout, ya sea el primer renderizado de la página de checkout o el clic que abre un diálogo de checkout.
- add_shipping_info: cuando se elige un método de envío. Omítelo si no haces envíos.
- add_payment_info: cuando se aceptan los datos de pago, no cuando aparece el formulario.
- purchase: una vez, cuando se confirma el pedido. La mayor parte de este artículo trata de hacer bien este evento.
- sign_up y start_trial: para un negocio de suscripciones, las conversiones anteriores a cualquier pago. SignalHost construye etiquetas para ambos junto a los eventos de tienda, y también para los leads: generate_lead, contact, schedule, submit_application y subscribe.
Envía desde el manejador que provocó el cambio, no desde un renderizado. Una aplicación de página única vuelve a renderizar sin reparos, y React ejecuta los efectos dos veces en desarrollo: un envío dentro de un efecto se duplica en la compilación de desarrollo y se repite cada vez que un componente se vuelve a montar.
La compra en tu página de confirmación: lee el pedido de tu servidor
Cuando el visitante vuelve a una página tuya después de pagar, esa página es donde va purchase, con dos condiciones. Toma el pedido de tu propio backend, no de la URL: los parámetros de consulta se pueden editar, las redirecciones los eliminan y también llegan con los pagos fallidos. Stripe, sin ir más lejos, devuelve al visitante a la misma dirección tanto si el pago se ha completado como si no, y lo indica en un parámetro propio. Y envíalo una sola vez: una recarga, el botón Atrás o una página de recibo guardada en marcadores vuelven a pasar por ella.
// 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 es una red de seguridad, no un plan. Analytics descarta un transaction_id repetido dentro de una misma sesión, pero no una recarga de la página del recibo a la mañana siguiente.
El pago en la página de otro: Stripe Checkout, PayPal, una redirección al banco
Una página de pago alojada es la opción habitual en un desarrollo propio, y tiene un punto peligroso: puede que el visitante nunca vuelva. Paga, cierra la pestaña y tu página de confirmación nunca llega a renderizarse. Con tarjeta, eso es un pequeño porcentaje de las compras. Con transferencias bancarias, facturas y métodos de pago aplazado puede ser la mayoría, porque el dinero llega horas o días después.
Así que decide, para cada método de pago, dónde está la verdad. Si tu backend se entera del pago por un webhook, el webhook es el lugar fiable para informar de él —lo verás en la siguiente sección—, y la página de confirmación no debe enviar purchase además, o cada pago con tarjeta se contará dos veces. Si el único sitio donde te enteras es la página de retorno, informa allí y asume el hueco.
Las compras que ningún navegador presencia: envíalas desde tu servidor
Con las suscripciones esto es inevitable. Una prueba se convierte a los siete días, una renovación llega cada mes, una factura se paga por transferencia la semana que viene. Cada una es una compra, y ninguna ocurre delante de un navegador. La solución es enviar el evento de servidor a servidor, desde el webhook que se entera del pago, al mismo punto de entrada de etiquetado que usan tus páginas —tu nombre de host de SignalHost o tu propio dominio—, para que pase por tu contenedor de servidor como cualquier otro hit.
Para las compras enviadas desde el servidor, guarda las decisiones reales de consentimiento del visitante junto con el pedido. La ausencia de una cookie `_ga` no demuestra un rechazo. Este ejemplo envía datos solo a Analytics y únicamente con un permiso registrado y un ID de cliente existente; la publicidad sigue denegada. El envío a plataformas publicitarias necesita sus propios permisos guardados.
// At checkout: save the visitor's actual banner choice with the order.
// Cookie presence alone is NOT consent. This example is Analytics-only.
const ga = req.cookies._ga;
order.gaClientId = ga ? ga.split('.').slice(-2).join('.') : null;
order.analyticsGranted = req.body.consent?.analytics === true;
// Later, in the payment webhook: only send with recorded permission and id.
if (order.analyticsGranted === true && order.gaClientId) {
const params = new URLSearchParams({
v: '2',
tid: 'G-XXXXXXXXXX',
cid: order.gaClientId,
gcs: 'G101', // Analytics allowed; advertising stays denied.
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' });
}Elige una fuente por evento y no te salgas de ella. Así es exactamente como signalhost.io mide sus propias ventas: el navegador envía sign_up, begin_checkout y add_payment_info; nuestro servidor envía start_trial cuando una prueba ha empezado de verdad, para que una pestaña cerrada no pueda perderlo; y el webhook de Stripe envía purchase cuando se paga la primera factura. Ningún evento tiene dos fuentes. El Measurement Protocol de Google también funcionaría, pero va directo a Google y se salta tu contenedor de servidor, así que ninguna de tus etiquetas del lado del servidor llega a ver la venta.
Las compras tardías son uno de los cuatro motivos por los que GA4 cuenta menos que tu lista de pedidos: los otros tres, y cuáles puedes corregir.
Configurar tu banner de cookies
Elige el modo adecuado para la política de tu organización. No desactives un bloqueo deliberado solo para que aparezcan eventos.
**Basic:** bloquea el fragmento de seguimiento hasta obtener el consentimiento correspondiente. No se esperan eventos de seguimiento en SignalHost antes de consentir.
**Advanced:** carga las etiquetas de Google con valores iniciales `denied`. Google puede recibir señales de medición sin cookies antes de elegir o después de rechazar. Una señal no es consentimiento.
Antes de que empiece el seguimiento, el banner debe establecer los permisos como denegados. Tras la elección, actualiza solo las finalidades permitidas por el visitante, recuerda su elección y gestiona su retirada.
[Guía de configuración del banner de cookies](/es/blog/banner-cookies-consent-mode-servidor)
Comprobar que funciona
- Abre el contenedor en SignalHost. La tarjeta de eventos enumera lo que llegó, por nombre, con la proporción que llevaba consentimiento. Un nombre que enviaste y que no ves ahí nunca llegó a tu servidor de etiquetado.
- Recorre tu checkout en el modo Vista previa de Tag Manager. Muestra cada envío, qué etiqueta se disparó y el objeto ecommerce que se envió.
- En Analytics, DebugView muestra los eventos a medida que llegan. Los informes estándar pueden tardar un día.
- Recarga la página de confirmación dos veces y cuenta las compras. Debería seguir habiendo una.
Aquí todos los fallos son silenciosos: un nombre con las mayúsculas mal puestas, value enviado como cadena, items anidado un nivel de más. Ninguno produce un error en ningún sitio. Todos producen un informe con un agujero.

