Misura in modo affidabile l'analisi e la telemetria dell'intera sessione

Per monitorare in modo affidabile i dati di analisi, Core Web Vitals e telemetria che coprono l'intera visita di un utente a una pagina web (non solo fino al caricamento della pagina), utilizza l'API fetchLater().

L'API fetchLater() è il modo più affidabile per inviare dati a un server nei casi in cui non è richiesta una risposta e i tempi di consegna non sono urgenti, come nel caso di dati quali analisi degli utenti, telemetria, monitoraggio degli errori e metriche sul rendimento come Core Web Vitals.

Le tecniche meno recenti, come la creazione di un pixel <img> in un listener di eventi unload, sono notoriamente inaffidabili (soprattutto sui dispositivi mobili) e possono influire negativamente sulle prestazioni (rendendo le pagine non idonee per bfcache).

Come implementare

  1. Pianifica la richiesta: non appena sono disponibili dati pertinenti, chiama fetchLater() con il payload dei dati. In questo modo, i dati vengono messi in coda per essere inviati in un secondo momento.

  2. Aggiorna il payload in base alle esigenze: se l'utente genera più dati o lo stato cambia, interrompi la richiesta pianificata in precedenza e chiama di nuovo fetchLater() con lo snapshot completamente aggiornato.

  3. Lascia che il browser gestisca il resto: quando l'utente esce dalla pagina o chiude la scheda, il browser invierà in modo affidabile il payload della chiamata fetchLater() più recente.

Esempio di codice

Questo codice misura la durata della sessione della visita di un utente a una pagina utilizzando fetchLater() per mettere in coda un nuovo beacon ogni 10 secondi con la durata della sessione aggiornata.

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

Best practice

  • SÌ: utilizza fetchLater() per inviare dati a un server in qualsiasi situazione in cui non è necessaria una risposta e non è fondamentale che i dati vengano inviati immediatamente.
  • SÌ: utilizza un AbortController per annullare il recupero in attesa nei casi in cui potrebbe essere necessario aggiornare i dati prima che l'utente abbandoni la pagina
  • SÌ: riduci al minimo le dimensioni del payload per evitare di superare la quota (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

Se fetchLater() non soddisfa il target Baseline, è 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.

La strategia di riserva consigliata è utilizzare il polyfill riportato di seguito, che gestisce internamente i browser non supportati utilizzando fetch() con keepalive o navigator.sendBeacon(). Il tuo codice DEVE chiamare direttamente fetchLater(). Non chiamare mai fetch(), sendBeacon() o altre API beacon.

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 utilizza visibilitychange per rilevare quando l'utente abbandona la pagina, anziché fare affidamento sulla gestione nativa dell'unload del browser. Questo è un dettaglio di implementazione interno: il tuo codice non deve ascoltare visibilitychange o altri eventi del ciclo di vita della pagina. Basta chiamare fetchLater() e il polyfill gestisce la pubblicazione.

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