Reactアプリケーション内でGoogle Analytics計測をする際、react-gaを使わず、gtag.jsを利用した方法とその選択理由
見出し画像

Reactアプリケーション内でGoogle Analytics計測をする際、react-gaを使わず、gtag.jsを利用した方法とその選択理由

電通デジタルのエンジニア、西山です。

この記事は、電通デジタルアドベントカレンダー2020の3日目の記事です。前回の記事は「2020年に作ったDevOps内製ツール」でした。

この記事ではReactでGoogle Analyticsの計測コードを埋め込む方法についてお話しします。他のブログなどですでに何度も紹介されているテーマですが、ブログによって用いられる手法は様々で、どれを採用すればいいか迷う人も多くいるのではないかと思いますし、中には情報が古くなっているものもあります。

そこで最新の状況を調査した上で、私たちが採用した手法を紹介しますので、ReactでGoogle Analytics計測コードを埋め込む際の参考にしていただければと思います。もし私たちとは異なる手法を採用する際にも参考になるように、手法の選択理由や背景も合わせて載せています。

弊社での導入背景の説明

弊社で社内ユーザー向けに提供しているEASIというプロダクトがあり、そのフロントエンドをReact + TypeScriptで実装しています。

私たちEASI開発チームでは、今年その新バージョンUIを開発している真っ最中で、UIの利用状況を計測するためGoogle Analyticsを埋め込むことになりました。旧バージョンのUIでも同様の計測にGoogle Analyticsを利用しており、react-gaで実現していました。当初は新UIでもreact-gaを使おうかと考えていたのですが、最終的には記事タイトルの通りreact-gaを使わない判断をすることになりました。

Google Analyticsの進歩

Reactの話題に入る前に、Google Analyticsのクライアントライブラリについてお話ししておきましょう。Google Analyticsのクライアントライブラリは大きく分けて以下の3つのバージョンがあります。

- ga.js(古い、deprecated)
- analytics.js(一世代前、しかしまだ現役、react-gaはこれを内部で利用)
- gtag.js(最新)

Google AnalyticsはGoogle Marketing Platformの中で中心的な役割を果たす、非常に進化の速いツールです。最近では2020年10月にGoogle Analytics 4 propertyがリリースされ、WebとAppの計測がより一層統合され、さらに機械学習によるサポート機能が追加されるなど、Googleの見据えるWeb計測の未来に沿った機能が提供されました。

スクリーンショット 2020-11-09 17.29.36

上図はGoogle Analyticsのpropertyとクライアントライブラリの組み合わせを表したものです。Google Analytics 4 propertyを使うには、最新のクライアントライブラリであるgtag.jsが必要です。analytics.jsを利用している場合はgtag.jsにクライアントライブラリを入れ替えなければ、Google Analytics 4 propertyは使えません。

現在もanalytics.jsは一世代前のUniversal propertyを使っている限り問題なく動作し続けますし、Google Analytics 4 propertyへは管理者による明示的な設定変更なくして勝手に切り替わることはありません。しかし、特別な理由がない限り新たなプロジェクトでanalytics.jsを選ぶ動機付けは弱いと言えます。

react-gaを利用しなかった理由

ReactにGoogle Analyticsを埋め込む方法を検索すると、2020年現在もreact-gaを解説したページが数多くヒットします。しかし、react-gaは内部で一世代前のクライアントライブラリであるanalytics.jsを使っています

さらに、こちらのイシューを見るとreact-gaの歴代コミット数第2位のSimeonC氏が、現在react-gaはアクティブにメンテナンスされていないという発言をしています

As no one is actively maintaining this repo due to time and interest this would need someone to submit a MR! (I just merge MRs and tag releases at present, at my work we just use the ga4 tag directly without an extra library - though we’re also looking at not using GA at all!)

別のイシューではanalytics.jsからgtag.jsへのアップグレードが推奨されている、という内容が報告されています。私はこの推奨ダイアログがどのような場面で表示されるのか発見できませんでしたが、Google Analytics 4 propertyが公式にリリースされた今、gtag.jsへのアップグレードが推奨され始めていてもおかしくはありません。

内部で一世代前のanalytics.jsを使っている、活発に保守されていない可能性が高い、という2点から私たちはreact-gaを使わず、直接gtag.jsを利用する方法を選びました。

Google Analytics計測コード埋め込み手法の説明

以下のコードスニペットは、gtag.js公式ページに載っているものです。これをそのままHTMLに埋め込むのですが、GA_MEASUREMENT_IDを開発環境と本番環境で変える場合はひと工夫必要になります。

<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script>
 window.dataLayer = window.dataLayer || [];
 function gtag(){dataLayer.push(arguments);}
 gtag('js', new Date());
 
 gtag('config', 'GA_MEASUREMENT_ID');
</script>

ReactのようなSingle Page Applicationの場合は上記のコードスニペットだけで完結するのではなく、ページリロードを伴わずパスが変わった際にページビューを記録するため、Reactアプリケーション内部でもGoogle Analyticsの計測コード埋め込みが必要です。

一応公式ページにSingle Page Application用の解説があるのですが、情報不足なためすぐに動くようなものではありません。それでは、Reactアプリケーション側での実装を紹介しましょう。

 const { listen } = useHistory()
 
 useEffect(() => {
   const unlisten = listen((location) => {
     if (!window.gtag) return
     if (!trackingId) {
       console.log(
         'Tracking not enabled, as `trackingId` was not given and there is no `GA_MEASUREMENT_ID`.'
       )
       return
     }
     window.gtag('config', trackingId, { page_path: location.pathname })
   })
   return unlisten
 }, [trackingId, listen])

