🏷️

GTMで非同期に外部サービス埋め込みとデータ連携を行う

2023/12/19に公開

この記事はエアークローゼットのアドベントカレンダー202319日目の記事になります。

みなさま、こんにちは!
株式会社エアークローゼットでエンジニアをやっている大西です。

みなさま、GTM(Google Tag Manager)は活用されてますでしょうか?
弊社ではCV計測やイベント計測、ABテスト等に使っていますが、うまく使えば外部サービスをGTM経由で埋め込むことにも利用できます。
今回は先日開発したGTMからユーザーデータ連携が必要な外部チャットサービスを埋め込む処理について書いていきたいと思います。

GTMのデータ連携について

GTMとデータ連携の仕組み

「GTM dataLayer」と検索すると解説記事がたくさん出てくるので細かな説明は割愛しますが、以下のようなコードでdataLayerなるグローバル変数に決められた形のオブジェクトをpushするとデータが連携されます。

export const sendGTMData = (event: string, data: Record<string, string>) => {
  const key = setInterval(() => {
    if (typeof window.dataLayer !== 'undefined') {
      window.dataLayer.push(params);
      clearInterval(key);
    }
  }, 300);
};

@cheez921(ちーず)様の記事が分かりやすかったので添付しておきます。
https://qiita.com/cheez921/items/7efb432380696b8a881f

GTMで埋め込んだ外部サービスでデータ連携を行う方法

GTM側に同期的なデータ連携する方法としては、GTMのdataLayerにpushしたイベントで発火するカスタムイベントをトリガーとしてscriptタグを挿入する方法が一般的です。
言葉だけではわからないと思うので、シーケンス図に起こしました。

カスタムイベントをトリガーとしてscriptタグを挿入&実行

トリガーの設定はGTMのカスタムイベントを利用できるので、イベントのpushだけ行えば残りはGTM内の設定だけで完結できます。
とても便利ですよね!

dataLayerにpushしたイベントで発火するカスタムイベントの問題点

先ほど紹介したカスタムイベントをトリガーとしてscriptタグを挿入する方法ですが明確な欠点があります。
それは、Web、API側で問題が発生してeventが発火されなかった場合に、外部サービス自体が埋め込まれないことです。

カスタムイベント_失敗_シーケンス図

今回埋め込む外部サービスはお問合せにかかるチャットサービスということもあり、データの取得に関わらず、埋め込みは常に行いたいという要件がありました。
すなわち、データ連携はオプショナルで、チャット自体は常に機能して欲しいということです。
(もちろん、タグが埋め込まれなかった時の対策は別で考えています。)

そこで今回は、dataLayerにイベントがpushされたことをGTMで挿入するscriptタグ内で検知してデータ連携を実施する方法を取りました。
実際にやっているのは、グローバル変数であるdataLayerにデータが突っ込まれているかを見ているだけです。

scriptタグの挿入はページビューなどのページを開いた瞬間に発火するトリガーで行う想定です。

シーケンス図で書くと以下の通りです。

dataLayerにイベントがpushされたことを検知してデータ連携を実施

こちらの方法であれば、仮にWeb,API側でデータ取得に失敗しても、外部サービスのコード自体は挿入されるため、そのような場合でも外部サービスを使ったお問合せが可能となります。

dataLayerにイベントがpushされたことを検知してデータ連携を実施_失敗_シーケンス図

実装

Web

まずはweb側の実装です。
と言ってもAPIにGET処理を投げて、返ってきたらdataLayerにイベントを入れているだけです。

import axios from 'axios';
const api = axios.create(opt);

const sendGTMUserInfo = (
  userInfo?: Recode<string, string>,
) => {
  const key = setInterval(() => {
    if (typeof window.dataLayer !== 'undefined') {
      window.dataLayer.push({
        event: 'userInfo',
        ...{userInfo || {}},
      });
      clearInterval(key);
    }
  }, 300);
};

export const fetchUserInfo = () => (
  api
    .get('/user/info')
    .then(response => {
      if (response.user) {
        const userInfo = response.user;
        sendGTMUserInfo(userInfo);
      }
    })
    .catch(error => {
      // data連携に失敗してもイベントを送ることでタイムアウトを待たずにチャットを起動する
      sendGTMUserInfo();
    });
);

GTM

続いてGTM側の実装です。
トリガーはAll Pagesのページビューです。
タグの実装は以下の通りです。

GTMはES2015以降の構文に対応しておらず、const宣言やarrow functionは使えないため、var宣言等々ES2015より前の少し古い書き方になっています。

<script type="text/javascript">
  (
    function () {
      var WAIT_INTERVAL_MS = 500; // イベント検知を行うスパン(0.5秒)
      var MAX_WAIT_MS = 10 * 1000; // イベント検知を行う上限時間。この時間を超えるとデータ連携なしで外部ツールを起動する(10秒)

      var boot = function (userInfo) {
        var _bootChatService = function (profile) {
          // 外部サービスを立ち上げる処理
        };

        var profile = {
          'name': (userInfo.first_name || '') + (userInfo.last_name || ''),
          'email': userInfo.email || '',
        };
        _bootChatService(profile);
      }

      var getUserInfo = function () {
        for (var i = 0; i < dataLayer.length; i++) {
	  // dataLayerには複数のイベントが配列で入っている
          var data = dataLayer[i];
          if (data.event === 'userInfo') {
            return data;
          }
        }
        return null;
      }

      var waitLinkUserInfoAndBoot = function () {
        var totalWaitTimeMS = 0;
        var waitDataLinkIntevalId = setInterval(function () {
          if (totalWaitTimeMS > MAX_WAIT_MS) {
	    // データ連携に失敗した場合は、ユーザー情報なしで外部サービスを起動する
            console.warn('Time out waiting data link from acm-web');
            boot();
            clearInterval(waitDataLinkIntevalId);
            waitDataLinkIntevalId = null;
          }

          var userInfo = getUserInfo();
          if (userInfo) {
            boot(userInfo);
            clearInterval(waitDataLinkIntevalId);
            waitDataLinkIntevalId = null;
          }

          totalWaitTimeMS += WAIT_INTERVAL_MS;
        }, WAIT_INTERVAL_MS);
      }

      // ページを開いたときにWebからデータ連携されるのを待って外部サービスを起動する。
      waitLinkUserInfoAndBoot();
  )();
</script>

さいごに

この記事ではGTMで同期的なデータ連携を含む場合の実装を紹介しました。
今回は若干特殊な要件を満たすために変わった実装を行ったので、備忘録も兼ねて記事にしました。
みなさまの助けになれば幸いです。

エアークローゼットのアドベントカレンダー2023はまだまだ続きますので、ぜひ他のエンジニア、デザイナー、PMの記事もご覧いただければと思います。

また、エアークローゼットはエンジニア採用活動も行っておりますので、興味のある方はぜひご覧ください!
https://corp.air-closet.com/recruiting/developers/

https://www.wantedly.com/companies/airCloset/projects

Discussion