Debounce e raggruppamento di più eventi di analisi

La maggior parte dei dati di analisi, telemetria e Core Web Vitals ha una priorità inferiore rispetto alla risposta all'utente, quindi puoi rimandare l'invio fino a quando l'utente non lascia la pagina. L'eccezione a questa regola è se devi fornire aggiornamenti in tempo reale, nel qual caso la tempestività è importante.

Il modo ottimale per fornire aggiornamenti di analisi in tempo reale, riducendo al minimo i beacon, è utilizzare fetchLater() con l'opzione di configurazione activateAfter. In questo modo, puoi raggruppare in modo efficace tutti gli eventi di analisi che si verificano in un determinato intervallo di tempo in un unico beacon, che viene inviato in modo affidabile anche se l'utente lascia la pagina prima della scadenza del timeout.

Come implementare

  1. Pianifica la richiesta: non appena sono disponibili dati di analisi pertinenti, chiama fetchLater() con il payload dei dati e passa un valore activateAfter. In questo modo, i dati vengono messi in coda per essere inviati dopo questo periodo di tempo o se l'utente abbandona la pagina prima.

  2. Raggruppa più eventi: se si verifica un nuovo evento di analisi prima della scadenza del timeout activateAfter, interrompi la richiesta pianificata in precedenza e chiama di nuovo fetchLater() (con lo stesso valore activateAfter) con la coda di eventi completa in un unico payload.

  3. Reimposta la coda degli eventi alla scadenza del timeout: se si verifica un nuovo evento di analisi dopo che l'invio del beacon pianificato è andato a buon fine (ovvero il valore activated del risultato fetchLater() è true), reimposta la coda degli eventi.

  4. Lascia che il browser gestisca il resto: se l'utente esce dalla pagina o chiude la scheda prima della scadenza del timeout di activateAfter, il browser invierà comunque in modo affidabile il payload della chiamata fetchLater() più recente.

Esempio di codice

Questo codice monitora tutti gli eventi load e click su una pagina e raggruppa tutti gli eventi che si verificano entro un timeout di 10 secondi.

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

Best practice

  • SÌ: utilizza fetchLater() con l'opzione activateAfter impostata per raggruppare più eventi di analisi che si verificano a breve distanza l'uno dall'altro.
  • SÌ: utilizza un AbortController per annullare il recupero in attesa nei casi in cui si verifica un nuovo evento di analisi.
  • SÌ: verifica di non raggruppare troppi eventi insieme e di non superare la quota di fetchLater() (attualmente circa 64 kB per origine).
  • SÌ: inserisci le chiamate a fetchLater() in un blocco try/catch per gestire gli errori di quota.
  • SÌ: esegui il rilevamento delle funzionalità per verificare la presenza di fetchLater() su globalThis e implementa una strategia di riserva per i browser che non supportano l'API.
  • NON utilizzare un oggetto ReadableStream per il corpo della richiesta, in quanto ciò genererà un errore.

Supporto del browser e strategie di riserva

Pertanto, in genere è necessaria una strategia di riserva.

Tuttavia, dati i vantaggi in termini di affidabilità e prestazioni di questa API, è consigliabile utilizzare fetchLater() se il browser la supporta.

fetchLater() polyfill

Utilizza il seguente polyfill fetchLater() minimo, che implementa l'API nel modo più fedele possibile nei browser non supportati.

L'unica differenza di comportamento degna di nota con questo polyfill è che, anziché inviare il payload quando l'utente abbandona la pagina, lo invia ogni volta che la visibilityState della pagina diventa "hidden", poiché questo è l'indicatore di fine sessione più affidabile attualmente disponibile.

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