コード自体はこちらのブログを参考にしており、Single Page Application内でURLのパスが変わるたびにwindow.gtagが変更されたlocation.pathに対して呼び出されることになります。useEffectの第2引数として[tracking, listen]を用いるのは、これらが変化しない限りuseEffectが再び呼び出さないようにするためです。さらに、このコードスニペットを呼び出すコンポーネントがアンマウントされたときに備え、useEffect内でlistenの戻り値であるunlistenをreturnしています。

注意すべきはuseHistoryはReactのhooksではなく、React Routerのhooksなので、Routerの内部で呼び出さなければ実行時エラーになるということです。下記のような呼び出しではなく...

function App() {
  //先ほどのコードスニペット
  const history = useHistory(); //エラー: AppコンポーネントはRouterの外側
  ...
  ...

  return (
    <Router>
      <..>
    </Router>
  );
}

このようにRouter内部にネストされたコンポーネントの中で呼び出しましょう。

function App() {
  return (
    <Router>
      <InnerComponent>
        <..>
      </InnerComponent>
    </Router>
  );
}

function InnerComponent() {
  //先ほどのコードスニペット
  const history = useHistory();
  ...
  ...

  return (
     <..>
  );
}

useHistoryは2019年に導入された比較的新しいAPIです。React RouterではReach Routerを参考に、さらに便利なhooks APIを用意しているようです。useHistoryを使えば、以前のcreateBrowserHistory APIに比べ、Reactの現行のプラクティスであるhooksに沿った形で、親子コンポーネント関係を意識することなく簡潔に書けるようになりました。

最後に、TypeScriptでのgtagの型定義を利用するために、@types/gtag.jsをインストールしましょう。 gtagは異なるパラメタをとる関数として定義されているので、TypeScript interfaceのFunction Typesになっています。

//@types/gtag.jsより
interface Gtag {
   (command: 'config', targetId: string, config?: ControlParams | EventParams | CustomParams): void;
   (command: 'set', config: CustomParams): void;
   (command: 'js', config: Date): void;
   (command: 'event', eventName: EventNames | string, eventParams?: ControlParams |  EventParams | CustomParams): void;
}

React側でのwindow.gtag関数の宣言はこうなります。

declare global {
 interface Window {
   gtag: Gtag.Gtag;
 }
}

少しだけ異なる手法 - Reactアプリケーション側の実装のみを行う

先ほど紹介した手法はgtag.js公式のコードスニペットをそのまま利用でき、Single Page Application特有の部分だけReactアプリケーション側で実装するものでした。

しかしデメリットとしてはHTML側とReact側でGoogle Analyticsの計測コード埋め込みが分離してしまい、わかりづらくなるということがあります。特にだんだん複雑な計測を行う必要性がでてきたとき、HTML側で実装すべきかReact側で実装すべきか迷うこともあるでしょう。

そこでReact側で全ての埋め込みを行う手法もあわせて紹介します。実は、私たちはこちらの手法を採用しました。まず下記のinitializeGA関数を用意します。これはgtag.js公式の静的サイト用のコードスニペットを、そのままTypeScriptで動くように変えたものです。

function initializeGA(measurementId: string) {
  // load gtag.js:  https://developers.google.com/analytics/devguides/collection/gtagjs
  let script1 = document.createElement('script');
  script1.src = 'https://www.googletagmanager.com/gtag/js?id=' + measurementId;
  script1.async = true;
  document.body.appendChild(script1);
 
  let script2 = document.createElement('script');
  // プレーンテキスト、グローバルな window.gtag 関数を定義するため
  script2.text = `window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'GA_MEASUREMENT_ID');`;
  document.body.appendChild(script2);
}

上記script2がプレーンテキストでJavaScriptスニペットを定義しているのは、gtagというグローバルな関数を定義するからです。これをnodeのモジュールシステム内で行うのは手間がかかります。モジュールシステムの都合に合わせて無理をするより、Google Analytics公式ページのコードスニペットからの乖離は最小限に止めるためプレーンテキストを利用しました。

useEffectの中身は、最初に紹介した手法と違いinitializeGA関数を呼び出しています。さらに第2引数が[trackingId, listen]ではなく空の配列[]となっているので、useEffectの2回目呼び出しが行われないようにするためです。これはinitializeGA関数の中でDOMに<script>要素の追加を行っているからで、こうしないとuseEffectの2回目呼び出しの時に、重複する<script>要素が追加されてしまいます。

const { listen } = useHistory()

useEffect(() => {
  initializeGA(measurementId);
  
  const unlisten = listen((location) => {
    if (!window.gtag) return
    if (!trackingId) {
      console.log(
        'Tracking not enabled, as `trackingId` was not given and there is no `GA_MEASUREMENT_ID`.'
      )
      return
    }
    window.gtag('config', trackingId, { page_path: location.pathname })
  })
  return unlisten
 }, [])

まとめ

私たちEASI開発チームでは今回、この記事で記載した理由からreact-gaを使わず、直接gtag.jsとuseHistoryを組み合わせることでReactアプリケーションでのGoogle Analytics埋め込みを実現しました。今回紹介した方法が記事を読んだ方の参考になれば幸いです。

次回の記事は「SPOFとはもう呼ばせない!Airflow 2.0で生まれ変わったHAスケジューラー」です。