Retour au blog

Blog

Événements e-commerce sans plateforme de boutique : faire remonter un tunnel de paiement sur mesure dans GA4

Publié · 6 min de lecture

Une plateforme de boutique vous donne au moins un plugin avec lequel vous disputer. Un tunnel de paiement que vous avez construit vous-même — une application React, un parcours de réservation, une inscription SaaS avec Stripe derrière — ne vous donne rien : aucun événement ne se déclenche tant que votre propre code ne le pousse pas. La bonne nouvelle, c’est que le contrat est court : une poignée de noms, une forme d’objet, et quelques règles sur la place de chaque push. Le voici en entier, y compris la partie que la plupart des guides laissent de côté : les achats qui ont lieu quand personne n’est sur votre page.

Le contrat : un nom et un objet ecommerce

Tout ici parvient à Analytics de la même façon. Votre page pousse un événement dans window.dataLayer sous son nom GA4, portant un objet ecommerce à la forme GA4. Un déclencheur Tag Manager reconnaît le nom, et une balise d’événement GA4 lit l’objet et le transmet à votre conteneur serveur. Si vous avez indiqué à SignalHost que votre site est une boutique — ou cliqué sur « Configurer » sous « Événements de conversion » sur la page du conteneur —, ces déclencheurs et ces balises existent déjà dans votre conteneur web, nommés avec le préfixe SH. Reste le push.

Deux règles évitent la plupart des erreurs. Videz d’abord l’objet ecommerce précédent, sinon un view_item d’il y a trois pages voyage en passager clandestin dans l’événement suivant. Et sur chaque événement qui porte de l’argent, envoyez value sous forme de nombre et currency sous forme de code ISO : une value en chaîne de caractères ou une currency absente est acceptée sans broncher et discrètement exclue des revenus.

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 }
    ]
  }
});

Quel événement, et où il a sa place

Un parcours sur mesure a les mêmes moments qu’une plateforme de boutique. Il vous faut seulement les trouver dans votre propre code.

  • view_item_list — une page ou un composant qui affiche plusieurs produits ou formules. Une fois quand la liste s’affiche, pas à chaque défilement.
  • view_item — la page d’un seul produit ou d’une seule formule.
  • add_to_cart et remove_from_cart — dans le gestionnaire du bouton, une fois que votre panier a réellement changé.
  • begin_checkout — le moment où le visiteur entre dans le paiement : le premier rendu de la page de paiement, ou le clic qui ouvre une fenêtre de paiement.
  • add_payment_info — quand les informations de paiement sont acceptées, pas quand le formulaire apparaît.
  • purchase — une fois, quand la commande est confirmée. L’essentiel de cet article porte sur la façon de réussir celui-ci.
  • sign_up et start_trial — pour une activité par abonnement, les conversions qui précèdent tout échange d’argent. SignalHost construit des balises pour les deux, à côté de celles des événements de boutique.

Poussez depuis le gestionnaire qui a provoqué le changement, pas depuis un rendu. Une application monopage refait ses rendus librement, et React exécute les effets deux fois en développement : un push placé dans un effet est doublé dans le build de développement et se répète à chaque nouveau montage d’un composant.

L’achat sur votre page de confirmation : lire la commande depuis votre serveur

Quand le visiteur revient sur l’une de vos pages après avoir payé, c’est sur cette page que purchase a sa place — à deux conditions. Prenez la commande dans votre propre backend, pas dans l’URL : les paramètres de requête peuvent être modifiés, se perdent dans les redirections et arrivent aussi lors des paiements échoués. Stripe, par exemple, renvoie à la même adresse que le paiement ait abouti ou non, et l’indique dans un paramètre qui lui est propre. Et poussez-le une seule fois : un rechargement, le bouton Retour ou une page de reçu enregistrée en favori y ramènent tous.

// 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 est un filet de sécurité, pas une stratégie. Analytics écarte un transaction_id répété au sein d’une même session, mais pas un rechargement de la page de reçu le lendemain matin.

Le paiement sur la page de quelqu’un d’autre : Stripe Checkout, PayPal, une redirection bancaire

