ページを10枚見ても「1」と数えられる問題

ページを10枚見た人も、1枚見て帰った人も、同じ「1ページビュー」として記録される。
Next.jsのようにページを読み込み直さないサイトに計測を入れると、初期状態ではこうなります。数字は出ているのに、実態とまったく合っていません。今回はGoogleタグマネージャー(GTM)を挟んで、この問題を解いた記録です。
GTMを経由させる理由、コンテナとタグとトリガーの関係、そしてページ内遷移を計測に伝える仕組み。最後に、二重計上を防ぐための設定をひとつ入れています。ここが今回いちばん判断を要した部分でした。
なぜ計測タグを直接貼らないのか
GA4は、測定IDを含む数行をサイトに貼れば動きます。GTMを挟むと手順が増えます。それでも挟む理由は、ひとつだけです。
計測の内容は必ず後から変わるからです。
今はページビューだけで十分でも、そのうち「どのリンクが押されたか」「記事をどこまで読んだか」「連絡先を見た人は何人か」を知りたくなります。タグを直接書いていると、そのたびにコードを直し、ビルドし、デプロイすることになります。
| タグを直接貼る | GTMを挟む | |
|---|---|---|
| 最初の設置 | 数行貼るだけ | コンテナ作成+タグ設定 |
| 計測を1つ足す | コード修正 → ビルド → デプロイ | GTMの画面だけ |
| 設定を戻す | コードを戻して再デプロイ | 前のバージョンを公開し直す |
| 変更でサイトが壊れる可能性 | ある | ない |
最後の行が本質です。計測を変えるたびにデプロイするということは、計測の都合でサイトが壊れる機会を作っているということでもあります。本来関係のない2つのことが、同じ操作に結びついています。
GTMを入れると、この2つが切り離せます。コードを触らずに計測を変えられる——これがGTMを挟む唯一にして最大の理由です。
コンテナは既存アカウントの下に作った
GTMもGA4と同じく入れ子構造で、アカウントの下にコンテナがあります。
アカウント(ポートフォリオ)
└ コンテナ(www.kisaku.site) ← サイト1つに対して1つ
├ タグ : 何を送るか
├ トリガー : いつ送るか
└ 変数 : 何の値を送るか
今回は新しいアカウントを作らず、すでにあった「ポートフォリオ」アカウントの下にコンテナを追加しました。個人で管理するサイトが増えたとき、アカウントを分けるとどこに何があるか分からなくなるからです。同じ管理単位に並べておけば、一覧で見つかります。
アカウントを分けるべきなのは、アクセス権を分けたいときだけです。アカウント単位でユーザーを招待するので、他人に片方だけ渡したい場合は分ける必要があります。逆にそういう予定がないなら、分ける理由はありません。
作成すると GTM- から始まるコンテナIDが発行されます。これがサイト側に埋め込む値です。測定IDと同じで、HTMLに出力されるので秘密ではありません。
タグとトリガーという考え方
GTMの設定は、この2つの組み合わせで表されます。
| 用語 | 意味 | 今回の例 |
|---|---|---|
| タグ | 何を送るか | GA4にページビューを送る |
| トリガー | いつ送るか | ページ内で移動したとき |
「いつ」と「何を」を分けて持つのがGTMの設計です。同じタグを複数のトリガーで使い回せますし、同じトリガーで複数のタグを一斉に発火させることもできます。
今回作ったのは、タグ2つとトリガー1つです。
タグ1 : GA4 - Google タグ
種類 Google タグ
タグID G-872K3PN503
トリガー Initialization - All Pages
設定 send_page_view = false
タグ2 : GA4 - page_view (SPA)
種類 GA4 イベント
イベント名 page_view
トリガー CE - spa_page_view
トリガー : CE - spa_page_view
種類 カスタム イベント
イベント名 spa_page_view
タグ1がGA4本体の読み込み、タグ2がページビューの送信です。役割が分かれているところが今回の要点になります。
ページ内の移動を計測に伝える
ここからが本題です。
Next.jsはリンクを押してもページ全体を再読み込みしません。画面の中身だけを差し替えて、アドレスバーを書き換えます。ブラウザから見れば「同じページのまま」なので、計測タグは何も気づきません。
そこで、アドレスが変わったことを自分で検知して知らせる部品をサイト側に置きました。
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (!window.dataLayer) return;
const query = searchParams.toString();
const pagePath = query ? pathname + "?" + query : pathname;
window.dataLayer.push({
event: "spa_page_view",
page_path: pagePath,
page_title: document.title,
});
}, [pathname, searchParams]);
return null;
dataLayer はGTMが監視している配列です。ここに event を含むオブジェクトを積むと、GTM側で「カスタムイベント」として拾えます。サイトとGTMの間の唯一の連絡口だと思えば分かりやすいです。
この部品は画面に何も表示しません。return null しています。副作用だけを持つコンポーネントです。
useSearchParams を使う部品は、Reactの Suspense で囲まないとビルド時に警告が出ます。囲まないと、そのページ全体が「事前生成できない」扱いになり、訪問のたびにサーバーで組み立てられるようになります。速度改善のために事前生成を入れたばかりだったので、ここは見落とせませんでした。計測を足したせいでサイトが遅くなる、というのは本末転倒です。
GTM側では、このイベント名を待ち受けるトリガーを作ります。トリガーの種類は「カスタム イベント」、イベント名に spa_page_view と入れるだけです。あとはそのトリガーで、イベント名 page_view のGA4イベントタグを発火させます。
二重計上を止める
ここで問題が起きます。
Googleタグは、読み込まれた時点で自動的にページビューを1回送ります。一方、上の RouteChangeTracker は useEffect で動くので、最初に表示されたときにも一度発火します。
| 場面 | Googleタグの自動送信 | spa_page_view | 記録されるPV |
|---|---|---|---|
| 最初にページを開く | 1 | 1 | 2 |
| リンクで次のページへ | 0 | 1 | 1 |
最初の1ページだけが二重に数えられます。すべてのページが2倍になるならまだ気づけますが、入口のページだけが多く見えるという歪み方をします。これは気づきにくく、しかも「トップページが人気」という誤った結論を導きます。
解決方法は2つ考えられました。
| 案 | やること | 問題 |
|---|---|---|
| 初回だけ送らない | フロント側で「初回マウントかどうか」を判定して除外する | 判定の条件がコードの中に隠れる。読み込み直しとの区別も難しい |
| 自動送信を止める | Googleタグの send_page_view を false にする |
設定を1つ追加するだけ |
後者を選びました。Googleタグの「設定」→「構成パラメータ」に、以下を追加しています。
send_page_view : false
これでGoogleタグはGA4を読み込むだけになり、ページビューを送るのは spa_page_view 経由の1本だけになります。
同じ数字が2か所から出てくる状態は、どちらが正しいかを常に考え続けることになります。片方を止めて発生源を1本にするほうが、計測を足すたびに悩まずに済みます。設定の数を減らすことより、考える回数を減らすことのほうが効きます。
IDが無いときは何も出力しない
サイト側の読み込み部分には、もうひとつ仕掛けを入れています。
const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID;
export default function Analytics() {
if (!GTM_ID) return null;
// ... GTMの読み込み
}
環境変数が設定されていなければ、何も出力しません。これで2つのことが同時に解決します。
ひとつは、IDを取得する前にデプロイしても壊れないこと。コードを先に入れておいて、あとから設定を足せます。もうひとつは、手元の開発中の操作が本番の数値に混ざらないことです。自分でページを何度も開き直す作業をしている最中に計測が動いていると、その分がそのまま記録されます。
公開を忘れると何も起きない
GTMには最後にひとつ落とし穴があります。
タグを作って保存しただけでは、サイトには反映されません。右上の「公開」を押してはじめて配信されます。
作業中の状態を「ワークスペース」と呼び、公開すると「バージョン」として固定されます。バージョンには名前と説明を付けられるので、今回は次のように残しました。
名前 : v1 GA4初期設定
説明 : Googleタグ(G-872K3PN503、send_page_view=false)
+SPA用GA4イベントpage_view(カスタムイベントspa_page_view)
ここを空欄で公開してしまうと、半年後に見たときに何をしたバージョンなのか分かりません。過去のバージョンに戻せるのがGTMの利点ですが、戻す先が分からなければ意味がありません。説明を書くのは、その利点を使えるようにしておく作業です。
今回の学び
まず、速いサイトほど計測が壊れるということ。ページを読み込み直さない仕組みは体験としては良いのですが、計測の前提を静かに崩します。表示が速いことと、計測が正しいことは、別々に確かめる必要がありました。
次に、数字は「出ている」と「合っている」が違うこと。タグを貼ればリアルタイムに自分のアクセスが出るので、動いているように見えます。しかし中身は初回だけ2倍で、遷移は数えていない状態でした。動作確認の基準を「表示されるか」ではなく「意図した回数か」に置く必要があります。
そして、同じことを2か所で管理しないこと。二重計上の対処で、フロント側を賢くする案とGTM側で止める案がありました。前者は判定条件がコードの中に隠れます。設定として見える場所に置いたほうが、あとから読み解けると判断しました。