Open8

APIでフェッチした任意のGoogleAnalytics計測IDをクライアント側で設定する(Next.js)

waddy_uwaddy_u

背景

計測ID = G-CSXXXXXXXX こういうのです。
固定の計測IDや、NEXT_PUBLIC_ で取得できるケースのサンプルはたくさんあります。

https://nextjs.org/docs/messages/next-script-for-ga

ここでは、動的に変化する、例えば同じサイトのページごとに異なる計測IDをアプリに設定したいとき、どのようなコードを組めばいいか考えます。

waddy_uwaddy_u

Google Tag Manager vs Google Analytics ダイレクト

自分のサイトを計測する場合は、Google Tag Manager を挟むのが無難です。

  • アプリケーションを変更せずにカスタムイベントを発火できる
  • アプリケーションを変更せずにカスタムイベントの発行を中止できる
  • 一度作成したトリガー条件(ここをClickしたら、など)はさまざまなカスタムイベントに流用できる

一方、以下のようなケースではGoogle Analytics4のタグを直接埋め込むことを考えます。

  • GTMのようにカスタムするまでもないケース
  • GTMのようにカスタムさせたくないケース
  • とりあえず計測さえできれば良いケース

シチュエーションとして「サイトにログイン可能な登録ユーザーが、自分の名前のslug配下のページを計測するために、GA4タグを埋め込める」を想定します。

番外編:これは?

登録・削除でシステム境界を超えないといけないのと、「このページでは計測する、このページでは計測しない」ということができそうにない(できるとしても大変)なので却下

waddy_uwaddy_u

どのような方法があるか

ログインユーザーの G-CSXXXXXXXX は動的にフェッチする状況です。さらにGA4の計測IDは、以下の方法で埋め込むことになっています。

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

  gtag('config', 'G-CSXXXXXXXX');
</script>

埋め込む方法の候補↓

  1. _document.tsx で素直に埋め込む(動的な要件に対応できないので却下)
  2. 「Next.js の Script タグで埋め込む」コンポーネントにする
  3. _document.tsxgtag('js', new Date());までを行い、 gtag('config', 'G-CSXXXXXXXX'); をhooksで実行する
waddy_uwaddy_u

2. Script タグを使った コンポーネントにする

それを実装したNext.jsの公式パッケージがある。

https://github.com/vercel/next.js/blob/canary/packages/third-parties/src/google/ga.tsx

利用イメージ

Next.js の Pageコンポーネントを考えます(App Routerじゃなくてごめん)

index.page.tsx
import type { GetServerSideProps } from 'next';
import { View } from './views/View';
import { getUser } from '@requests/users';
import { GoogleAnalytics } from '@next/third-parties/google'

type PageProps = {
  user: User;
};

const Page: NextPage<PageProps> = ({ user }) => {
  return (
    <>
      <View user={user} />
      <GoogleAnalytics gaId={user.gaId} />
    </>
  );
};

export const getServerSideProps: GetServerSideProps<PageProps> = async ({
  query,
}) => {
  const name = getValidQueryString(query.name, {
    allowOnlyBasicChars: true,
  });

  if (!name)
    return {
      notFound: true,
    };

  try {
    const { user } = await getUser({
      name,
    });

    return {
      props: {
        user,
      },
    };
  } catch (e) {
    throw e;
  }
};
export default Page;

これでも計測自体はできますが、以下の課題が残ります。

  • ユーザーページ以外の、計測したくないページでも計測されてしまう

読み込んだタグがブラウザに残り、SPAのページ遷移、history_change イベントで Google Analytics4 の拡張計測機能「ブラウザの履歴イベントに基づくページの変更」が反応して他のページでも計測してしまっているようです。

waddy_uwaddy_u

計測したくないページでも計測されてしまう について

  • gtag('config', 'G-CSXXXXXXXX'); を済ませる
  • 「ブラウザの履歴イベントに基づくページの変更」が有効

この状況だと、history_change では gtag('config', 'G-CSXXXXXXXX'); が生きたままなので、GA4側のスクリプトで履歴変更が検知されて page_view が送信されてしまう、という理屈。

