今回はページ遷移や離脱時であっても、漏れなくデータをサーバに送信するための Beacon API の紹介をします。弊社の ferret One というプロダクトでは Google Analytics のようなアクセス解析機能を提供しているため、なるべく漏れなく解析データを受け取りたいということで調査したサマリです。

漏れる or 遷移をブロックする例

まずはあまり良くない実装ケースを紹介します。

以下の実装は fetch を使って非同期に送信しています。a タグのページ遷移が遅かったり、ログ送信が高速にされれば成功しますが、ページ遷移の方が速かった場合は、 fetch のログ送信は実行されません。

<ul>
    <li><a href="https://google.com/" data-event="async-event">async event track</a></li>
</ul>
<script>
    /**
     * 非同期で送信 (fetch)
     * @return {Promise<Response>}
     */
    function sendAsyncEvent() {
      return fetch('https://example.com/tracker?event=async-event');
    }
    const asyncEventElement = document.querySelector('[data-event="async-event"]');
    asyncEventElement.addEventListener('click', () => {
      sendAsyncEvent();
    });
</script>

次は同期的に送信する実装です。 fetch の Promise を使って手動でページ遷移をしたり XMLHttpRequest の同期オプションを使って処理を待たせたりしています。これであれば確実にログ送信されますが、ページ遷移をブロックしてしまうのでユーザ体験が良くありません。

<ul>
    <li><a href="https://google.com/" data-event="sync-event">sync event track (fetch)</a></li>
    <li><a href="https://google.com/" data-event="xhr-sync-event">sync event track (XHR)</a></li>
</ul>
<script>

    /**
     * 同期で送信 (fetch)
     * @return {Promise<Response>}
     */
    function sendSyncEvent() {
      return fetch('https://example.com/tracker?event=sync-event');
    }
    const syncEventElement = document.querySelector('[data-event="sync-event"]');
    syncEventElement.addEventListener('click', (e) => {
      e.preventDefault();
      sendSyncEvent().then(() => {
        location.href = e.target.getAttribute('href');
      });
    });

    /**
     * 同期で送信 (XHR)
     */
    function xhrSyncEvent() {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', 'https://example.com/tracker?event=xhr-sync-event', false);
      xhr.send();
    }
    const xhrEventElement = document.querySelector('[data-event="xhr-sync-event"]');
    xhrEventElement.addEventListener('click', () => {
      xhrSyncEvent();
    });
</script>

Google Analytics の解決方法

上記のような状況は Google Analytics でも起こりえます。そこで Google Analytics は下記のようなオプションを提供しています。

function handleOutboundLinkClicks(event) {
  ga('send', 'event', {
    eventCategory: 'Outbound Link',
    eventAction: 'click',
    eventLabel: event.target.href,
    transport: 'beacon' // Beacon API を使用
  });
}

この transport: 'beacon' オプションを利用すると、Beacon API を使って漏れなくログの送信をしてくれます。

Beacon API とは

Beacon API とはブラウザがページ遷移などに関係なく非同期でサーバに送信してくれます。以下のような特徴があります。

  • 非同期に送信される
  • ページの遷移をブロックしない
  • CORS 気にする必要がない
  • POST で送信される
  • ブラウザが閉じられても、次に起動した時に送信される

特に一番最後のページ遷移中やデータ送信前にブラウザを終了しても、次に起動した時にデータを送信してくれるというのは嬉しいですね。

Beacon API の使い方

Beacon API を使うのは非常に簡単です。

navigator.sendBeacon(`URL`, payload);

これだけで非同期にデータを送信することができます。

text/plain で送る

JSON や単純な文字列を送りたい場合は、普通に文字列を sendBeacon に渡すと Content-type: text/plain で送信されます。受け取るサーバ側でよしなに処理をしましょう。

const data = JSON.stringify({key: 'value'});
navigator.sendBeacon('https://example.com/tracker', data);

multipart/form-data で送る

通常の POST データで送りたい場合は FormData でデータを生成すれば Content-type: multipart/form-data で送信されます。

const data = new FormData();
data.append('key', 'value');
navigator.sendBeacon('https://example.com/tracker', data);

送信データのサイズ制限

sendBeacon で送信できるデータサイズは最大で 64KB です(ただしブラウザの実装に依存する)。基本は軽量のデータをやり取りする目的で作られているので実用上は問題ないと思います。

検証コード
const data = new Array(64 * 1024 + 1).join('0');
const result = navigator.sendBeacon('https://example.com/tracker', data);
console.log(`sendBeacon result: ${result}`);

最初のよくない例を Beacon API で書き直すとこうなります。シンプルですね。

<ul>
    <li><a href="https://google.com/" data-event="beacon-event">beacon event track</a></li>
</ul>
<script>
    /**
     * beacon で送信
     */
    const beaconEventElement = document.querySelector('[data-event="beacon-event"]');
    beaconEventElement.addEventListener('click', () => {
      const data = JSON.stringify({event: 'beacon-event'});
      // or
      // const data = new FormData();
      // data.append('event', 'beacon-event');
      navigator.sendBeacon('https://example.com/tracker', data);
    });
</script>

以上が Beacon API の特徴と使い方の紹介でした。

最後に…主要なモダンブラウザは全て対応をしていますが IE は対応をしていません…。polyfill はありますが、実装としては XHR の同期オプションを使って解決をしています。ここら辺はアプリケーションの要件に合わせて選択をすると良いでしょう。