Next.js + Google Analytics でページビューイベントが二重送信されてしまう問題に対処する

3 min read読了の目安(約3100字 2

Next.js で Google Analytics を使っているのですが、ページビューイベントが重複して計測されていることが多かったので調べてみました。

前提として、Next.js 公式の examples/with-google-analytics の通りに実装している場合、今回の事象が発生します。(2021/3/19 時点)

不具合という訳ではないので公式の example が間違っているという訳ではありませんが、割と発生して困る人がいそうなので、個人的な対処方法をメモしておきます。

起こった問題

クエリストリング(URLパラメータ)付きURLで静的生成ページにアクセスすると、 Google Analytics にページビューイベントが二重で送信されていました。発生するのは静的生成されたページで、SSRのページでは発生しません。

(補足: そもそも静的生成ページになぜURLパラメータをつける必要があるのかというと、広告流入などの分析をするためです。)

原因

next/router の routeChangeComplete イベント(ルートの変化が完了した際に発火するイベント)が、初回ランディング時にも発火していたことが原因でした。

ページ遷移した時だけでなく、クエリストリング付きのURLで静的生成ページに着地した場合でも発火するようです。SSRのページやクエリストリングなしの静的URLの場合、着地時に routeChangeComplete は発火しません。

pages/_app.js
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import * as gtag from '../lib/gtag'

const App = ({ Component, pageProps }) => {
  const router = useRouter()
  useEffect(() => {
    const handleRouteChange = (url) => {
      gtag.pageview(url) // ランディング時にもこれが実行されてしまう
    }
    router.events.on('routeChangeComplete', handleRouteChange)
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange)
    }
  }, [router.events])

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

export default App
pages/_document.js
...
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  // 本来、ランディング時はこっちだけ送信してほしい
  gtag('config', '${GA_TRACKING_ID}', {
    page_path: window.location.pathname,
  });
...

対処方法

next/router の routeChangeComplete イベントのコールバック関数では、第2引数に { shallow } を取ることができます。
next/router - router.events

この shallowShallow Routing を有効にした遷移の時に true になる値ですが、ランディング時にクエリストリングが付いているURLの場合も Shallow Routing の遷移と同様に shallowtrue になるようです。

なので、クエリストリング付きURLでの着地時に発火しないようにするためには、下記のように shallowtrue でない場合だけページビューイベント送信処理をしてやればOKです。

pages/_app.js
-   const handleRouteChange = (url) => {
+   const handleRouteChange = (url, { shallow }) => {
-     gtag.pageview(url)
+     if (!shallow) { gtag.pageview(url) }
    }
    router.events.on('routeChangeComplete', handleRouteChange)

注意

ただ上記の修正だと Shallow Routing を有効にしたページ遷移(shallowtrue)の場合もページビューイベントが送信されなくなってしまうので注意してください。Shallow Routing を有効にしたページ遷移も計測したい場合は、url を判定してページビューイベントを送信するかどうかを決めるのが良いかと思います。

クエリストリング付きURLで着地したページが ISR の初回生成だった場合は、生成が完了(または404などエラーページを表示)するタイミングで routeChangeComplete イベントが発火します。shallowfalse でコールバック関数が実行されるため、今回の方法では二重送信を防げません。こちらを防ぎたい場合は、 getStaticPaths の返却値に fallback: 'blocking' を指定することで、着地時のコールバック関数で shallowtrue で受け取ることができるようになります。ただ初期レンダリングがブロックされて遅く感じるため、そこまでして対応するか悩みどころです。

最後に

色々試行錯誤して対策しましたが、そもそも routeChangeComplete イベント以外の他の方法で解決する方法があるかもしれません。もしかしたら useEffectrouter.asPath が変わったら送信する、とかでもいいのかもしれません。。

あとはコメントいただいて知ったのですが、Google Analytics 4 から「拡張計測機能」というURLが切り替わったときに自動でPV計測が行われる機能ができたようです(試せてません)。

いい対応案があったらぜひコメントください。