Надежно измеряйте аналитику и телеметрию за весь сеанс

Чтобы надежно отслеживать аналитику, Core Web Vitals и данные телеметрии, которые охватывают все посещение пользователем веб-страницы (а не только до загрузки страницы), используйте fetchLater() API.

fetchLater() API – это самый надежный способ отправки данных на сервер в случаях, когда ответ не требуется и время доставки не имеет решающего значения. Это относится к таким данным, как пользовательская статистика, телеметрия, отслеживание ошибок и показатели эффективности, такие как Core Web Vitals.

Устаревшие методы, такие как создание пикселя <img> в прослушивателе событий unload, известны своей ненадежностью (особенно на мобильных устройствах) и могут негативно влиять на производительность (делая страницы непригодными для bfcache).

Рекомендации по внедрению

  1. Запланируйте запрос. Как только нужные данные станут доступны, вызовите fetchLater() с полезной нагрузкой. Данные будут поставлены в очередь на отправку.

  2. Обновляйте полезную нагрузку по мере необходимости. Если пользователь генерирует больше данных или состояние меняется, отмените ранее запланированный запрос и снова вызовите fetchLater() с полностью обновленным снимком.

  3. Предоставьте браузеру сделать все остальное. Когда пользователь покинет страницу или закроет вкладку, браузер надежно отправит полезную нагрузку из вашего последнего вызова fetchLater().

Пример кода

Этот код измеряет длительность сеанса пользователя на странице, используя fetchLater() для постановки в очередь нового веб-маяка каждые 10 секунд с обновленной длительностью сеанса.

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

Рекомендации

  • РЕКОМЕНДУЕТСЯ использовать fetchLater() для отправки данных на сервер в любой ситуации, когда ответ не требуется и не критично, чтобы данные были отправлены немедленно.
  • РЕКОМЕНДУЕТСЯ использовать AbortController, чтобы отменить ожидающий запрос, если данные нужно обновить до того, как пользователь покинет страницу.
  • РЕКОМЕНДУЕТСЯ минимизировать размер полезной нагрузки, чтобы не превысить квоту (в настоящее время около 64 КБ на источник).
  • РЕКОМЕНДУЕТСЯ обертывать вызовы fetchLater() в try/catch, чтобы обрабатывать ошибки, связанные с квотами.
  • РЕКОМЕНДУЕТСЯ проверять наличие fetchLater() в globalThis и реализовывать резервную стратегию для браузеров, которые не поддерживают API.
  • НЕ ИСПОЛЬЗУЙТЕ объект ReadableStream в теле запроса, иначе возникнет ошибка.

Поддержка браузеров и резервные стратегии

Если fetchLater() не соответствует вашему базовому целевому значению, требуется резервная стратегия. Однако, учитывая повышенную надежность и производительность этого API, следует использовать fetchLater(), если он поддерживается браузером.

Рекомендуемая резервная стратегия – использовать полифил, который обрабатывает неподдерживаемые браузеры с помощью fetch() с keepalive или navigator.sendBeacon(). Ваш код должен вызывать fetchLater() напрямую. Никогда не вызывайте fetch(), sendBeacon() или другие API маяков самостоятельно.

Полифил fetchLater()

Используйте следующий минимальный полифил fetchLater(), который реализует API в неподдерживаемых браузерах максимально близко к оригиналу.

Единственное заметное отличие в поведении этого полифила заключается в том, что он использует visibilitychange для обнаружения ухода пользователя, а не полагается на встроенную обработку выгрузки браузера. Это внутренняя деталь реализации – вашему коду не нужно прослушивать visibilitychange или другие события жизненного цикла страницы. Просто вызовите fetchLater(), и полифил обработает доставку.

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