Next.js に GoogleAnalytics と GoogleTagManager を導入し計測する

Next.js でアプリケーションを作った際に、アクセス解析などやりたく GoogleAnalytics を導入しました。GoogleAnalytics をそのまま使うのではなく GoogleTagManager を経由すると何かと便利なので、Next.js → GoogleTagManager → GoogleAnalytics という流れで計測を行えるようにしました。

現状、インターネットにはエンジニア目線で GoogleAnalytics や GoogleTagManager について書かれた記事が少なく、これを書くことで何かの助けになると感じましたので、ここに記録しておきます。

ところで GoogleTagManager は長いので以下 GTM とします。また GoogleAnalytics も長いので以下 GA とします。

GoogleTagManager を設定する

アカウントとコンテナを用意する

https://tagmanager.google.com/

まずは GTM のアカウントを用意します。タグマネージャーアカウントという名称です。サービスと対応するのがタグマネージャーアカウントと思えば良いです。「ウェブサービスAのために、アカウントを1つ作る」という具合です。

用意したら、タグマネージャーアカウント内にコンテナというものを1つ作成しましょう。タグマネージャーアカウントは複数作成することができ、またタグマネージャーアカウント内には複数のコンテナを持つことができます。コンテナはサイトやモバイルアプリケーションごとに1つずつ作られるのが一般的とされています。

例えば自分が関わったプロダクトでは、プロダクトごとにアカウントを作っています。適当に置き換えて例を上げると「サービスA用のアカウント」「サービスB用のアカウント」「サービスC用のアカウント」といった具合です。それぞれのアカウントの中で、必ず一つ以上のコンテナがあります。コンテナは Web アプリ、Android アプリ、iOS アプリで分けていたりします。

ここではとりあえず1つのアカウントと、その中にコンテナが1つだけ用意されているという例で話を進めます。今回サンプルとして作った GTM では、テストと言う名前のアカウントを作り、その中に www.example.com というコンテナを用意しています。 f:id:funnelbit:20210601184234p:plain

環境を分ける

GoogleTagManager には「環境」という機能が用意されています。「開発用」「本番用」という環境を作ることで、それぞれの環境ごとに GoogleTagManager の設定を分けることができるものです。

support.google.com

この環境を作る前に、どのような開発環境があり得るのかを整理します。まず、手元環境(local とします)がありますよね。そして開発中の機能を社内向けのみにデプロイして確認する環境があるかも知れません(preview とします)。本番前に最終確認する社内向けもあるかも知れません(staging とします)。最後に本番環境(production とします)がありますね。

このうちの local ですが、用意すると常に手元環境から GA の確認ができて良さそうです。一方で、仮に開発メンバーが20人いた場合、20人が同じ GA を叩きまくることになるので、GA に仮の名前の計測値が入ったり、リアルタイム計測で本当に自分の開発中環境から値が送られているか確認したいけど、他の誰かの計測値が次々と飛んできてノイズになったりと、あまり嬉しくない結果になるかも知れません。

個人的なおすすめとしては、local 環境から常に GA(GTM を経由するので正確には GTM ですが)を叩く設定にはせず、本当に必要なときにのみ local 環境から GA を叩く設定にすると良いと思います。こうすると、GA に値を飛ばす機能に関連する部分を実装している人だけが開発中 GA に値を送ることが可能になるので、理想的な開発用 GA が用意されることになると思います。ただしこれが通用するのは少人数開発の話なので、チーム人数が多くなった場合は改めて管理を考える必要はあると思います。

ここでは staging、production 2つを用意する手順を紹介します。

Production の環境

いきなり Production 用環境の話からします。なぜかと言うと、Production 用環境は作る必要がないからです。 GTM の画面で、環境ページに行ってみてください。以下のような状態になっていると思います。

f:id:funnelbit:20210601183859p:plain

ここに行くと、何も作ってないのにデフォルト環境というものがあります。GTM ではデフォルトの環境が用意されており、それが今回の話で言う Production 用環境に該当します。よって Production 環境を新たに作る必要はありません。

Staging 環境

