Mesurez de manière fiable les données analytiques et télémétriques de l'intégralité des sessions

Fonctionnalités Web : fetchLater AbortController et AbortSignal

Pour suivre de manière fiable les données d'analyse, les métriques Core Web Vitals et les données de télémétrie qui couvrent l'intégralité de la visite d'un utilisateur sur une page Web (et pas seulement jusqu'au chargement de page), utilisez l'API fetchLater().

L'API fetchLater() est le moyen le plus fiable d'envoyer des données à un serveur lorsqu'une réponse n'est pas requise et que le délai de livraison n'est pas urgent. Cela s'applique aux données telles que les analyses utilisateur, la télémétrie, le suivi des erreurs et les métriques de performances telles que les Core Web Vitals.

Les techniques plus anciennes, comme la création d'un pixel <img> dans un écouteur d'événements unload, sont notoirement peu fiables (en particulier sur mobile) et peuvent avoir un impact négatif sur les performances (en rendant les pages inéligibles au bfcache).

Mise en œuvre

  1. Planifiez la requête : dès que les données pertinentes sont disponibles, appelez fetchLater() avec votre charge utile de données. Cela met les données en file d'attente pour qu'elles soient envoyées plus tard.

  2. Mettez à jour la charge utile si nécessaire : si l'utilisateur génère plus de données ou si l'état change, annulez la requête précédemment planifiée et appelez à nouveau fetchLater() avec l'instantané entièrement mis à jour.

  3. Laissez le navigateur gérer le reste : lorsque l'utilisateur quitte la page ou ferme l'onglet, le navigateur envoie de manière fiable la charge utile de votre appel fetchLater() le plus récent.

Exemple de code

Ce code mesure la durée de la session d'un utilisateur sur une page à l'aide de fetchLater() pour mettre en file d'attente une nouvelle balise toutes les 10 secondes avec la durée de session mise à jour.

const ANALYTICS_ENDPOINT = '/path/to/analytics/endpoint';

const sessionData = {
  duration: 0,
  id: crypto.randomUUID(),
};

let fetchLaterController = null;

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

  // Update session duration to the current page time.
  sessionData.duration = performance.now();

  // Schedule a fetch for the data payload to be sent later.
  // IMPORTANT: wrap the call in a try/catch to handle quota errors.
  try {
    fetchLater(ANALYTICS_ENDPOINT, {
      method: 'POST',
      headers: {'content-type': 'application/json'},
      body: JSON.stringify(sessionData),
      signal: fetchLaterController.signal,
    });
  } catch (error) {
    // Handle errors as needed.
  }
}

// Update the session data and queue a new beacon every 10 seconds.
setInterval(queueBeacon, 10000);

Bonnes pratiques

  • À FAIRE : Utilisez fetchLater() pour envoyer des données à un serveur dans toutes les situations où une réponse n'est pas nécessaire et où il n'est pas essentiel que les données soient envoyées immédiatement.
  • À FAIRE : Utilisez un AbortController pour annuler la récupération en attente si les données doivent être mises à jour avant que l'utilisateur ne quitte la page.
  • À FAIRE : Réduisez la taille de la charge utile pour éviter de dépasser le quota (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

Une stratégie de remplacement est requise si fetchLater() n'atteint pas votre objectif de référence. Toutefois, compte tenu des avantages de cette API en termes de fiabilité et de performances, il est préférable d'utiliser fetchLater() si le navigateur le permet.

La stratégie de remplacement recommandée consiste à utiliser le polyfill ci-dessous, qui gère en interne les navigateurs non compatibles à l'aide de fetch() avec keepalive ou navigator.sendBeacon(). Votre propre code DOIT appeler fetchLater() directement. N'appelez jamais fetch(), sendBeacon() ou d'autres API de balise vous-même.

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'il utilise visibilitychange pour détecter le moment où l'utilisateur quitte la page, plutôt que de s'appuyer sur la gestion native du déchargement du navigateur. Il s'agit d'un détail d'implémentation interne. Votre code n'a pas besoin d'écouter visibilitychange ni d'autres événements de cycle de vie de la page. Il vous suffit d'appeler fetchLater(), et le polyfill gère la diffusion.

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;
    },
  };
};