Une page de paiement hébergée est le choix habituel pour un développement sur mesure, et elle a un écueil : le visiteur peut ne jamais revenir. Il paie, ferme l’onglet, et votre page de confirmation ne s’affiche jamais. Avec les cartes, cela représente quelques pour cent des achats. Avec les virements, les factures et les solutions de paiement différé, cela peut en être la majorité, parce que l’argent arrive des heures ou des jours plus tard.

Décidez donc, moyen de paiement par moyen de paiement, où se trouve la vérité. Si votre backend apprend le paiement par un webhook, le webhook est l’endroit fiable pour le signaler — voir la section suivante — et la page de confirmation ne doit pas pousser purchase en plus, sinon chaque paiement par carte est compté deux fois. Si le seul endroit où vous l’apprenez est la page de retour, signalez-le là et acceptez l’écart.

Les achats auxquels aucun navigateur n’assiste : envoyez-les depuis votre serveur

Les abonnements rendent cela inévitable. Un essai se convertit au bout de sept jours, un renouvellement a lieu chaque mois, une facture est réglée par virement la semaine prochaine. Chacun est un achat, et aucun n’a lieu devant un navigateur. La solution consiste à envoyer l’événement de serveur à serveur, depuis le webhook qui apprend le paiement, vers le même point de terminaison de tagging que vos pages — votre nom d’hôte SignalHost ou votre propre domaine — pour qu’il traverse votre conteneur serveur comme n’importe quel autre hit.

Il ne faut qu’une chose du tunnel de paiement : l’identifiant client Analytics du visiteur, pour que l’achat soit attribué à la personne dont les visites y ont mené. Il se trouve dans le cookie _ga, que votre requête de paiement transporte déjà quand elle va vers votre propre domaine. Stockez-le avec la commande. Pas de cookie _ga signifie que le visiteur n’a pas accordé le consentement analytics : envoyez le hit en consentement refusé avec un identifiant jetable, ou ne l’envoyez pas du tout — n’inventez jamais d’identité à sa place.

// 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' });

Choisissez une seule source par événement et tenez-vous-y. C’est exactement ainsi que signalhost.io mesure ses propres ventes : le navigateur pousse sign_up, begin_checkout et start_trial ; le webhook Stripe envoie purchase quand la première facture est payée ; rien n’envoie les deux. Le Measurement Protocol de Google fonctionnerait aussi, mais il va directement chez Google et contourne votre conteneur serveur, si bien qu’aucune de vos balises côté serveur ne voit jamais la vente.

Le consentement ne vous demande rien de plus

Poussez les événements quel que soit le choix du visiteur. Les balises obéissent à Consent Mode : avec consentement, elles envoient normalement ; sans, elles envoient des pings sans cookie qu’Analytics modélise au lieu de les rapporter. Retenir vous-même les pushes ne fait que cacher des conversions à cette modélisation. Ce qui compte, en revanche, c’est l’ordre : l’extrait Tag Manager vient après le script de votre bannière de consentement, pour que les valeurs par défaut du consentement existent avant l’envoi du premier événement.

Vérifier que tout fonctionne

  • Ouvrez le conteneur dans SignalHost. La carte « Événements » liste ce qui est arrivé, par nom, avec la part qui portait un consentement. Un nom que vous avez poussé et que vous n’y voyez pas n’a jamais atteint votre serveur de tagging.
  • Parcourez votre tunnel de paiement dans le mode Aperçu de Tag Manager. Il montre chaque push, la balise qui s’est déclenchée et l’objet ecommerce qui a été envoyé.
  • Dans Analytics, DebugView affiche les événements à mesure qu’ils arrivent. Les rapports standard peuvent prendre une journée.
  • Rechargez deux fois la page de confirmation et comptez les achats. Il ne doit toujours y en avoir qu’un.

Les échecs ici sont tous silencieux : un nom avec la mauvaise casse, value envoyé en chaîne de caractères, items imbriqué un niveau trop bas. Aucun ne produit d’erreur nulle part. Chacun produit un rapport avec un trou dedans.

Cessez de perdre vos données

Dix mille requêtes par mois, gratuitement, aussi longtemps que vous voulez. Quatre minutes pour savoir si ça aide.