Staging 相当のものは用意されていないので作る必要があります。環境画面の「新規」ボタンを押して作成しましょう。

f:id:funnelbit:20210525184403p:plain

f:id:funnelbit:20210720131627j:plain この画面では「Staging に公開」を押して大丈夫です。Staging はガシガシ変更を公開していくスタイルで行こうと思います。

f:id:funnelbit:20210525184511p:plain このような状態になると思います。

環境ごとのスニペットの差異を確認する

今、2つの環境を用意しましたが、このそれぞれの環境は、どうやって GTM の呼び出し元(今回の場合は Next.js)から使い分けが行われるのでしょうか?それは環境ごとのコードスニペット(GTM を呼び出すために貼り付けるスクリプトタグ)の違いを見るとわかります。

まず、Production 用の環境を見てみましょう。 f:id:funnelbit:20210720131354j:plain 「デフォルト環境」の「Live(公開中)」の「操作」ボタンを押して「コードを取得」を押してください。

f:id:funnelbit:20210601184116p:plain

色々書いてありますね。では、同じように Staging 用も「コードを取得」して見てみましょう。

f:id:funnelbit:20210601184126p:plain

この2つのコードを比較すると、異なる部分があります。画像ではなくコードを貼ってわかりやすくしてみました。

Live 環境

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=-FNbDkDEkVnwbkvCdu8gYA&gtm_preview=env-1&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-MJ28WWZ');</script>
<!-- End Google Tag Manager -->

Staging 環境

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=f-U7CczicK9t6Rkp4NP-HQ&gtm_preview=env-4&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-MJ28WWZ');</script>
<!-- End Google Tag Manager -->

https://www.googletagmanager.com/gtm.js という url が書かれていますが、ここに付いているクエリパラメータ gtm_auth, gtm_preview の値が異なります。Live 環境は gtm_auth=-FNbDkDEkVnwbkvCdu8gYA&gtm_preview=env-1、Staging 環境では gtm_auth=f-U7CczicK9t6Rkp4NP-HQ&gtm_preview=env-4 となっています。この部分が各環境ごとに異なるため、フロントエンドはこの2つのクエリパラメータの値を変えさえすれば、それぞれの環境を切り分けることができるという仕組みになっていることがわかります(gtm_preview はプレビューのときのみ有効なものであると思いますが、まあ変えておいたら良いと思います)。

(小話)Latest 環境について

GTM の環境には「Latest」というものも存在します。GTM の変更を保存するとき、保存して特定の環境に公開するか、保存だけするかを選ぶことができます。このときに保存だけしたとき「Latest」環境だけ新しくなります。Staging で参照する GTM を常に最新の変更へ自動的に追従させたければ「Latest」を参照するという方針も取ることができます。ただし間違った変更も自動的に追従してしまうリスクはあります。

Next.js アプリケーションに GTM のコードを埋め込む

Next.js アプリケーション に GTM のコードをセットして、GTM と通信できるようにしましょう。先ほどのコードスニペットを Next.js アプリケーション にセットして、環境ごとに変わるコードスニペットの部分を切り替えることで、各環境ごとに動作する GTM 呼び出しを実現します。

まず、コードスニペットをどこに書くかです。これは ./pages/_document.tsx(Typescript ではない方は ./pages/_document.js) に書くのが良いでしょう。Next.js のカスタムドキュメントと呼ばれるものです。

nextjs.org

まずは GTM の Production 環境のコードスニペットをそのまま貼り付けてみましょう。とはいえそのままベタッと貼り付けるわけではなく、Next.js アプリケーション の書き方へコンバートする必要はあります。以下のようになりました。

import Document, { Head, Html, Main, NextScript } from "next/document";

