APIでフェッチした任意のGoogleAnalytics計測IDをクライアント側で設定する(Next.js)
背景
計測ID = G-CSXXXXXXXX
こういうのです。
固定の計測IDや、NEXT_PUBLIC_
で取得できるケースのサンプルはたくさんあります。
ここでは、動的に変化する、例えば同じサイトのページごとに異なる計測IDをアプリに設定したいとき、どのようなコードを組めばいいか考えます。
Google Tag Manager vs Google Analytics ダイレクト
自分のサイトを計測する場合は、Google Tag Manager を挟むのが無難です。
- アプリケーションを変更せずにカスタムイベントを発火できる
- アプリケーションを変更せずにカスタムイベントの発行を中止できる
- 一度作成したトリガー条件(ここをClickしたら、など)はさまざまなカスタムイベントに流用できる
一方、以下のようなケースではGoogle Analytics4のタグを直接埋め込むことを考えます。
- GTMのようにカスタムするまでもないケース
- GTMのようにカスタムさせたくないケース
- とりあえず計測さえできれば良いケース
シチュエーションとして「サイトにログイン可能な登録ユーザーが、自分の名前のslug配下のページを計測するために、GA4タグを埋め込める」を想定します。
番外編:これは?
登録・削除でシステム境界を超えないといけないのと、「このページでは計測する、このページでは計測しない」ということができそうにない(できるとしても大変)なので却下
どのような方法があるか
ログインユーザーの 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>
埋め込む方法の候補↓
-
_document.tsx
で素直に埋め込む(動的な要件に対応できないので却下) - 「Next.js の Script タグで埋め込む」コンポーネントにする
-
_document.tsx
でgtag('js', new Date());
までを行い、gtag('config', 'G-CSXXXXXXXX');
をhooksで実行する
2. Script タグを使った コンポーネントにする
それを実装したNext.jsの公式パッケージがある。
利用イメージ
Next.js の Pageコンポーネントを考えます(App Routerじゃなくてごめん)
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 の拡張計測機能「ブラウザの履歴イベントに基づくページの変更」が反応して他のページでも計測してしまっているようです。
計測したくないページでも計測されてしまう について
-
gtag('config', 'G-CSXXXXXXXX');
を済ませる - 「ブラウザの履歴イベントに基づくページの変更」が有効
この状況だと、history_change では gtag('config', 'G-CSXXXXXXXX');
が生きたままなので、GA4側のスクリプトで履歴変更が検知されて page_view が送信されてしまう、という理屈。
これに対して、 window['ga-disable-'G-CSXXXXXXXX'] = true
を設定する方法があります。
本当はオプトアウトに使うみたいです。
作戦としては、ページ離脱時に 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の埋め込みと、無効化を組み合わせればイケる?
イメージ的にはこういうことかと。(データレイヤー名などの変数は省略しています)
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;
しているので、クライアントサイドでレンダリングすることを強制しなければならない点に注意です。
_document.tsx
で gtag('js', new Date());
までを行い、 gtag('config', 'G-CSXXXXXXXX'); をhooksで実行する
3. これがどこから湧いたかというと、このーページです。
<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
を次のような状態にしてみます。
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
にまとめてしまうのが筋がよさそうです。
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イベントを送っているか。「ブラウザの履歴イベントに基づくページの変更」に対応するためです。
動作を整理しながらみていきます。
初回ロード時 ページA
ページAで useTrackGA
を呼び出しているとします。
-
_document.page.tsx
の共用gtagスクリプトがダウンロードされる - ページAがマウントされる。useTrackGAが呼び出される。中身はuseEffectなのでレンダリング完了を待つ
- 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
を呼び出しているとします。
- ページAnoコンポーネントがアンマウントされる
8.disableTracking(gaTrackingId);
【G-CSXXXXXXXX
無効】 - ページBへルーティング開始【
G-CSXXXXXXXX
無効】 - 遷移先のコンポーネントがマウントされる。useTrackGAが呼び出される。中身はuseEffectなのでレンダリング完了を待つ【
G-CSXXXXXXXX
無効】 - ルーティングが完了し、「ブラウザの履歴イベントに基づくページの変更」を検知、PVを送信しようとするが、オプトアウトされているためなにもしない【
G-CSXXXXXXXX
無効】 - 0.5秒後
13.enableTracking(gaTrackingId);
【G-CSXXXXXXXX
有効】
14.window.gtag('config', gaTrackingId)
PVは送信されない
15.window.gtag('event', 'page_view')
PVが送信される
「ブラウザの履歴イベントに基づくページの変更」をオプトアウトで無力化し、こちらで用意したpage_view
イベントが送信されるようにします。これで、ページロード時も、クライアントサイドでのページ遷移時も、計測したいページでPVがひとつカウントされるようになりました。
PVが2カウント計測されてしまう場合?
10と11が入れ替わってしまうことがあるようです。0.5秒待つ箇所を1秒にしてみるなど試して下さい。あまり伸ばしすぎると、ユーザーがすぐに離脱した場合にPVを計測できなかったりするため注意してください。