Next.js で Google Analytics を使う・2019年冬

これは はてなエンジニア Advent Calendar 2019 14日目の記事です。

昨日は id:takuji31 さんの Kotlin Coroutinesをテストする - Takuji->find; でした。


最近は仕事で Next.js を使っているのですが、その中で Google Analytics による計測をどうするかという話題があったので紹介します。

前提

  • Next.js ((+ TypeScript) + Apollo Client) の話です
    • SPA 的遷移は使って無くて、都度 SSR で遷移してる
  • 僕は Google Analytics とその実装にまあまあ詳しい自信があります
  • 一方で React を完全に理解しているかというとそんな自信はない

tl;dr

自転車置き場の議論っぽい気もしますが、このような選択に至った理由について書いていきます。いろいろ悩みながらの判断という感じだったので、よりよい方法やご意見などあればぜひ教えていただけると嬉しいです!!!

そもそもどうやって Google Analytics を設置するか

Web ページに Google Analytics を導入する方法は数多存在するわけですが、ふつうはこれらの選択肢の中から選ぶことになると思います:

  1. いわゆる普通の設置タグ (gtag.jsanalytics.js) をコピペしてくる
    • もっとも無難と思われる
  2. Google タグマネージャー (GTM) を設置し、GTM の設定で Google Analytics を呼び出す
    • これも無難
  3. 利用しているフレームワークなどに合わせて、gtag.js や analytics.js の設置を肩代わりしてくれるライブラリを使う
  4. 自分で Measurement Protocol を叩く、もしくはこれをラップしたライブラリなどを使う
    • ハードコアだと思う……

gtag.js・analytics.js・GTM のどれかを利用する方法がもっともメジャーかつ支配的だと思いますが、これらのタグはだいたい自分でなんらかの状態を持っていたり、提供されている読み込み用コードの実行に副作用を持つものもあることに注意する必要があります。

たとえば、gtag.js のヘルプには以下のような記述があります:

ページビュー測定を無効化する

このコードのデフォルトの動作は、Google アナリティクスにページビュー ヒットを送信することです。ほとんどの場合はこの動作で問題ありません。サイトの各ページにコードを追加すれば、以降は自動的にページビューが測定されます。なんらかの理由で、Google アナリティクスにページビュー ヒットが送信されないようにしたい場合は、send_page_view パラメータを false に設定します。

gtag('config', 'GA_MEASUREMENT_ID', { 'send_page_view': false });

サイトに gtag.js を追加する  |  ウェブ向けアナリティクス(gtag.js)  |  Google Developers

つまるところ、gtag.js をロードしている <script> タグが不意に再レンダリングされないようする必要がある、という話なのですが、これは next/head パッケージで提供されている <Head> コンポーネントを利用することで回避できます *1。具体的にはこういう例がよく示されています:

<Head>
  <script async src={`https://www.googletagmanager.com/gtag/js?id=UA-XXXXXX-X`} />
  <script
    dangerouslySetInnerHTML={{
      __html: `
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments)};
        gtag('js', new Date());
        gtag('config', 'UA-XXXXXX-X');
      `,
    }}
  />
</Head>

"Next.js Google Analytics" とかで検索した人がたいてい見るであろう zeit/next.js の issue #160 などでもこれが紹介されていますし、前述の next-plugin-google-analytics も今の時点ではこの方式を使おうとしているようです。

というわけで、そういうの ☝️を _app.js に書くのが一番良さそう、というのがいまのところの結論です。念の為 key prop を設定してもよいかもしれません。

これはあんまりわかっていなくて *2、というのは

  • Router から routeChangeComplete(url) イベントというのが取れるのでそれを使えるはず
  • 一方で、routeChangeComplete されたタイミングではまだページのコンテンツを組み立て中ということがある
    • 具体的には、何らかの API からコンテンツを取得している最中だったりとか
    • 遷移後の URL はわかっているものの、title 要素が意図したものに置き換わってなかったりするので、遷移後の URL + 遷移前の title のページビューとして Google Analytics に送信されてしまう
      • Google Analytics で分析をするときに、ページタイトルを軸にして集計することはたまにあるはずなので、これでは困りそう
    • 何を持ってページビュー計測準備完了かというのはそのページによって異なるはずなので、外側から何かを観測してフックしたりするのは不可能な印象がある、ページのコンポーネントそれぞれでなんかやる必要がある?

