Как устранить дребезг и объединить несколько событий аналитики в пакет

Большинство данных аналитики, телеметрии и Core Web Vitals имеют более низкий приоритет, чем ответ пользователю, поэтому вы можете безопасно отложить их отправку до тех пор, пока пользователь не покинет страницу. Исключение составляют случаи, когда вам нужно передавать обновления в реальном времени.

Оптимальный способ получать обновления аналитики в реальном времени и при этом минимизировать количество маячков – использовать fetchLater() с параметром конфигурации activateAfter. Это позволяет эффективно устранять дребезг и объединять все события аналитики, происходящие в течение заданного временного окна, в один маяк, который надежно отправляется, даже если пользователь покидает страницу до истечения времени ожидания.

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

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

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

  3. Сброс очереди событий по истечении времени ожидания. Если новое событие аналитики происходит после успешной отправки запланированного веб-маяка (т. е. значение activated результата fetchLater() равно true), сбросьте очередь событий.

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

Пример кода

Этот код отслеживает все события load и click на странице и объединяет в пакет все события, которые происходят в течение 10 секунд.

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

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

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

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

Поэтому обычно требуется резервная стратегия.

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

Полифил fetchLater()

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

Единственное заметное отличие в поведении этого полифила заключается в том, что вместо отправки полезной нагрузки, когда пользователь покидает страницу, он отправляет ее всякий раз, когда visibilityState страницы меняется на "скрыто", поскольку это наиболее надежный сигнал окончания сеанса, который широко доступен сегодня.

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