Medir de forma confiável a telemetria e as análises de sessões completas

Para rastrear de forma confiável as análises, as Core Web Vitals e os dados de telemetria que cobrem toda a visita de um usuário a uma página da Web (não apenas até o carregamento da página), use a API fetchLater().

A API fetchLater() é a maneira mais confiável de enviar dados a um servidor quando uma resposta não é necessária e o tempo de entrega não é urgente, o que se aplica a dados como análises de usuários, telemetria, rastreamento de erros e métricas de performance, como as Core Web Vitals.

Técnicas mais antigas, como criar um pixel <img> em um listener de eventos unload, são notoriamente não confiáveis (especialmente em dispositivos móveis) e podem afetar negativamente o desempenho (tornando as páginas não qualificadas para o bfcache).

Como fazer a implementação

  1. Programe a solicitação: assim que os dados relevantes estiverem disponíveis, chame fetchLater() com o payload de dados. Isso coloca os dados em fila para serem enviados mais tarde.

  2. Atualize o payload conforme necessário: se o usuário gerar mais dados ou o estado mudar, cancele a solicitação programada anteriormente e chame fetchLater() de novo com o snapshot totalmente atualizado.

  3. Deixe o navegador cuidar do resto: quando o usuário sair ou fechar a guia, o navegador vai enviar de forma confiável o payload da chamada fetchLater() mais recente.

Exemplo de código

Esse código mede a duração da sessão de um usuário em uma página usando fetchLater() para colocar um novo beacon na fila a cada 10 segundos com a duração atualizada da sessão.

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áticas recomendadas

  • USE fetchLater() para enviar dados a um servidor em qualquer situação em que uma resposta não seja necessária e não seja essencial que os dados sejam enviados imediatamente.
  • USE um AbortController para cancelar a busca pendente nos casos em que os dados precisam ser atualizados antes que o usuário saia da página.
  • REDUZA o tamanho do payload para evitar exceder a cota (atualmente, cerca de 64 KB por origem).
  • FAÇA o agrupamento de chamadas para fetchLater() em um try/catch para lidar com erros de cota.
  • FAÇA a detecção de recursos da presença de fetchLater() em globalThis e implemente uma estratégia de substituição para navegadores que não oferecem suporte à API.
  • NÃO use um objeto ReadableStream para o corpo da solicitação, porque isso vai causar um erro.

Suporte ao navegador e estratégias de substituição

Uma estratégia de fallback será necessária se fetchLater() não atingir sua meta de referência. No entanto, considerando os benefícios de confiabilidade e desempenho dessa API, use fetchLater() se o navegador for compatível.

A estratégia de fallback recomendada é usar o polyfill abaixo, que lida internamente com navegadores incompatíveis usando fetch() com keepalive ou navigator.sendBeacon(). Seu código PRECISA chamar fetchLater() diretamente. Nunca chame fetch(), sendBeacon() ou outras APIs de beacon por conta própria.

Polyfill fetchLater()

Use o seguinte polyfill mínimo de fetchLater(), que implementa a API da forma mais próxima possível em navegadores sem suporte.

A única diferença de comportamento notável com esse polyfill é que ele usa visibilitychange para detectar quando o usuário sai, em vez de depender do processamento de descarregamento nativo do navegador. Esse é um detalhe de implementação interna. Seu código não precisa detectar visibilitychange ou qualquer outro evento de ciclo de vida da página. Basta chamar fetchLater(), e o polyfill vai processar a 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;
    },
  };
};