どうするのがいいんでしょうかね、ページのコンポーネント

イベント計測

ここでこういうイベントを拾う、というのを React 世界でどう表現するか

どういうイベントを計測したいか、というのも場合によりけりですが、要素のクリックや視認 (imp) などを拾えるようなグッズを準備できれば大抵の場合は間に合うものと思います。で、実際にページを記述するコンポーネントにグッズを仕込んでいくわけですが、それにもいくつかのやり方があると思います:

  1. onClick などイベントハンドラに仕込む用のメソッドを用意して、計測したいポイントでそれを呼ぶことにする
    • Cons: 計測に関する話題までそのコンポーネントの責務になってしまうことで話が難しくなる (個人の感想)
  2. イベントバブリングを利用する想定で、計測対象のコンポーネントを囲むような形で設置するコンポーネントを用意する
    • <ClickTracker eventName="foobar"><Foobar /></ClickTracker> みたいな形で使う
    • Pros: 1. 案と比べて、コンポーネントごとに一つの機能を果たす世界観に近い
    • Cons: 内側で preventDefault などされるとイベント計測は発火しなくなる
    • Cons: div などが必要になるので、そのぶん HTML 要素のネストが深くなってしまう
  3. 計測対象のコンポーネントをくるむ HOC を用意する
    • Pros: 計測対象の要素にそのまま onClick なりを設定するようにすれば、2. 案と比べて余分に HTML のネストが深くなるということはない
    • Cons: HOC で onClick なりを触りたくない…… (個人の感想)
      • イベントバブリングに乗っかるのとどっちがいいのか、という話
  4. data- 属性などにイベントの情報を含めて、その属性を持つ要素に対してクリックイベントを設定する、というふうにする
    • Google タグマネージャーとこれを組み合わせることで、アプリケーションのデプロイなしで計測の設定をよしなにできるので、弊社ではまあまあこの手法が使われています
      • Google タグマネージャーによるイベントの計測は、「要素に対して一度だけ発火する」ような設定の状態が DOM 側にねじ込まれる実装になっており React などとは相性があまり良くなさそう、ということで今回は使わないことにしました
      1. 案と同様に、どのような文脈で計測されるのかという話題がコンポーネント側の責務になる

どの方式が一番適しているのかも場合によりけりなのではないかとは思っているのですが、今回はチームでの議論を経て 2. 案を選びました。

イベントを送る

実際にイベントを送信する際には、自前の軽いラッパーを通して gtag('event', ...) を直接叩く方法を選びました。

Google アナリティクスのイベントを測定する  |  ウェブ向けアナリティクス(gtag.js)  |  Google Developers

gtag() は引数にいろいろ指定することでいろいろできる、という感じの API なのですが、TypeScript を使っているのであれば DefinitelyTyped の @types/gtag.js パッケージを使うことで単純な引数のミスなんかは減らせると思います。

@types/gtag.js - npm

たびたび言及している react-ga などを使わないことにしたのは、この程度であればライブラリに頼るほどでもないかなと思ったのが理由です。

おわり

ということで、Next.js に Google Analytics による計測を実装していく中で考えたことをご紹介しました。2020 年も next-plugin-google-analytics 元年としてやっていきましょう。


はてなエンジニア Advent Calendar 2019 明日の担当は id:susisu さんです。フロントエンドネタが続きそうな気がする!

*1:Head コンポーネントは、渡された children の中身を独自のロジックで比較して実際の head タグに反映する、というような実装になっているので、同じ children を渡しっぱなしにしている限りは実際に DOM の抜き差しなどが起こることはない、という理解でいます

*2:今回はこのへんはスコープ外だったので……