これに対して、 window['ga-disable-'G-CSXXXXXXXX'] = true を設定する方法があります。

https://developers.google.com/tag-platform/security/guides/privacy?hl=ja#turn-off-analytics

本当はオプトアウトに使うみたいです。

作戦としては、ページ離脱時に window['ga-disable-'G-CSXXXXXXXX'] = true を設定することで次のページでの計測を無効にするというもの。

function enableTracking(trackingId: string) {
  window[`ga-disable-${trackingId}`] = false;
}
function disableTracking(trackingId: string) {
  window[`ga-disable-${trackingId}`] = true;
}
  useEffect(() => {
    if (!gaTrackingId) return;

    // 計測を有効化
    enableTracking(gaTrackingId);    

    // ページ遷移時に計測をオフにする
    return () => {
      disableTracking(gaTrackingId);
    };
  }, [gaTrackingId]);

Scriptタグを使ったGAの埋め込みと、無効化を組み合わせればイケる?

イメージ的にはこういうことかと。(データレイヤー名などの変数は省略しています)

GoogleAnalyticsForPage.tsx
import React, { useEffect } from 'react'
import Script from 'next/script'

import type { GAParams } from '../types/google'

function enableTracking(trackingId: string) {
  window[`ga-disable-${trackingId}`] = false;
}
function disableTracking(trackingId: string) {
  window[`ga-disable-${trackingId}`] = true;
}

export function GoogleAnalyticsForPage(props: GAParams) {
  const { gaId, debugMode, dataLayerName = 'dataLayer', nonce } = props

  useEffect(() => {
    if (!gaTrackingId) return;

    // ページ遷移時に計測をオフにする
    return () => {
      disableTracking(gaTrackingId);
    };
  }, [gaTrackingId]);

    // 計測を有効化
  enableTracking(gaTrackingId);

  return (
    <>
      <Script
        id="_next-ga-init"
        dangerouslySetInnerHTML={{
          __html: `
          window['${dataLayerName}'] = window['${dataLayerName}'] || [];
          function gtag(){window['${dataLayerName}'].push(arguments);}
          gtag('js', new Date());

          gtag('config', '${gaId}' ${debugMode ? ",{ 'debug_mode': true }" : ''});`,
        }}
        nonce={nonce}
      />
      <Script
        id="_next-ga"
        src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
        nonce={nonce}
      />
    </>
  )
}

これでもいいかもしれませんね(試していないです)。レンダリング中に window[ga-disable-${trackingId}] = false; しているので、クライアントサイドでレンダリングすることを強制しなければならない点に注意です。

waddy_uwaddy_u

3. _document.tsxgtag('js', new Date()); までを行い、 gtag('config', 'G-CSXXXXXXXX'); をhooksで実行する

これがどこから湧いたかというと、このーページです。

https://developers.google.com/tag-platform/gtagjs/configure?hl=ja#set_up_data_collection

<head>
 ...
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments)};
  gtag('js', new Date());
  gtag('config', 'GT-XXXXXX');
  gtag('config', 'DC-ZZZZZZ');
</script>
</head>

複数のタグを設置するには、gtag/js が読み込まれた状態で、tag('config', ''); を計測ID分だけ呼び出せば良いことがわかります。ですので _document.page.tsx を次のような状態にしてみます。

_document.page.tsx
type MyDocumentProps = {};

class MyDocument extends Document<MyDocumentProps> {
render() {
return (
  <script
    id="ga"
    src="https://www.googletagmanager.com/gtag/js"
    }}
  />
  <script
    id="ga_load"
    dangerouslySetInnerHTML={{
      __html: `
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
      `,
    }}
  />
);
);

export default MyDocument;

gtag('config', 'G-CSXXXXXXXX') は hooksにする

コンポーネント化の話で、離脱時に window[ga-disable-${trackingId}] = false; する必要があるということでした。このやり方の場合も同様です。gtag('config', 'G-CSXXXXXXXX') もクライアントサイドで実行したい事情を考えると、useEffectにまとめてしまうのが筋がよさそうです。

