SPAでGA4のスクロールイベントを計測する方法

GA4のscrollイベント計測を実際のページ移動を伴わないSPAの画面切替があるページにも対応させる設定方法。

Google Analytics 4（GA4）は、[拡張イベント計測機能](https://support.google.com/analytics/answer/9216061)により、デフォルトでWebページのスクロール計測に対応していますが、以下の問題があります。

- 90%スクロール時にしか計測できない
- SPA（シングルページアプリケーション）など実際のページ移動を伴わない画面切替に対応して、再度スクロール計測を再開することができない

本記事で紹介するコードをGoogle Tag Manager(GTM)で使ってスクロール計測設定を行うことでこれら2つの問題を解決できます。

## 設定方法
既にGA4の設定タグがGTM設定内に存在している状態で以下の設定を作成します。

### dataLayer変数設定「dataLayer - customScrollThreshold」
後述のカスタムHTMLタグ「custom - scroll dataLayer.push for SPA」がdataLayer.pushするカスタムイベント`customScroll`が持っている**達成したスクロール目標の値**を取得するための設定です。

変数タイプ : データレイヤーの変数 / データレイヤーの変数名 : `customScrollThreshold`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/808afbd8-41d2-e16e-5ab9-0e6c7a29a21f.png)


### カスタムイベントトリガー設定「Event - customScroll」
後述のカスタムHTMLタグ「custom - scroll dataLayer.push for SPA」がdataLayer.pushするカスタムイベント`customScroll`に反応するトリガー設定です。

トリガータイプ : カスタムイベント / イベント名 : `customScroll`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/85313b0c-b463-c91a-4fb9-222a8370a187.png)

### カスタムHTMLタグ「custom - scroll dataLayer.push for SPA」
スクロールを監視してカスタムイベント`customScroll`をdataLayer.pushするタグ設定です。このタグ設定にはGA4のpage_viewイベントを計測するためのタグ設定（多くの場合「GA4設定タグ」）と同じトリガー設定を紐づけます。必要に応じてコード内の以下の変数を変更してください。

- `TARGETS` : GA4のscrollイベントタグを発火させたい目標スクロール率をカンマ区切りの数値で入力してください。
- `STABLE_MS` : SPAの作りによってページ更新をGTMで検知できるタイミングと描画完了タイミングが一致しないことに対応するための機能です。本コードはタグが呼び出されてからここで指定したミリ秒の間ページの長さが変わらなかったことを確認した後にスクロール検知を開始します。計測対象に合わせて調整してください。不要なときは0を指定してください。

#### HTMLコード

```html:
<script>
(function(){
  var TARGETS = [50, 70, 90]; // スクロール率の目標値を指定
  var STABLE_MS = 1000; // 計測開始するために必要なページの長さ安定時間を指定

  var targets = TARGETS.filter(function(a){return a >= 0 && a <= 100;}).sort(function(a,b){return a - b;});
  var currentIndex = 0;

  // 既にこのコードによるスクロール計測が行われている場合は既存のイベントリスナーを削除
  if(typeof window.gtmCustomHandleScroll !== 'undefined'){
    window.removeEventListener('scroll', gtmCustomHandleScroll);
  }
  
  function initHandleScroll() {
    currentIndex = 0;
    executeAfterStablePageLength(function(){
      window.addEventListener('scroll', gtmCustomHandleScroll);
    },STABLE_MS);
  }

  window.gtmCustomHandleScroll = function() {
    // ウィンドウ全体の高さと、現在のスクロール位置から、現在のスクロール率を計算する
    var scrollHeight = document.body.scrollHeight - window.innerHeight;
    var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    var scrollPercentage = (scrollTop / scrollHeight) * 100;

    // 現在のスクロール率が次の目標値を超えた場合は、カスタムイベント「customScroll」をdataLayer.pushする
    var target = targets[currentIndex];
    if (scrollPercentage >= target) {
      dataLayer.push({
        'event': 'customScroll',
        'customScrollThreshold': target,
        'customScrollUnits': "percent",
        'customScrollDirection': "vertical",
      });
      currentIndex++;
    }

    // 全ての目標値でdataLayer.pushの出力を終えた場合は、scrollイベントリスナーを削除
    if (currentIndex === targets.length) {
      window.removeEventListener("scroll", gtmCustomHandleScroll);
    }
  }

  // 指定したミリ秒ページの長さが変わらないことを確認後コールバック関数を実行する
  function executeAfterStablePageLength(callback, duration) {
    // 0.2秒ごとにページの長さをチェック
    var detectionRate = 200;
    var timer = 0;
    duration = duration || 0;
    var previousPageLength = document.body.offsetHeight;
    function func() {
      var currentPageLength = document.body.offsetHeight;
      if (currentPageLength === previousPageLength) {
        timer += detectionRate;
      }else{
        timer = 0;
      }
      if(duration <= timer){
        callback();
      }else{
        previousPageLength = currentPageLength;
        setTimeout(func, detectionRate);
      }
    }
    if(duration <= 0){
      callback();
    }else{
      setTimeout(func, detectionRate);
    }
  }
  
  // History APIのpushStateまたはreplaceStateが呼び出されたとき自動でスクロール計測を最初から再開(不要ならこの行を削除)
  window.addEventListener('popstate', initHandleScroll);
  
  initHandleScroll();
})();
</script>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/32a2dc52-611b-f1b5-78d3-02a6650b5436.png)


### GA4イベント計測タグ設定「GA4 for web - scroll」
- イベント名 : `scroll`
- イベントパラメータ `percent_scrolled` : `{{dataLayer - customScrollThreshold}}`
- トリガー設定 : `Event - customScroll`

:::note warn
2023-06-17 追記
Googleが新しく公開したヘルプ記事により、 `scroll` の名前を使ったイベントを手動で作成することが望ましくない可能性が出てきました。現在のところこの実装によるトラブルは確認できていませんが、`scroll_custom` のようなカスタムイベント名を使用した方が安全です。

> ウェブ
> **予約済みのイベント名**
> 以下のイベント名は予約済みのため使用できません。
> (中略)
> - scroll

[GA4] イベントの命名規則 - アナリティクス ヘルプ
https://support.google.com/analytics/answer/13316687?sjid=12422469411440258592-AP
:::


※ パラメータの変数とトリガー設定は先ほど作成したものを使用する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/97e3f6b3-a0b2-190c-6529-48eb0072687f.png)

ここまでの設定をGTMで行い問題なく計測できることをGTMプレビューモードとGA4のDebugViewで確認した後、設定を公開します。

この設定を行ったときは役割が被るGA4の拡張計測機能によるscrollイベント計測はオフ状態に変更します。

#### 拡張計測機能によるscrollイベント計測のオフ
GA4プロパティの管理 > データストリーム > 計測対象のウェブデータストリームを選択 > 拡張計測機能 の歯車アイコンをクリック > 「スクロール数」のスイッチをオフにして「保存」をクリック

:::note warn
2023-06-17 追記
 GTM設定で`scroll` ではなく、`scroll_custom` のようなカスタムイベント名を使用した場合にこの対応は不要です。

:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/2905b467-3929-225a-54d2-e8a89b3938c4.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/f6639e07-62ab-ed76-0ce5-7d219e44a658.png)


以上の設定でGA4のscrollイベントをSPAにも対応する形で計測できます。SPAはサイトの作りによって挙動が大きく異なるため、うまくいかない場合は本コードをJavaScriptが分かる方に調整してもらって実装にトライしてみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/4fe94dbb-8f2e-b22b-da3f-e6a005d505b1.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/c8b25a67-04a5-b37d-752f-df63050f988b.png)