class AppDocument extends Document {
  render(): JSX.Element {
    return (
      <Html>
        <Head>
          {/* Google Tag Manager */}
          <script
            dangerouslySetInnerHTML={{
              __html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
              new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
              j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
              'https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=-FNbDkDEkVnwbkvCdu8gYA&gtm_preview=env-1&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);
              })(window,document,'script','dataLayer','GTM-MJ28WWZ');`,
            }}
          />
        </Head>
        <body>
          {/* Google Tag Manager (noscript) */}
          <noscript>
            <iframe
              src={
                "https://www.googletagmanager.com/ns.html?id=GTM-MJ28WWZ&gtm_auth=-FNbDkDEkVnwbkvCdu8gYA&gtm_preview=env-1&gtm_cookies_win=x"
              }
              height="0"
              width="0"
              style={{ display: "none", visibility: "hidden" }}
            ></iframe>
          </noscript>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default AppDocument;

試しに、この状態で GTM と通信できるのか確認してみましょう。GTM にはデバッグできるモードが存在するので、それを利用します。GTM のコンソール画面から「プレビュー」を押しましょう。

f:id:funnelbit:20210706190459p:plain

接続先 URL を聞かれるので、手元で Next.js アプリケーションを起動し、起動したアプリケーションの URL を入れましょう。 f:id:funnelbit:20210706190531p:plain

すると勝手に指定した URL のウィンドウが出てきて、以下のような画面になります。 f:id:funnelbit:20210706190352p:plain

左側にイベントが出ていて、DOM Ready や Window Loaded などがキャッチされており、実際にアプリケーションと接続されていることがわかります。 f:id:funnelbit:20210706190905p:plain

次に、環境ごとに接続先を変えるようにしてみましょう。まず、試したい環境を「公開」してください。試しに Staging にしてみましょう。GTM コンソール画面の「公開」ボタンを押して

f:id:funnelbit:20210706192905p:plain

「環境への公開」というところがあるので、そこが多分 Live となっているので f:id:funnelbit:20210706192746p:plain

Live の横にある編集ボタンを押して、Staging に変えてみましょう f:id:funnelbit:20210706192944p:plain

変えたら「公開」を押してください。

この状態で、コードを以下のように変更します。

import Document, { Head, Html, Main, NextScript } from "next/document";

class AppDocument extends Document {
  render(): JSX.Element {
    return (
      <Html>
        <Head>
          {/* Google Tag Manager */}
          <script
            dangerouslySetInnerHTML={{
              __html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
              new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
              j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
              'https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=f-U7CczicK9t6Rkp4NP-HQ&gtm_preview=env-4&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);
              })(window,document,'script','dataLayer','GTM-MJ28WWZ');`,
            }}
          />
        </Head>
        <body>
          {/* Google Tag Manager (noscript) */}
          <noscript>
            <iframe
              src={
                "https://www.googletagmanager.com/ns.html?id=GTM-MJ28WWZ&gtm_auth=f-U7CczicK9t6Rkp4NP-HQ&gtm_preview=env-4&gtm_cookies_win=x"
              }
              height="0"
              width="0"
              style={{ display: "none", visibility: "hidden" }}
            ></iframe>
          </noscript>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default AppDocument;

この状態でもう一度プレビューして、コードがおかしくないか確認をしましょう。正しければつながるはずです。この時点では、まださっきのプレビューとの差がわかりにくいですね。実はこの時点では Staging 環境の動作は確認できていない(!?)のですが、とりあえずシンタックスが正しいか確認できたら良いのでこのまま進めます。

GTM で環境情報を取得する

それでは、この状態で環境を判別してみましょう。まず、環境情報を取得する設定を有効にします。GTM コンソール画面の変数から「組み込み変数」の「設定」を押しましょう。 f:id:funnelbit:20210713185915p:plain

すると Environment Name という組み込み変数名があると思うので、これにチェックを入れます。 f:id:funnelbit:20210713185956p:plain これによって、環境の名前が GTM で取得できるようになります。変数名の一覧に Environment Name が出ているはずです。 f:id:funnelbit:20210713190153p:plain

とりあえずこの変更を「公開」してみましょう。Staging で試したいので、Staging 環境で公開します。 f:id:funnelbit:20210713190305p:plain

公開したら、再びプレビューをしましょう。プレビューには Variables という項目があるので、そこで Environment Name が来ているか確認してください。

f:id:funnelbit:20210715141655p:plain

...来ていないように見えますね。実は Environment Name 変数をオンにするだけではこの画面では確認できません。実際に Environment Name を使うタグというものを書かなければ現れないのです。こういう暗黙感ある振る舞いがそこら中にあたりが GTM の難しいところです...。

(小話) 自力で html を書かずに js のみで実装する

今回は使っていませんが、自分で今回のような html を書かずとも以下のライブラリを使えば js でのみ GTM の導入ができます。 github.com

GTM でタグを作ってみる ~ 環境変数ごとに GA の ID を変更する

気を取り直してタグとやらを作ってみましょう。これは何かというと、ページを表示したら、環境ごとに GA の ID を変更し GA に接続するという機能を提供するものです。

ユーザー定義変数の作成

まず、ユーザー定義変数を作ってください(タグと言ってたのに!!)。変数画面に「ユーザー定義変数」というものがあるので f:id:funnelbit:20210715142433p:plain ここで「新規」を押し、そこから更に変数選択画面に行って「ルックアップテーブル」を選択します。そして下の画面のような状態にしてください。

f:id:funnelbit:20210715142712p:plain 読み方としては、環境情報名が Staging なら xxxxxx という文字列を返す、というものです。ほかに環境情報名がある場合はここに条件を追加していけます。値を返す Switch 文みたいなものと思ってもらえば良いと思います。本当はここに GA の ID が入りますが、今はまだ無いという状態なのでダミーを入れています。この状態で保存しましょう。

次に、再びユーザー定義変数を作ってください(またか!!)。Google アナリティクス設定というユーザー定義変数が必要です。 f:id:funnelbit:20210715142954p:plain

先ほど作った「GA ルックアップテーブル」を、Google アナリティクスの ID として読み込むためにこの変数が必要なのです。ほかにも様々な設定ができますが、今回は無視します。画像のような状態になったら保存しましょう。

トリガーの作成

そしてにトリガー画面からトリガーを作成します。ページビューを選んで、そのまま保存しましょう。 f:id:funnelbit:20210715143226p:plain ページが表示されたら何かをするというロジックを提供するものです。何をするのかの実装はタグというものに委ねられています。

タグの作成

タグ画面に行って、以下のようなタグを作ってください。 f:id:funnelbit:20210715143551p:plain トリガーにてページが表示されたら、ユーザー定義変数のGoogleアナリティクス設定を使い、GA にトラッキングを送るというタグがここに完成しました。

この状態でプレビューしてみましょう。 f:id:funnelbit:20210715144034p:plain f:id:funnelbit:20210715144049p:plain

GA タグが働いていることがわかり、また Environment Name も来ていることがわかります。Environment Name は 見慣れない値になっています。"Preview Environment 1 2021-07-06 024400" というものです。これはプレビュー専用の Environment Name であることを意味します。よって、GA ルックアップテーブルは "Staging" という文字列に合致しないので undefined を返しています。

今おそらくクライアントは Staging のコードスニペットになってますよね?実はこれは意味がないのです(わざわざやらせたのにすみません)。プレビュー中は、接続中クライアントのコードスニペットがどんな環境であれ関係なく、独自の環境に置き換えられるのです。クライアントのコードスニペットが Staging だろうがなんだろうが、強制的に別の環境にされてしまいます。環境を選ぶ権限は GTM 側にあり、そして環境を GTM 側で切り替えることもできます。ここで、プレビュー対象を Staging に変更してみましょう。この画面、よく見ると Previewing: というコーナーがあり、そこで env-4 を選んでみました。env-4は私の環境では Staging を表す Environment ID です。この数値を覚えていなくとも、Previewing: を押すとリストが出てくるので、Staging の Debug を押すと選択されます。 f:id:funnelbit:20210715144411p:plain

f:id:funnelbit:20210715144500p:plain

すると Environment Name に Staging が入っており、GA ルックアップテーブルにも値が入ってることが確認でき、設定できていることがわかります。 f:id:funnelbit:20210715144626p:plain

それでは、試しに GA を作って計測されるかどうか見てみましょうか。

GA を設定する

今回はユニバーサル アナリティクスを想定しているので、ユニバーサル アナリティクスで作ります。ユニバーサル アナリティクスの作成導線は少々巧妙に隠されており、プロパティの設定項目にある詳細オプションを表示を押すと出てきます。Staging のアナリティクスを試したいので Staging という名前にしました。 f:id:funnelbit:20210715152342p:plain

適当に作ったら、トラッキング ID を覚えておいてください。ユニバーサルアナリティクスで作ったなら UA-xxxxx となっているはずです。 f:id:funnelbit:20210715152447p:plain

この変更を Staging 環境にて「公開」してください。

GTM でトラッキング ID をセット

再び GTM にもどります。先ほどのトラッキング ID を GA ルックアップテーブルの Staging 枠に入れて保存しましょう。f:id:funnelbit:20210715152639p:plain

これで Staging 環境にてアクセスしたら GA に接続できるはずです!

GA で接続を確認する

GA をリアルタイムを開きます。 f:id:funnelbit:20210715152835p:plain

手元の Next.js アプリケーションは既に Staging 環境になっているはずなので、そのままアクセスしてみましょう。 f:id:funnelbit:20210715153113p:plain

リアルタイム計測が反応しました!ちゃんと接続できているようです!

本当に環境ごとに切り替わっているのか確認したい場合は、Next.js アプリケーションのコードスニペットを Live のものに変更しましょう。反応しなくなるはずです。

Next.js アプリケーションで環境ごとのコードスニペットを簡単に切り替える

環境ごとに GA を切り替えれることはわかりましたが、これをいちいちデプロイごとに手で書き換えるのは現実的ではないので、環境を切り替えれるように Next.js アプリケーションの環境変数 Environment Variables を使いましょう。

nextjs.org

とりあえず .env.local を作ってみましょうか。GTM のコードスニペットにある環境変数ごとに変化する部分だけを抜き出し、Next.js アプリケーション の環境変数に落とし込みます。

NEXT_PUBLIC_GTM_CONTAINER_ID="GTM-MJ28WWZ"
NEXT_PUBLIC_GTM_CONTAINER_ENV="&gtm_auth=f-U7CczicK9t6Rkp4NP-HQ&gtm_preview=env-4" # とりあえず Staging 試したいので Staging 固有の文字列を入れます

そして Next.js アプリケーション の _document.tsx のコードは環境変数を使うように変更します。

import Document, { Head, Html, Main, NextScript } from "next/document";

const gtmContainerId = process.env.NEXT_PUBLIC_GTM_CONTAINER_ID;
const gtmContainerEnv = process.env.NEXT_PUBLIC_GTM_CONTAINER_ENV;

class AppDocument extends Document {
  render(): JSX.Element {
    return (
      <Html>
        <Head>
          {/* Google Tag Manager */}
          <script
            dangerouslySetInnerHTML={{
              __html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
                       new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
                      j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
                      'https://www.googletagmanager.com/gtm.js?id='+i+dl${
                        gtmContainerEnv ? `+'${gtmContainerEnv}&gtm_cookies_win=x'` : ""
                      };f.parentNode.insertBefore(j,f);
                      })(window,document,'script','dataLayer','${gtmContainerId}');`,
            }}
          />
        </Head>
        <body>
          {/* Google Tag Manager (noscript) */}
          <noscript>
            <iframe
                src={`https://www.googletagmanager.com/ns.html?id=${gtmContainerId}${
                  gtmContainerEnv ?? ""
                }&gtm_cookies_win=x`}
              height="0"
              width="0"
              style={{ display: "none", visibility: "hidden" }}
            ></iframe>
          </noscript>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default AppDocument;

この状態で Next.js アプリケーションをリスタートすれば、再び GA のリアルタイムに計測が出てくるはずです。あとはこの環境変数をデプロイごとに変更すれば良いと言うことになります。ここはビルドする環境ごとに細かく違うと思うので割愛します。

これで環境ごとに切り替える Next.js アプリケーション、 GTM、GA それぞれの設定ができました。ほかにも環境を追加したければ、ここに書いた手順をたどればできるはずです。

本番環境 GA へ接続

本番環境 GA へ接続できるようにしましょう。本番用 GA の作成は Staging 用 GA を作ったときと同じなので割愛します。

Staging のときと同じような感じです。GA ルックアップ テーブル に Live だったら Live 用のトラッキング ID を返すようにします(説明のために GA 用意するのめんどくさくなってきたので xxxx としてますが、正しい GA のトラッキング ID を入れてください) f:id:funnelbit:20210715163930p:plain

公開しましょう。本番の環境なので Live 環境で公開するのを忘れないでください!

そして Next.js の .env.local の値を本番用に書き換え、Next.js アプリケーションを再起動してアクセスしてみましょう。本番用 GA のリアルタイム計測が変化するはずです。

Next.js アプリケーションで画面遷移したときにも計測するようにする

これが最後の手順になります。今の状態だと、画面遷移しても計測がなされません。現状反応するのはページをまるごと読み込みしたときだけです。Next.js は SPA であるので、ページを読み込んだ後は画面遷移しても今の設定だと何も反応しません。なので、ページ遷移しても計測するようにしましょう。

GTM のトリガには履歴の変更というものがあります。Next.js アプリケーションが、ブラウザの History API を操作し、ブラウザの戻る・進むボタンをハンドリングしていることは、既にご存知かと思います。この操作をフックするには、履歴の変更で行うことができます。試しに作成してみました。 f:id:funnelbit:20210715165841p:plain

これを GA のタグのトリガーとしてセットします。読み方としては「ページを新規読み込みした、または Next.js アプリケーションによる画面遷移が起こった」です。 f:id:funnelbit:20210715165933p:plain

さて、これで保存はしておいて GTM のプレビューを起動して挙動をみてみましょう。こちらの手元では画面遷移を行うコードを追加したので、遷移するボタンを押すと画面遷移できます。

f:id:funnelbit:20210715170437p:plain

画面遷移すると、GTM のコンソールの Summary 枠に History という項目が現れます。これが「履歴の変更」トリガーの効果です。一見するとこれで良さそうに見えます。 f:id:funnelbit:20210715170632p:plain

では、この状態でブラウザの戻るボタンを押してみるとどうなるでしょうか。

f:id:funnelbit:20210715170946p:plain 先ほど一つだけだった History が3つになっています。1つ多いです。つまり、戻るボタンを押すと、二重に GA に計測が飛んでいく事になります。このままでは正しい計測を行うことができません。

この問題に対して、以下の2つの方法があります。GTM のトリガー設定を工夫し乗り切るか、トリガーを諦め、自力で GTM にイベントを送るかの方法です。

選択肢1: GTM の履歴の変更トリガーの設定を工夫する方法

そもそも戻るボタンを押したときに何故 GTM に二重に History が発火されるのかというと、Next.js はブラウザの戻るが押されたときに出る popstate イベント発生後、 replaceState をしているため、GTM としては「履歴が二回操作されたから、二回 History イベントでしょ」ということでこうなっています。なのでこれをちょっと工夫します。

まず変数で History Source を有効にし、履歴の状態を変数として取得できるようにします。 f:id:funnelbit:20210715172331p:plain

次に、履歴の変更トリガーの設定を以下のように変更します。 f:id:funnelbit:20210720155936p:plain

履歴の変更トリガには正規表現で HistorySource を厳選できる機能があるので、ここで popstate は無視するようにします。これによって二重に GA 計測が呼ばれないようにはなります(replaceState は router.replace() を呼んだときに使われるので popstate を選択しています)。

この状態でプレビューを試してみると、History 自体は余分に出てしまいますが

f:id:funnelbit:20210715173145p:plain 片方は GA のタグが Fired になっておらず(GA が送られておらず)

f:id:funnelbit:20210715173141p:plain もう片方は GA のタグが Fired になり、片方だけ GA にイベントが送られていることがわかります。

無事に動きましたが、果たして popstate を除外するのがベストなのかどうかは不明です。またこれは Next.js のルーティング処理内部の仕様に左右されるところではあるので、今後挙動が変わったら計測が変になる懸念はあると思います。

選択肢2: 自力で GTM にイベントを送る方法

GTM の履歴の変更トリガーで GA に送るのをやめる選択です。Next.js の公式リポジトリには、この方法が書かれたファイルが存在します。

github.com

これは _app.tsx(_app.js) にて、履歴の変更を監視し、ページが読み込まれたら GTM にイベントを送るというものです。router にはページ遷移したら通知してくれる router.events.on('routeChangeComplete', () => {}) というものがあるので、これを使って履歴の変更をキャッチし、自力で GTM へイベントを送っています。単純に各ページの useEffect などで、ページが表示されたら一度だけ GMT にイベントを送るということもできますが、全部のページに送るコードを書いてられないので _app.tsx にだけ書いて送ろうという試みになります。

ここが難しいポイントですが、router.events.on('routeChangeComplete', () => {}) はブラウザによる初回読み込み時・再読込時には通知されないので、それらの場合だけは一度だけ動作する useEffect などを使ってカバーすることになります。ただし例外が2パターンあります。

1つ目は、クエリパラメータが付いているときです。このとき、ブラウザによる初回読み込み時・再読込時に router.events.on('routeChangeComplete', () => {}) が動いてしまいます。クエリパラメータがある時は特別扱いし router.events.on('routeChangeComplete', () => {}) に任せるようなコードが先ほどのNext.js の公式リポジトリには書かれています。

しかしこの方法にはまだ問題があります。2つ目の例外として、 Dynamic routing なページな場合もクエリパラメータ付き URL と同じく初回読み込み時・再読込時にも router.events.on('routeChangeComplete', () => {}) が動いてしまい、Next.js リポジトリの例通りにやると二重にイベントを送ってしまいます。これは issue も出ていますが、特に動かずに今に至っています。

github.com

Next.js のサンプルを参考に実装するのであれば、Dynamic routing のときもクエリパラメータありと同じく特別扱いするコードを追加で書くべきです。現在開いているページが Dynamic routing かどうかは、以下の方法でチェックすることができます。

funnelbit.hatenablog.com

これを利用し、_app.tsx(_app.js) にて以下のように書きましょう。

import { isDynamicRoute } from 'next/dist/next-server/lib/router/utils';
import { useEffectOnce } from 'react-use';
...

export default function App({ Component, pageProps }) {
  const router = useRouter();

  useEffectOnce(() => {
    if (!isDynamicRoute(router.pathname) && !router.asPath.includes('?')) {
      log();
    }
  });

  useEffect(() => {
    router.events.on('routeChangeComplete', log);
    return () => router.events.off('routeChangeComplete', log);
  }, [router.events]);

  return <Component {...pageProps} />
}

react-use には一度だけ動く useEffect であることをより明確にした文法である useEffectOnce があるので利用しています。 github.com これを利用せずに useEffect で依存変数を空配列にしてももちろん構いません。

次に、この routeChanged イベントを GTM がキャッチしたら動作するトリガーを作成します。トリガーのメニューから「新規」ボタンを押して「カスタム イベント」というトリガーを以下の画像のように作ってください。 f:id:funnelbit:20210720155207p:plain

次に、折角作りましたが GA タグのページビューと履歴の変更トリガーを外して、このカスタムイベントトリガーに付け替えます。 f:id:funnelbit:20210720155420p:plain

これで保存し、プレビューしてみましょう。 f:id:funnelbit:20210720155600p:plain

routeChanged イベントがあり、 GA タグが Fired になっていることがわかります。新規ページ読み込み時はもちろん、ブラウザの戻る・進むボタンでも正しく動きますし、二重に呼ばれているということもありません。

どちらが良いのか?

時間がないなら前者でシャッと作ってしまっても良いと思いますが、正直言ってこれは正しいのかわからず不気味です。後者を選択してもさほど工数は変わらないと思うので、できれば後者を選ぶと良いと思います。

終わりに

Next.js + GTM + GA は情報がかなり少なく、あっても二重に計測される問題をはらんでいる状態だったので、めっちゃ大変でしたが書いてみました。めっちゃ疲れました(特にスクショ用意するのが)。もっといい方法もあるかもしれないので、あればぜひ教えてほしいです。

ちなみにこの記事に出ている GTM とか Analytics の ID はアクセスしても既に削除されているので使えないです。