Debounce and batch multiple analytics events

Większość danych analitycznych, telemetrycznych i dotyczących Core Web Vitals ma niższy priorytet niż odpowiadanie użytkownikowi, więc możesz bezpiecznie odłożyć ich wysyłanie do momentu, gdy użytkownik opuści stronę. Wyjątkiem jest sytuacja, gdy musisz dostarczać aktualizacje w czasie rzeczywistym – wtedy liczy się aktualność.

Optymalnym sposobem na dostarczanie aktualizacji statystyk w czasie rzeczywistym przy jednoczesnym ograniczeniu liczby sygnałów jest użycie fetchLater() z opcją konfiguracji activateAfter. Dzięki temu możesz skutecznie eliminować powtórzenia i grupować wszystkie zdarzenia analityczne, które wystąpiły w danym przedziale czasu, w jedną sygnalizację, która jest niezawodnie wysyłana, nawet jeśli użytkownik opuści stronę przed upływem limitu czasu.

Jak to zrobić?

  1. Zaplanuj żądanie: gdy tylko będą dostępne odpowiednie dane analityczne, wywołaj fetchLater() z ładunkiem danych i przekaż wartość activateAfter. Spowoduje to umieszczenie danych w kolejce do wysłania po upływie tego czasu lub po wcześniejszym opuszczeniu strony przez użytkownika.

  2. Grupowanie wielu zdarzeń: jeśli nowe zdarzenie analityczne wystąpi przed upływem limitu czasu activateAfter, anuluj wcześniej zaplanowane żądanie i ponownie wywołaj fetchLater() (z tą samą wartością activateAfter) z pełną kolejką zdarzeń w jednym ładunku.

  3. Resetowanie kolejki zdarzeń po upływie limitu czasu: jeśli po pomyślnym wysłaniu zaplanowanego sygnału (czyli gdy wartość activated wyniku fetchLater() to true) wystąpi nowe zdarzenie analityczne, zresetuj kolejkę zdarzeń.

  4. Pozwól przeglądarce zająć się resztą: jeśli użytkownik opuści stronę lub zamknie kartę przed upływem limitu czasu activateAfter, przeglądarka i tak niezawodnie wyśle ładunek z ostatniego wywołania fetchLater().

Przykładowy kod

Ten kod śledzi wszystkie zdarzenia load i click na stronie i grupuje wszystkie zdarzenia, które wystąpiły w ciągu 10 sekund.

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

Sprawdzone metody

  • UŻYWAJ fetchLater() z opcją activateAfter, aby grupować wiele zdarzeń analitycznych, które występują blisko siebie.
  • UŻYWAJ AbortController, aby anulować oczekujące pobieranie w przypadku wystąpienia nowego zdarzenia analitycznego.
  • DO check to ensure you do not batch too many events together and exceed the fetchLater() quota (currently roughly 64KB per origin).
  • ZALECENIE: wywołania fetchLater() umieszczaj w try/catch, aby obsługiwać błędy związane z limitami.
  • DO feature detect the presence of fetchLater() on globalThis and implement a fallback strategy for browsers that don't support the API.
  • NIE używaj obiektu ReadableStream w treści żądania, ponieważ spowoduje to błąd.

Obsługa przeglądarek i strategie zastępcze

Dlatego zwykle wymagana jest strategia zastępcza.

Jednak ze względu na większą niezawodność i wydajność tego interfejsu API należy używać fetchLater(), jeśli przeglądarka go obsługuje.

fetchLater() polyfill

Użyj tego minimalnego kodu polyfill fetchLater(), który implementuje interfejs API w jak największym stopniu w nieobsługiwanych przeglądarkach.

Jedyna istotna różnica w działaniu tego kodu polyfill polega na tym, że zamiast wysyłać ładunek, gdy użytkownik opuszcza stronę, wysyła go, gdy visibilityState strony zmienia się na „ukryty”, ponieważ jest to najbardziej niezawodny sygnał zakończenia sesji, który jest obecnie powszechnie dostępny.

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