フルセッションの分析とテレメトリーを確実に測定

ユーザーがウェブページにアクセスしている間(ページ読み込み時だけでなく)の分析データ、Core Web Vitals、テレメトリー データを確実にトラッキングするには、fetchLater() API を使用します。

fetchLater() API は、レスポンスが不要で、配信タイミングが緊急でない場合に、サーバーにデータを送信する最も信頼性の高い方法です。これは、ユーザー分析、テレメトリー、エラー トラッキング、Core Web Vitals などのパフォーマンス指標などのデータに適用されます。

unload イベント リスナーで <img> ピクセルを作成するなどの古い手法は、信頼性が低いことで知られており(特にモバイルの場合)、パフォーマンスに悪影響を及ぼす可能性があります(ページが 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 KB)を超えないように、ペイロード サイズを最小限に抑えます。
  • fetchLater() の呼び出しを try/catch でラップして、割り当てエラーを処理する。
  • globalThis で fetchLater() の存在を検出する機能を実装し、API をサポートしていないブラウザ用のフォールバック戦略を実装します。
  • リクエスト本文に ReadableStream オブジェクトを使用しないでください。エラーが発生します。

ブラウザのサポートとフォールバック戦略

fetchLater() がベースライン目標を達成していない場合は、フォールバック戦略が必要です。ただし、この API の信頼性とパフォーマンスの向上を考慮すると、ブラウザがサポートしている場合は fetchLater() を使用する必要があります。

推奨されるフォールバック戦略は、以下のポリフィルを使用することです。このポリフィルは、keepalive または navigator.sendBeacon() で fetch() を使用して、サポートされていないブラウザを内部的に処理します。独自のコードで 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;
    },
  };
};