GA4とGTMでA/Bテストする

## はじめに

[mediba Advent Calender 2024](https://qiita.com/advent-calendar/2024/mediba)の19日目の記事になります。

今回は、Ruby on Railsの学習用に作成したECサイトにて、GA4とGTMを利用してA/Bテストを行う方法について書いていきます。

## 背景

2023年9月のGoogle Optimizeのサービス終了に伴い、無償で気軽にA/Bテストを実施できなくなったという方が多いのではないでしょうか。（私もその一人です。）

https://support.google.com/analytics/answer/12979939?hl=ja

現に、GoogleからGA連携可能なA/Bテストツールのアナウンスがありましたが、すべて有償となっています。（2024/10/16現在）

- AB Tasty
- Optimizely
- VWO

そのため、自前でA/Bテストを行う方法を検討することにしました。

## アウトプット

GTMのカスタムHTMLタグ（JavaScript）を利用し、商品一覧画面の「Add to Cart」のボタンテキストをランダムに50%の確率でオリジナルとテストパターンのどちらかに変更し表示させることができました。

![ab_test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/58875feb-bcfc-ecd7-ba79-50729624f249.png)

また、以下のアクセスログをGA4に送信し、A/Bテストの結果をBigQueryで集計することができました。（GA4の探索レポートでも集計可能です）

- A/Bテストのパターンごとのページビューイベント
- A/Bテストのパターンごとの対象箇所のクリックイベント

集計した結果は、以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/713a0a02-ef97-e1b0-b99f-89a5ee29a2b1.png)

## A/Bテスト対象のページ

A/Bテスト対象のページとして、Ruby on Railsの学習用に作成したECサイトを利用します。

なお、A/Bテストはユーザーに対して「Add to Cart」のボタンテキストをランダムに50%の確率で以下の内容に変更し表示させることとします。

- オリジナル：「Add to Cart」ボタン（既存のボタンテキスト）
- テストパターン：「カートへ追加」ボタン（ボタンテキストを変更）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/c904521f-5b37-4a24-4ddc-d27a78b2bd2a.png)

補足となりますが、ECサイトの環境は以下の通りです。
    
-  Ruby 3.2.1
-  Rails 7.0.6
-  Bootstrap 5.2.3
-  Active Storage（AWS S3）
-  Action Mailer
-  Docker
-  Heroku

## 作業手順

A/Bテストを実施するための作業手順は、次の通りです。

1. GTMで必要なタグを公開する
1. A/Bテストの表示を確認する
1. GA4ログの疎通を確認する
1. A/Bテストの結果を集計する

## GTMで必要なタグを公開する

### ga4ClickEvent.js

以下のスクリプトをカスタムHTMLタグで配信し、GA4のクリックイベント送信関数を定義します。

後述のab_test.jsで、テスト対象箇所のaタグのonclick属性の値に本関数をappendします。

本関数が実行された（クリックされた）タイミングでカスタムイベントトリガーが発動し、GTMのGA4イベントタグ（クリックイベントタグ）が発火する設定とします。

``` html
<script>
    function ga4ClickEvent(category, action, label) {
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({
            'event': 'ga4ClickEvent',
            'event_category': category,
            'event_action': action,
            'event_label': label,
        });
    }
</script>
```

### GA4のクリックイベントタグ

前述の```ga4ClickEvent```関数が実行されたら（ボタンがクリックされたら）、発火するGA4のイベントタグ（イベント名：```click```）を設定します。

なお、本イベントの以下イベントパラメータには、先ほどの```ga4ClickEvent```関数内の
dataLayer変数の値を設定します。

- event_category
- event_action
- event_label

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/35645cd5-ff72-ad50-e7b2-edf325cbbc99.png)

トリガーは、カスタムイベントトリガー（イベント名：```ga4ClickEvent```）を設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/7c40e6c2-e708-a90c-a82a-19d762e87687.png)


### ab_test.js

A/Bテストを行うための以下スクリプトをGTMのカスタムHTMLタグで配信します。

簡潔にまとめると、スクリプト内では以下の処理を行なっています。

- ブラウザのCookie「pattern」の値としてランダムに0と1を振り分けて、A/Bテストを実施
    - Cookie「pattern」の値が「0」の場合
        - オリジナル「Add to Cart」ボタンを表示
    - Cookie「pattern」の値が「1」の場合
        - テストパターン「カートへ追加」ボタンを表示

クリックイベント送信関数がボタンのaタグのonclick属性の値としてappendされ、ユーザーがクリックしたタイミングで、クリックイベントがGA4に送信されます。

なお、どのパターンがクリックされたかを判別するために```ga4ClickEvent```関数の第三引数に、```0```(オリジナル)または```1```（テストパターン）の値を設定します。

