Anti-rebond et traitement par lot de plusieurs événements d'analyse

Fonctionnalités Web : fetchLater AbortController et AbortSignal

La plupart des données d'analyse, de télémétrie et d'analyse des métriques Core Web Vitals sont moins prioritaires que la réponse à l'utilisateur. Vous pouvez donc attendre que l'utilisateur quitte la page pour les envoyer. La seule exception est si vous devez fournir des mises à jour en temps réel, auquel cas la rapidité est importante.

La méthode optimale pour fournir des mises à jour d'analyse en temps réel, tout en minimisant les balises, consiste à utiliser fetchLater() avec l'option de configuration activateAfter. Cela vous permet de regrouper efficacement tous les événements d'analyse qui se produisent dans un intervalle de temps donné en une seule balise, qui est envoyée de manière fiable même si l'utilisateur quitte la page avant l'expiration du délai avant expiration.

Mise en œuvre

  1. Planifier la requête : dès que des données d'analyse pertinentes sont disponibles, appelez fetchLater() avec votre charge utile de données et transmettez une valeur activateAfter. Les données sont alors mises en file d'attente pour être envoyées une fois ce délai écoulé ou si l'utilisateur quitte la page avant.

  2. Regrouper plusieurs événements : si un nouvel événement d'analyse se produit avant l'expiration du délai avant expiration activateAfter, annulez la requête précédemment planifiée et appelez à nouveau fetchLater() (avec la même valeur activateAfter) avec la file d'attente d'événements complète dans une seule charge utile.

  3. Réinitialiser la file d'attente d'événements à l'expiration du délai avant expiration : si un nouvel événement d'analyse se produit après l'envoi de la balise planifiée (c'est-à-dire que la valeur activated du résultat fetchLater() est true), réinitialisez la file d'attente d'événements.

  4. Laissez le navigateur gérer le reste : si l'utilisateur quitte la page ou ferme l'onglet avant l'expiration du délai avant expiration de activateAfter, le navigateur enverra toujours de manière fiable la charge utile de votre appel fetchLater() le plus récent.

Exemple de code

Ce code suit tous les événements load et click sur une page, et regroupe tous les événements qui se produisent dans un délai de 10 secondes.

// Replace with your analytics endpoint.
const ANALYTICS_ENDPOINT = '/path/to/analytics/endpoint';

// Replace with a time window of your choice. All analytics events that
// occur within this time window will be batched together.
const BATCH_WINDOW = 10 * 1000;

// The maximum number of events to batch. Pick a number that is unlikely
// to overflow the fetchLater() quota for the page.
const MAX_QUEUE_SIZE = 100;

const eventQueue = [];
let fetchLaterResult;
let fetchLaterController;

function trackEvent(eventData) {
  // If the previously queued beacon has already been sent, or if the
  // max queue size has been met, reset the queue.
  if (fetchLaterResult?.activated || eventQueue.length > MAX_QUEUE_SIZE) {
    fetchLaterController = null;
    fetchLaterResult = null;
    eventQueue.length = 0;
  }

  eventQueue.push(eventData);

  // Abort any pending beacons before creating a new one.
  if (fetchLaterController) {
    fetchLaterController.abort();
  }
  fetchLaterController = new AbortController();

  // Schedule a fetch for the events to be sent when the batch window expires.
  // IMPORTANT: wrap the call in a try/catch to handle quota errors.
  try {
    fetchLaterResult = fetchLater(ANALYTICS_ENDPOINT, {
      method: 'POST',
      headers: {'content-type': 'application/json'},
      body: JSON.stringify(eventQueue),
      signal: fetchLaterController.signal,
      activateAfter: BATCH_WINDOW,
    });
  } catch (error) {
    // Handle errors as needed.
  }
}

// Track page loads.
window.addEventListener('load', () => {
  trackEvent({type: 'page_load'});
});

// Track click events.
window.addEventListener('click', (event) => {
  trackEvent({type: 'click', target: serializeElement(event.target)});
});

Bonnes pratiques

  • À FAIRE : utilisez fetchLater() avec l'option activateAfter pour regrouper plusieurs événements d'analyse qui se produisent à proximité les uns des autres.
  • À FAIRE : Utilisez un AbortController pour annuler la récupération en attente lorsqu'un nouvel événement d'analyse se produit.
  • À FAIRE : Vérifiez que vous ne regroupez pas trop d'événements et que vous ne dépassez pas le quota fetchLater() (actuellement d'environ 64 Ko par origine).
  • À FAIRE : encapsulez les appels à fetchLater() dans un try/catch pour gérer les erreurs de quota.
  • À FAIRE : Détectez la présence de fetchLater() sur globalThis et implémentez une stratégie de remplacement pour les navigateurs qui ne sont pas compatibles avec l'API.
  • N'utilisez PAS d'objet ReadableStream pour le corps de la requête, car cela générerait une erreur.

Compatibilité des navigateurs et stratégies de remplacement

Par conséquent, une stratégie de remplacement est généralement requise.

Toutefois, compte tenu de l'amélioration de la fiabilité et des performances de cette API, fetchLater() doit être utilisé si le navigateur le permet.

fetchLater() polyfill

Utilisez le polyfill fetchLater() minimal suivant, qui implémente l'API aussi fidèlement que possible dans les navigateurs non compatibles.

La seule différence de comportement notable avec ce polyfill est qu'au lieu d'envoyer la charge utile lorsque l'utilisateur quitte la page, il l'envoie chaque fois que l'état visibilityState de la page passe à "hidden", car il s'agit du signal de fin de session le plus fiable actuellement disponible.

globalThis.fetchLater ??= function fetchLater(url, init = {}) {
  let timeoutHandle;
  let activated = false;

  function sendNow() {
    if (!(init.signal && init.signal.aborted)) {
      // Use fetch keepalive if the browser supports it or if custom fetch
      // parameters are specified (e.g. custom headers or methods).
      // Otherwise fall back to `navigator.sendBeacon()`.
      if (
        'keepalive' in Request.prototype ||
        init.method !== 'POST' ||
        init.headers
      ) {
        fetch(url, Object.assign({}, init, {keepalive: true}));
        activated = true;
      } else {
        activated = navigator.sendBeacon(url, init.body);
      }
    }
    destroy();
  }

  function destroy() {
    document.removeEventListener('visibilitychange', sendNow);
    clearTimeout(timeoutHandle);
  }

  if (document.visibilityState === 'hidden') {
    // If the beacon was created while the page is already hidden, send data
    // ASAP but wait until the next microtask to allow all sync code to run.
    queueMicrotask(sendNow);
  } else {
    document.addEventListener('visibilitychange', sendNow);

    if (typeof init.activateAfter === 'number' && init.activateAfter >= 0) {
      timeoutHandle = setTimeout(sendNow, init.activateAfter);
    }
  }

  if (init.signal) {
    init.signal.addEventListener('abort', destroy);
  }

  return {
    get activated() {
      return activated;
    },
  };
};