useTrackingGA.ts
import { useRouter } from 'next/router';
import { useEffect } from 'react';

declare global {
  // eslint-disable-next-line no-unused-vars
  interface Window {
    [key: string]: any;
  }
}
function enableTracking(trackingId: string) {
  window[`ga-disable-${trackingId}`] = false;
}
function disableTracking(trackingId: string) {
  window[`ga-disable-${trackingId}`] = true;
}

function useTrackGA({
  gaTrackingId,
}: {
  gaTrackingId?: null | string;
}) {
  const { asPath } = useRouter();

  useEffect(() => {
    if (!gaTrackingId || !asPath) return;

    // この時点ではまだGAは無効。「ブラウザの履歴イベントに基づくページの変更」が反応しないように少し時間をおく

    // 0.5秒あとにGA有効化、計測開始
    setTimeout(() => {
      enableTracking(gaTrackingId);
      window.gtag('config', gaTrackingId,
        // configでPVを送信するか制御できるが、OFFにして個別でPVを送る方針に統一する
        // ちなみに、ここのPV送信をOFFにしても 。「ブラウザの履歴イベントに基づくページの変更」はOFFにできない…
        send_page_view: false
      });
      window.gtag('event', 'page_view', {
        page_path: asPath,
        send_to: gaTrackingId,
      });
    }, 1000);
    

    // ページ遷移時にイベントの送信をオフにする
    return () => {
      console.log('disableTracking', gaTrackingId);
      disableTracking(gaTrackingId);
    };
  }, [asPath, gaTrackingId]);
}

これを任意のぺージで呼び出して利用します。なぜ gtag('config', G-CSXXXXXXXX) では send_page_view: false とし、あえて window.gtag('event', 'page_view') としてPVイベントを送っているか。「ブラウザの履歴イベントに基づくページの変更」に対応するためです。

動作を整理しながらみていきます。

waddy_uwaddy_u

初回ロード時 ページA

ページAで useTrackGA を呼び出しているとします。

  1. _document.page.tsx の共用gtagスクリプトがダウンロードされる
  2. ページAがマウントされる。useTrackGAが呼び出される。中身はuseEffectなのでレンダリング完了を待つ
  3. 0.5秒後
    4. enableTracking(gaTrackingId);
    5. window.gtag('config', gaTrackingId) PVは送信されない
    6. window.gtag('event', 'page_view') PVが送信される

結果として、configが完了し、PVが1つ送信された状態になります(理想)。ここから、SPAとして別のページへ遷移した状況へ移ります。

ページ遷移時 ベージB

ページBでも useTrackGA を呼び出しているとします。

  1. ページAnoコンポーネントがアンマウントされる
    8. disableTracking(gaTrackingId);G-CSXXXXXXXX無効】
  2. ページBへルーティング開始【G-CSXXXXXXXX無効】
  3. 遷移先のコンポーネントがマウントされる。useTrackGAが呼び出される。中身はuseEffectなのでレンダリング完了を待つ【G-CSXXXXXXXX無効】
  4. ルーティングが完了し、「ブラウザの履歴イベントに基づくページの変更」を検知、PVを送信しようとするが、オプトアウトされているためなにもしない【G-CSXXXXXXXX無効】
  5. 0.5秒後
    13. enableTracking(gaTrackingId);G-CSXXXXXXXX有効】
    14. window.gtag('config', gaTrackingId) PVは送信されない
    15. window.gtag('event', 'page_view') PVが送信される

「ブラウザの履歴イベントに基づくページの変更」をオプトアウトで無力化し、こちらで用意したpage_viewイベントが送信されるようにします。これで、ページロード時も、クライアントサイドでのページ遷移時も、計測したいページでPVがひとつカウントされるようになりました。

waddy_uwaddy_u

PVが2カウント計測されてしまう場合?

10と11が入れ替わってしまうことがあるようです。0.5秒待つ箇所を1秒にしてみるなど試して下さい。あまり伸ばしすぎると、ユーザーがすぐに離脱した場合にPVを計測できなかったりするため注意してください。