``` html
<script>
    (function() {
        function getCookieValue(name) {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = cookies[i].trim().split('=');
                if (cookie[0] === name) {
                    return cookie[1];
                }
            }
            return null;
        }

        function setButton(element, text, patternValue) {
            element.textContent = text;
            element.setAttribute('onclick', 'ga4ClickEvent("商品一覧", "クリック", "' + patternValue + '")');
        }

        function updateButtons() {
            var elements = document.getElementsByClassName('gtm-click');
            var pattern = getCookieValue("pattern");

            for (var i = 0; i < elements.length; i++) {
                if (!pattern) {
                    pattern = Math.random() > 0.5 ? '0' : '1';
                    document.cookie = "pattern=" + pattern + "; expires=expireDate; domain=domainName";
                }

                if (pattern === '0') {
                    setButton(elements[i], "Add to Cart", "0");
                } else {
                    setButton(elements[i], "カートへ追加", "1");
                }
            }
        }
      
        document.addEventListener("DOMContentLoaded", updateButtons);
    })();
</script>

```

### GA4のページビュータグ（既存）

既存のGA4のページビュータグのイベントパラメータに、どちらのパターンが表示されたかという情報をもたせておきます。

以下のような形で、イベントパラメータ「pattern」の値に前述のab_test.jsで作成したファーストパーティーCookie「pattern」の値を設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/ba518d8a-8b25-7d5e-4043-19042d9f5d87.png)

以上のGTMタグの設定を公開し、本作業は完了となります。

## A/Bテストの表示確認を行う

実際にページにアクセスすると、ブラウザごとにランダムにオリジナルとテストパターンが表示されました。

![ab_test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/58875feb-bcfc-ecd7-ba79-50729624f249.png)

## GA4ログの疎通を確認する

次に、ChromeのNetworkパネルでGA4のページビュー/クリックイベントが想定通りに送信されているか確認します。

### ページビューイベント

既存のページビューイベントのイベントパラメータとして、```pattern```が追加されました。

値には、テストパターンが表示されたことを示す```1```が入っています。オリジナルが表示された場合は値に```0```が入ります。

![page_view.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/56e64c77-b568-658d-5604-defc4878acc7.png)

### クリックイベント

ボタンがクリックされた際に、クリックイベントの送信が確認できました。

イベントパラメータ```event_label```の値として、テストパターンがクリックされたことを示す```1```が入っています。オリジナルがクリックされた場合は```0```が入ります。

![click.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/5ccb1298-e456-ed13-03d1-6dfbcb10f47f.png)

## A/Bテストの結果を集計する

以下の通り、BigQueryのGA4テーブルからA/Bテスト結果の集計をすることができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/1b7594b6-878c-5839-1d25-00e8e94cd010.png)

結果の集計に利用したSQLは、以下の通りです。

``` SQL
CREATE TEMPORARY FUNCTION get_ga4_value(params ANY TYPE,name STRING) AS (
  ( 
    SELECT
      COALESCE( 
        value.string_value,
        cast(value.int_value AS string),
        cast(value.float_value AS string),
        cast(value.double_value AS string) 
      ) 
    FROM
      UNNEST(params) AS x 
    WHERE 
      x.key = name 
  )
);

WITH raw_data AS (
  SELECT
    event_name,
    get_ga4_value(event_params, 'pattern') AS pattern,
    get_ga4_value(event_params, 'event_category') AS event_category,
    get_ga4_value(event_params, 'event_action') AS event_action,
    get_ga4_value(event_params, 'event_label') AS event_label,
  FROM
  projectId.analytics_{propertyId}.events_*
  WHERE
    _TABLE_SUFFIX BETWEEN "yyyymmdd" AND "yyyymmdd"
),

page_view_count AS (
  SELECT
    pattern,
    COUNT(event_name) AS page_view_count
  FROM
    raw_data
  WHERE
    event_name = "page_view"
  GROUP BY
    pattern
  ORDER BY
    pattern ASC
),

click_count AS (
  SELECT
    event_label AS pattern,
    COUNT(event_name) AS click_count
  FROM
    raw_data
  WHERE
    event_name = "click"
    AND event_category = "商品一覧"
    AND event_action = "クリック"
  GROUP BY
    pattern
  ORDER BY
    pattern ASC
)

SELECT
  page_view_count.pattern,
  page_view_count.page_view_count,
  click_count.click_count,
  ROUND((CAST(click_count.click_count AS NUMERIC) / CAST(page_view_count.page_view_count AS NUMERIC)) * 100, 2) AS click_rate
FROM
  page_view_count
INNER JOIN
  click_count
USING(pattern)
ORDER BY
  pattern ASC
  ;
```

※イベントパラメータの値の取り出しには、ユーザー定義関数（UDF）```get_ga4_value```を利用しています。

## カイ二乗検定で有意差検定を行う

最後に、カイ二乗検定でオリジナルとテストパターンのCTRに差があったか判断します。

詳細は割愛しますが、以下のようにスプレッドシート上で```CHISQ.TEST```という関数を利用すれば簡単に実施できるので、ぜひ試してみてください。（今回はサンプルサイズが少なすぎますね。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/6574e416-d38f-3c9c-0a84-c67eecfc78df.png)

参考：[そのABテスト意味ないかも！カイ二乗検定で「優位性」を判断する方法をわかりやすく解説│デジマログ](https://www.ecfs.jp/eckaizenlab/abtest-chi-squared-test/)

## おわりに

GA4とGTMを利用して、自前でA/Bテストを行う方法について解説しました。

有償版のA/Bテストツールの導入障壁が高いと感じている方は、本記事を参考にA/Bテストを実施してみてはいかがでしょうか。

どなたかの参考になれば、幸いです。