Mide de forma confiable las estadísticas y la telemetría de las sesiones completas

Para hacer un seguimiento confiable de las estadísticas, las Métricas web esenciales y los datos de telemetría que abarcan la totalidad de la visita de un usuario a una página web (no solo hasta que se carga la página), usa la API de fetchLater().

La API de fetchLater() es la forma más confiable de enviar datos a un servidor en los casos en que no se requiere una respuesta y el tiempo de entrega no es urgente, lo que se aplica a datos como las estadísticas de los usuarios, la telemetría, el seguimiento de errores y las métricas de rendimiento, como las Métricas web esenciales.

Las técnicas más antiguas, como crear un píxel de <img> en un objeto de escucha de eventos de unload, son notoriamente poco confiables (en especial en dispositivos móviles) y pueden afectar negativamente el rendimiento (ya que hacen que las páginas no sean aptas para la bfcache).

Cómo implementarlo

  1. Programa la solicitud: En cuanto estén disponibles los datos pertinentes, llama a fetchLater() con tu carga útil de datos. Esto pone en cola los datos para enviarlos más tarde.

  2. Actualiza la carga útil según sea necesario: Si el usuario genera más datos o cambia el estado, anula la solicitud programada anteriormente y vuelve a llamar a fetchLater() con la instantánea completamente actualizada.

  3. Deja que el navegador se encargue del resto: Cuando el usuario salga de la página o cierre la pestaña, el navegador enviará de forma confiable la carga útil de tu llamada más reciente a fetchLater().

Ejemplo de código

Este código mide la duración de la sesión de la visita de un usuario a una página con fetchLater() para poner en cola una nueva baliza cada 10 segundos con la duración de la sesión actualizada.

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

Prácticas recomendadas

  • SÍ usa fetchLater() para enviar datos a un servidor en cualquier situación en la que no sea necesaria una respuesta y no sea fundamental que los datos se envíen de inmediato.
  • SÍ usa un AbortController para cancelar la recuperación pendiente en los casos en que los datos deban actualizarse antes de que el usuario abandone la página.
  • SÍ reduce al mínimo el tamaño de la carga útil para evitar exceder la cuota (actualmente, alrededor de 64 KB por origen).
  • SÍ une las llamadas a fetchLater() en un try/catch para controlar los errores de cuota.
  • SÍ detecta la presencia de fetchLater() en globalThis y, además, implementa una estrategia de resguardo para los navegadores que no admiten la API.
  • NO uses un objeto ReadableStream para el cuerpo de la solicitud, ya que se producirá un error.

Compatibilidad con navegadores y estrategias de resguardo

Se requiere una estrategia de resguardo si fetchLater() no cumple con tu objetivo de referencia. Sin embargo, dadas las mejoras en la confiabilidad y el rendimiento de esta API, se debe usar fetchLater() si el navegador la admite.

La estrategia de resguardo recomendada es usar el polyfill que se muestra a continuación, que controla de forma interna los navegadores no compatibles con fetch() con keepalive o navigator.sendBeacon(). Tu propio código DEBE llamar a fetchLater() directamente. Nunca llames a fetch(), sendBeacon() ni a otras APIs de balizas por tu cuenta.

Polyfill de fetchLater()

Usa el siguiente polyfill mínimo de fetchLater(), que implementa la API lo más fielmente posible en los navegadores que no la admiten.

La única diferencia de comportamiento notable con este polyfill es que usa visibilitychange para detectar cuándo el usuario abandona la página, en lugar de depender del control de descarga nativo del navegador. Este es un detalle de implementación interna; tu código no necesita escuchar visibilitychange ni ningún otro evento del ciclo de vida de la página. Solo llama a fetchLater() y el polyfill se encargará de la entrega.

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