タグ: アクセス解析

  • 除外しているつもりで、1件も除外していなかった

    除外しているつもりで、1件も除外していなかった

    アクセス解析に、自分のアクセスを入れないための設定があります。ずっと「テスト」のままになっていたので、「有効」に切り替えました。

    それでも、何も除外されません。設定がもう一段、奥にありました。

    この記事で扱うこと

    自分のアクセスを除外していたつもりで、1件も除外できていなかった話です。設定が2段構えになっていること、片方だけでは何も起きないこと、そしてそれが画面からは分からない理由を書きます。

    印を付ける側と、捨てる側

    この除外は、2つの設定が噛み合ってはじめて動きます。

    置き場所 役割
    内部トラフィックの定義 指定したIPからのアクセスに「内部」という印を付ける
    データフィルタ その印が付いたものを捨てる

    片方だけでは何も起きません。印を付けても捨てる設定が無ければ残りますし、捨てる設定があっても印が付いていなければ、捨てる対象が存在しません。

    捨てる側だけがあった

    確認したところ、こうなっていました。

    設定 状態
    データフィルタ あり(内部の印が付いたものを除外)
    内部トラフィックの定義 ルールが1件も作られていない

    つまり、どのアクセスにも「内部」の印が付いていませんでした。フィルタは、一致するものが1件も無い状態で回っていたことになります。

    「テスト」のままだったことを問題だと思っていましたが、有効にしたところで結果は変わりません。除外対象がゼロだからです。

    なぜ気づかなかったか

    一覧の画面には、フィルタが1件、名前も種類も状態も表示されていました。

    設定が「ある」ことと、「効いている」ことは別

    見えるのは、フィルタという入れ物があることだけです。その中身が参照している印を、誰が付けているのかまでは出てきません。

    設定の一覧は、設定した本人が覚えている前提で作られています。半年前の自分が何をしたかは、そこには書いてありません。

    もうひとつ、状態の表示も助けになりませんでした。「テスト」は評価はするが捨てないという意味で、一致が0件かどうかは区別されません。一致が無いことと、一致したが捨てていないことが、同じ表示になります。

    効いていないことを、どう確かめるか

    この種の設定は「何も起きないこと」が正常な状態なので、確かめ方を別に持っておく必要があります。

    噛み合っているかを、1本の線で追う

    除外の設定を確認するときは、フィルタの画面を見るだけでは足りません。

    「誰が印を付けるのか」から順に辿る必要があります。今回で言えば、IPのルールがあるか、そのルールが付ける印の名前が、フィルタが探している名前と一致しているか、という2点です。

    実際に確認したら、印を付ける側の画面には「ルールはまだ作成されていません」と1行だけ書かれていました。探しに行かなければ、ずっと見ない場所です。

    直すときに気をつけたこと

    ルールを作るには、自分のIPアドレスを登録します。ここで、手元のメモに残っていた古い値を使うのはやめました。

    古いIPは、いま別の人が使っている

    家庭の回線のIPは、契約が同じでも変わることがあります。

    変わったあとの値を登録すると、自分は除外されず、代わりに無関係な誰かが除外されます。しかも、どちらも画面からは分かりません。

    設定画面に現在のIPを表示するリンクがあるので、そこで確認した値を使いました。複数の回線から見るなら、条件を並べて足せます。

    さかのぼっては、消えない

    最後にひとつ。この設定が効くのは、これから溜まるデータだけです。

    これまでに入ってしまった自分のアクセスは、あとから消えません。過去の数字は、自分が混ざったまま読むことになります。どのくらい混ざっているかも分からないので、公開直後の数字は参考程度に扱うのが正しい、ということになりました。

    今回の学び

    まず、設定が2段構えのときは、片方だけ直しても何も変わらないということ。「テストを有効にする」は正しい操作でしたが、直す場所がそこではありませんでした。

    次に、一覧に出ているのは入れ物だけだということ。名前も種類も状態も表示されますが、それが誰と噛み合う想定なのかは出てきません。設定を確認するときは、一覧ではなく経路を辿る必要がありました。

    そして、古い値を使い回さないこと。合っていれば自分が消え、間違っていれば他人が消えます。どちらの結果も画面には出ないので、確かめられる方法で取り直すほうが安全でした。

  • ソース解説(3) 旗を折るのではなく、読み込まない

    ソース解説(3) 旗を折るのではなく、読み込まない

    クッキー同意のバナーは、置くこと自体は難しくありません。難しいのは、「同意していない人には何も送っていない」と自分で言い切れる形にすることでした。

    今回はその部分のコードを読んでいきます。保存する形、版番号を持たせた理由、そして「読み込まない」という選び方です。

    この記事で扱うこと

    同意の記録をどう保存しているか、壊れた記録をどう扱うか、離れた部品へどうやって変化を伝えているか、そしてJavaScriptが動かない環境で何をしないことにしたかです。

    止めているのは1つだけ

    最初に決めたのは、何に対して同意を求めるかです。

    このサイトが読み込むものを数えると、外部へ通信するのはタグマネージャー1つでした。書体は自前で配信しているので提供元への通信は起きません。紹介リンクは、押して移動したときに初めて相手へ届きます。問い合わせの送信先は、送信ボタンを押したときだけ呼ばれます。

    項目を増やすほど、選んでもらえなくなる

    読了状態と明暗の設定も保存していますが、こちらはサイトの中で完結していて外へ出ません。止める理由が無いので、同意の対象に入れていません。

    並べるのは、実際に止まるものだけにしています。止まらないものを並べると、選ぶ手間だけが増えます。

    保存する形

    記録はこの形です。

    export type ConsentRecord = {
      analytics: boolean;
      decidedAt: string;
      version: number;
    };

    保存先はCookieではありません。サーバー側がこの値を使わないからです。Cookieはリクエストのたびに自動で送られますが、送り先に使い道がありません。ブラウザの中だけに置いています。

    decidedAt を持っているのは、あとから「いつ同意したか」を示せるようにするためです。

    版番号を持たせた理由

    version が、この設計でいちばん考えた部分です。

    const VERSION = 1;

    何に同意を求めるかが変わったら、この数字を上げます。版が違う記録は「まだ答えていない」として扱い、もう一度たずねます。

    古い同意を、新しい送信先に流用できない

    同意は「そのとき説明されたものに対して」もらっています。

    あとから送信先を1つ足したときに、以前の同意をそのまま使い回すと、その人が知らない相手へ黙って送り始めることになります。聞き直す手間より、そちらのほうが問題として大きい。

    版番号は、その判断を忘れても実行される形にしたものです。

    壊れている記録は、無かったことにする

    読み出す側は、自分で書いた値でも疑っています。

    const parsed: unknown = JSON.parse(raw);
    if (typeof parsed !== "object" || parsed === null) return null;
    
    const record = parsed as Partial<ConsentRecord>;
    if (typeof record.analytics !== "boolean") return null;
    if (record.version !== VERSION) return null;

    型を unknown で受けているのが要点です。保存されている文字列が、こちらの期待する形である保証はありません。古い形式が残っていることも、別のタブで書き換わっていることもあります。

    全体が try で囲んであり、保存が使えない環境では例外が出ます。そのときも null、つまり「同意していない」扱いになります。

    壊れたときに、どちらへ倒れるか

    プライベートモードなどで保存が読めないと、答えが分かりません。

    分からないときに送る側へ倒すか、送らない側へ倒すか。ここでは送らない側に倒しています。判断がつかない状態を「同意あり」として扱う理由がありません。

    書き込む側も同じ考え方です。保存に失敗しても、そのページのあいだは選んだとおりに動かします。記録が残らないので次に開いたときはまたたずねますが、「保存できないから同意も無かったことにする」よりは筋が通っています。

    離れた部品へ、どう伝えるか

    記録するのはバナー、それを見て読み込むかどうか決めるのはタグマネージャー側の部品です。2つは画面上の別の場所にあり、親子の関係もありません。

    保存の仕組みには「変わったよ」と教えてくれる機能がないので、自分で合図を投げています。

    window.dispatchEvent(
      new CustomEvent<ConsentRecord>(CONSENT_CHANGED, { detail: record }),
    );

    受け取る側は、こう待っています。

    useEffect(() => {
      const sync = () => setGranted(readConsent()?.analytics === true);
      sync();
      window.addEventListener(CONSENT_CHANGED, sync);
      return () => window.removeEventListener(CONSENT_CHANGED, sync);
    }, []);

    最初に1回読んで、そのあとは合図が来るたびに読み直します。最後の return は後片付けで、これを書かないと画面を移動するたびに待ち受けが増え続けます。

    旗を折るのではなく、読み込まない

    同意の実装には2通りあります。先に読み込んでおいて「送ってよいか」の旗だけ切り替えるやり方と、同意までそもそも読み込まないやり方です。

    ここでは後者にしました。

    if (!GTM_ID || !granted) return null;

    この1行で、同意が無いあいだは何も出力されません。

    前者だと、自分のコードでは確かめられない

    先に読み込む方式では、同意していない人のブラウザにもタグの本体が届いています。何を送る・送らないの判断が相手側の設定に移るので、こちらのコードを全部読んでも「同意前は何も送っていない」とは言い切れません。

    読み込まなければ、通信そのものが起きません。確かめたいことと、確かめられることが一致します。

    代償として、同意しなかった人の分は数字に出ません。それは正しく「同意しなかった人がいる」という事実なので、受け入れています。

    JavaScriptが動かない環境で、何をやめたか

    タグマネージャーには、JavaScriptが無効な環境向けに noscript の中へ枠を1つ置く決まりがあります。それを置くのをやめました。

    同意は、ブラウザに保存した記録を読んで判断しています。JavaScriptが動かない環境では、その記録を読むことも、同意をたずねることもできません。それでも枠だけは動きます。

    置いたままにすると、「同意を確かめられない人にだけ、確かめずに送る」ことになります。分からないときに送らない側へ倒す、という前提をここでも通しました。

    今回の学び

    まず、同意は対象とセットでしか意味を持たないということ。何に対する同意かが変われば、前の同意は使えません。版番号は、その判断を人の記憶に頼らず実行させるための仕掛けでした。

    次に、分からないときにどちらへ倒れるかを、先に決めておくこと。読めない・保存できない・確かめられない、という状態は必ず起きます。そのとき何が起きるかは、書いた時点で決まっています。

    そして、自分のコードで説明できる形を選ぶこと。旗を切り替える方式でも、おそらく実際には送られません。ただ、それは相手の設定を信じているだけで、こちらからは確かめられない。言い切れるかどうかで選びました。

  • 「送っていない」と言い切るために、同意まで読み込まないことにした

    「送っていない」と言い切るために、同意まで読み込まないことにした

    クッキーの同意を取る仕組みを入れました。作りながら一番考えたのは、見た目や文言ではなく、「同意していない状態」をどう表すかでした。

    この記事で扱うこと

    止める対象の数え方、旗を立てる方式と読み込まない方式の違い、JavaScriptが無い環境の扱い、そして同意の記録に版を持たせた理由です。法律の解釈ではなく、作りの話です。

    止める対象を、先に数えた

    最初にやったのは、ページを開いたときに外部へ通信するものを数えることでした。

    送り先 いつ通信するか 同意の対象か
    タグマネージャー(解析) ページを開いた瞬間 対象
    アフィリエイトのリンク先 押して移動したときだけ 対象外
    問い合わせの送信先 送信ボタンを押したときだけ 対象外
    書体 自前で配信しているので通信しない 対象外

    結果、止める必要があるのは1つだけでした。読了状態と画面の明暗もブラウザに保存していますが、こちらはサイトの中で完結していて外部へは出ません。

    項目を増やすほど、選べなくなる

    同意の画面に細かい分類を並べたくなりますが、実際には止まらないものを並べても、選ぶ手間が増えるだけです。

    読んでいる途中で急に出てくる画面なので、そこに置いた選択肢の数がそのまま負担になります。数えた結果が1つなら、1つだけ出すのが正しい。

    旗を立てるか、読み込まないか

    同意していない状態の表し方には、大きく2通りあります。

    ひとつは、タグを先に読み込んでおいて「送ってよいか」の旗だけを切り替えるやり方。もうひとつは、同意までそもそも読み込まないやり方です。

    旗を切り替える 読み込まない
    同意前のブラウザ タグ本体は届いている 何も届いていない
    送る・送らないの判断 相手側の設定にもある こちらのコードだけで決まる
    確かめ方 設定を含めて追う必要がある 通信が0件かを見れば済む
    同意しなかった人の数字 推定として残ることがある 残らない

    後者を選びました。

    確かめたいことと、確かめられることを一致させる

    確かめたいのは「同意前は何も送っていない」ことです。

    旗を切り替える方式では、タグ本体は相手のブラウザに届いています。何を送るかの判断が相手側の設定に移るので、こちらのコードを最後まで読んでも、送っていないと言い切れません。設定画面まで含めて追う必要が出ます。

    読み込まなければ、通信が起きる余地そのものがありません。開発者ツールで通信の一覧を見て0件なら、それが答えになります。実際に本番で測って、同意前は0件、「同意する」を押した瞬間に読み込まれることを確認しました。

    代償として、同意しなかった人の分は数字に出ません。これは正しく「同意しなかった人がいる」という事実なので、受け入れています。

    JavaScriptが無い環境をどうするか

    タグマネージャーには、JavaScriptが無効な環境向けにnoscriptで画像枠を置く決まりがあります。これを外しました。

    同意はブラウザに保存した記録を読んで判断しています。JavaScriptが動かない環境では、その記録を読むことも、同意をたずねることもできません。それでもnoscriptの中身だけは動きます。

    置いたままにすると、同意を確かめられない人にだけ、確かめずに送ることになります。分からないときに送らない側へ倒すのがこの仕組みの前提なので、ここでも送らないほうを選びました。

    バナーは操作を止めていない

    画面の中央に出して、選ぶまで先へ進めなくする作り方もあります。採らなかったのは、止める必要が無いからです。

    先に読み込みを止めてあるので、答えないまま読み進めても何も送られません。止めなければ計測が始められない作りなら、選ばせるまで通さない意味があります。先に止めてあるなら、たずねるほうは静かでよいはずです。

    ボタンは2つとも同じ大きさにしました。片方を小さくしたりリンクのように見せたりすると、選べるように見せかけて実際には選びにくい形になります。

    同意の記録に版を持たせた

    保存している記録には、選択と日時のほかに版の番号を入れています。何に同意を求めるかが変わったら、この数字を上げます。版が違う記録は「まだ答えていない」として扱い、もう一度たずねます。

    同意は「そのとき説明されたものに対して」もらっています。あとから送信先を足したときに古い同意を流用すると、その人が知らない相手へ黙って送り始めることになります。聞き直す手間より、そちらのほうが問題が大きいと考えました。

    今回の学び

    まず、選択肢の数は、数えてから決めること。分類を先に用意すると、実際には止まらないものまで並びます。外部へ通信するものを数えたら1つだったので、聞くことも1つになりました。

    次に、「していない」を証明できる形にしておくこと。旗を切り替える方式でも、実際には送られません。ただし、送っていないことをこちらのコードだけで示せません。同じ結果でも、確かめられるかどうかは別の性質です。

    そして、分からないときにどちらへ倒すかを、先に決めておくこと。JavaScriptが無い環境では同意を確かめられません。そのときに送るのか送らないのかは、個別に迷うと毎回違う結論になります。「分からないなら送らない」と決めておけば、判断が要らなくなります。

  • GTMの設定は誰でも読める

    GTMの設定は誰でも読める

    GTMのコンテナIDをURLに打ち込むと、その設定が誰でも読めます。

    開いてみる
    https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX

    返ってくるのはJavaScriptですが、その中にどんなタグが、どんな条件で発火するかが全部入っています。ログインは要りません。これは不具合ではなく、そうでなければ動かない仕組みです。

    この記事で扱うこと

    なぜコンテナの中身が公開されているのか、そこから実際に何が読めるのか、そして「だから何をしてはいけないか」。計測IDが公開情報であることと、その先で何が見えるかは、分けて考える必要があります。

    公開されていないと動かない

    理由は考えてみれば当たり前です。

    GTMは、閲覧者のブラウザの中で動きます。ブラウザが設定を読み込めなければ、タグを発火させようがありません。そしてブラウザが読めるということは、人間も読めるということです。

    置き場所 誰が読めるか
    GTMのコンテナ設定 閲覧者のブラウザ 誰でも
    サーバー側の処理 自分のサーバー 自分だけ

    ブラウザで動くものに秘密は持てません。これはGTMに限った話ではなく、フロントエンドに置いたものはすべて公開されているという一般則の一例です。JavaScriptを難読化しても、通信を暗号化しても、最終的にブラウザが解釈する以上は読めます。

    何が読めるのか

    このサイトのコンテナを例にすると、次のようなことが分かります。

    読めること この例では
    使っている計測ツール GA4(測定ID付き)
    タグの名前 GA4 – article_read など
    発火の条件 カスタムイベント article_read
    送っているパラメータ名 article_slug / article_title
    データレイヤー変数の名前 同上

    つまり「このサイトは何を測っているか」が丸ごと見えます。競合他社のサイトを調べる手段としても使われますし、それ自体は行儀の悪いことではありません。

    逆に言えば、あなたのサイトも同じように見られています。計測の設計を秘密にしておくことはできない、と最初から考えておくのが正しい前提です。

    だから、書いてはいけないもの

    ここが実務上いちばん大事な帰結です。

    カスタムHTMLタグに秘密の値を書かない

    GTMには「カスタムHTML」というタグの種類があり、任意のJavaScriptを書けます。便利なので、外部サービスのAPIキーやトークンを直接書いてしまう事故が起こります。それは公開されたテキストファイルに書くのと同じです。コンテナを公開した瞬間、世界中から読めます。

    同じ理由で、次のようなものも置けません。

    社内向けの管理画面のURL、テスト環境のアドレス、まだ発表していないキャンペーンの名前。タグの名前や変数の名前にも情報が乗ります。「新製品X_LP_2026春」というタグ名を作れば、それも読めます。

    IDが公開なのと、悪用できるのは別

    「IDが見えるなら、誰かに偽のデータを送られるのでは」という心配が出ます。実際その通りで、GA4の測定IDが分かれば、第三者がデータを送ることは技術的にできます。

    ただしこれはIDを記事に書いたから起きるのではありません。サイトを開けば誰でも分かる以上、隠しようがないからです。

    心配 実際のところ
    設定を書き換えられる できない。読めるだけで、変更にはGTMアカウントの権限が要る
    偽のデータを送られる できる。ただしIDを隠しても防げない
    秘密の値を盗まれる 置かなければ盗まれない。ここだけが自分で決められる

    3行目だけが、こちらの判断でどうにでもなります。防げないことに対策を考えるより、防げることを確実にやるほうが効きます。

    偽データへの現実的な対処

    GA4にはホスト名で受信を制限する機能がありません。実務では、GTM側のトリガーに「ホスト名が自分のドメインのときだけ発火する」条件を足します。これはサイトを複製された場合に効きます。直接データを送りつけられるケースは、サーバー側で計測する構成にしない限り防げません。個人サイトでそこまでやる必要があるかは、別途考えるべきことです。

    公開前に自分で読んでみる

    実際に一度開いてみることを勧めます。自分のコンテナが何を晒しているかを、公開する前に知っておけます。

    確認の手順
    1. https://www.googletagmanager.com/gtm.js?id=(自分のコンテナID)を開く
    2. ページ内検索で、次のような語を探す
       - key / token / secret / password
       - 社内のドメイン名
       - 未発表の企画名

    何も出てこなければ問題ありません。見つかったら、公開を止めてそのタグを直します。一度公開したものは、消しても誰かの手元に残っている可能性があります。

    今回の学び

    まず、ブラウザで動くものに秘密は持てないということ。GTMのコンテナが公開されているのは設計の欠陥ではなく、そうでなければ動かないからです。「見えてしまう」ではなく「見えるように作られている」と理解すると、置いていいものと悪いものの線が引けます。

    次に、IDが公開なのと、悪用できるのは別の話だということ。読めることと書き換えられることは違います。混同すると、防げないことに時間を使い、防げることを見落とします。

    そして、名前にも情報が乗ること。値だけを気にして、タグ名や変数名を無防備にしてしまいがちです。読まれる前提で名前を付ける習慣は、持っておいて損がありません。

  • 「読み終えた」をどう数えるか

    「読み終えた」をどう数えるか

    「この記事は最後まで読まれたのか」を数えたい。

    GA4には最初からスクロール距離の計測がありますが、今回はそれを使いませんでした。使わなかった理由と、代わりに何を測ることにしたか。そして確認の途中で、自分の測り方のほうが間違っていたという話です。

    この記事で扱うこと

    スクロール率で読了を測ることの問題、サイト側にすでにある読了状態を使うという判断、「未読から読了に変わった瞬間」だけを送る実装。最後に、計測は必ず一部を取りこぼすという前提について。

    スクロール率では「読んだ」と言えない

    GA4の拡張計測には scroll というイベントがあり、1回の表示の中で画面が90%まで下がると発火します。設定を1つ入れるだけで使えます。

    それでも使わなかったのは、測っているものが違うからです。

    場面 scrollは発火するか 実際に読んだか
    最後まで読み進めた する 読んだ
    目次から最後の見出しへ飛んだ する 読んでいない
    末尾までスクロールだけした する 読んでいない
    同じ記事を3回開いた 3回する 読了は1回

    下3行が問題です。スクロール率が測っているのは「画面が下まで移動したか」であって、「その人が読み終えたか」ではありません。近い場面が多いので代用できているように見えますが、別のものです。

    特に最後の行は効きます。気に入った記事ほど開き直されるので、読まれている記事ほど水増しされるという、いちばん困る方向に歪みます。

    すでに答えを持っていた

    このサイトには、以前から読了状態を保存する仕組みがあります。記事を最後まで読むと、そのことをブラウザ(localStorage)に記録し、一覧に「読了済み」のバッジを出す機能です。

    つまり「この人がこの記事を読み終えたか」は、すでにサイトが知っていました。計測のために新しく測り直す必要はなく、その状態が変わった瞬間を送ればいい。

    測り直さない

    同じことを2か所で測ると、必ずどこかでずれます。ずれたとき、どちらが正しいかを毎回考えることになります。すでに持っている情報を使うほうが、増やす手間も、あとで悩む回数も少なくて済みます。

    「変わった瞬間」を返させる

    読了を記録する関数は、もともと何も返していませんでした。

    変更前
    export function markAsRead(slug: string): void {
      const current = getReadSlugs();
      if (current.includes(slug)) return;   // すでに読了なら何もしない
      // 保存する
    }

    ここで注目したいのは、この関数はすでに「読了済みかどうか」を知っているということです。3行目で判定しています。知っているのに、その情報を捨てていました。

    そこで、戻り値を持たせました。

    変更後
    export function markAsRead(slug: string): boolean {
      try {
        const current = getReadSlugs();
        if (current.includes(slug)) return false;   // 再訪
    
        window.localStorage.setItem(KEY, JSON.stringify([...current, slug]));
        return true;                                // 新しく読了になった
      } catch {
        // 保存できない環境(プライベートモード等)では判定材料が無い。
        // 呼び出し側がページ内で1回に制限しているので、そこで歯止めがかかる。
        return true;
      }
    }

    呼び出し側は、これが true のときだけ計測へ送ります。

    MarkAsRead.tsx(要点)
    const isNewlyRead = markAsRead(slug);
    if (!isNewlyRead) return;
    
    window.dataLayer.push({
      event: "article_read",
      article_slug: slug,
      article_title: title,
    });

    結果として、1つの記事から送られる読了は、そのブラウザにつき1回だけになります。再訪しても増えません。読了数が再訪回数に化けることがなくなりました。

    呼び出し側で判定しなかった理由

    「読了済みかどうか」を呼び出し側でもう一度調べることもできました。しかしそうすると、同じ判定が2か所に存在します。片方の条件を変えたときに、もう片方を直し忘れます。すでに判定している場所に、判定結果を返させるほうが素直でした。

    タグマネージャー側の設定

    サイトから送られた article_read を、GTMで受け取ってGA4へ流します。作ったのは変数2つ、トリガー1つ、タグ1つです。

    GTMの構成
    変数     DLV - article_slug     データレイヤー変数 article_slug
    変数     DLV - article_title    データレイヤー変数 article_title
    トリガー CE - article_read      カスタムイベント article_read
    タグ     GA4 - article_read     GA4イベント / イベント名 article_read
                                    パラメータに上の2変数を付与

    スラッグとタイトルを両方送っているのは、レポートでどの記事が読み切られたかを記事単位で見るためです。スラッグだけだと一覧が英数字の羅列になり、読めません。

    GTMでバージョンを公開した画面
    公開時には必ず説明を書く。半年後の自分が、どのバージョンに戻せばいいか判断できなくなるため。

    動かないときは、測り方を疑う

    実装が終わり、本番で確認しました。記事を最後までスクロールさせても、読了に切り替わりません。進捗表示は「あと100%」のままでした。

    コードを疑って何度か読み返しましたが、間違いは見つかりませんでした。そこで、確認に使っていたスクリプトのほうを調べました。

    やっていたこと
    document.documentElement.scrollTop = 一番下;

    スクロール位置は確かに変わっていました。しかし進捗を計算している処理は、スクロールイベントを受け取って動きます。この方法では、そのイベントが期待どおりに発生していませんでした。

    イベントを明示的に発生させたところ、即座に読了になり、計測も送られました。

    対象より先に、測り方を疑う

    「動かない」と思ったとき、疑う先はまず対象のコードになります。しかし今回、間違っていたのは確認する側でした。手で操作していれば一度で分かったはずのことに、時間を使いました。以前にも正規表現の数え方を間違えて、コードのせいだと思い込んだことがあります。同じ形の失敗です。

    数字が1件も届かない

    読了イベントがブラウザ内で発火することは確認できました。ところが、GA4の管理画面には「受信したデータはまだありません」と出たままでした。

    サイトからGTMへの受け渡しは、この目で確認しています。止まっているとすれば、その先です。ネットワークの記録を見ました。

    読み込まれていたもの
    chrome-extension://(拡張機能のID)/
        rule_resources/redirects/google-analytics_analytics.js

    広告ブロッカーが、Googleの計測スクリプトを無害化した別のファイルに差し替えていました。gtm.js の取得自体は成功(200)しているのに、コンテナは動いていない。読み込まれてはいるが、実行されていない状態です。

    別の端末で同じ記事を読んだところ、そちらは正しく届きました。

    GA4のリアルタイムでarticle_readが1件記録されている画面
    ページビューに混ざって article_read が1件。読了が数えられている。

    計測は必ず一部を取りこぼす

    この経験には、設定の話より大きな中身がありました。

    閲覧者の一定割合は広告ブロッカーを使っています。つまりアクセス解析の数字は、はじめから実数より少なく出ます。これは不具合ではなく、仕様として受け入れるしかない前提です。

    知らないと、2つの困ったことが起きます。ひとつは、正常なのに「壊れている」と考えて原因を探し続けること。実際、私はしばらくそう考えていました。もうひとつは、数字を実数だと思って判断してしまうことです。

    絶対値ではなく、傾向として読む

    取りこぼしがあっても、記事どうしの比較は成立します。どの記事がよく読まれたか、読了率が高いのはどれか。取りこぼす割合はどの記事でも似た程度なので、比べる分には使えます。「何人が読んだか」ではなく「どちらがよく読まれたか」を見る道具だと考えるのが実態に合っています。

    今回の学び

    まず、近い場面で発火するものは、同じものではないということ。スクロール率は読了と多くの場面で一致しますが、目次から飛んだときも、読まずに流したときも発火します。名前が似ているだけで代用すると、あとで数字の意味を説明できなくなります。

    次に、すでに知っている情報を捨てないこと。読了を記録する関数は、すでに「読了済みかどうか」を判定していました。その結果を返させるだけで、呼び出し側の二重判定がなくなりました。新しく調べるより、判定している場所に返させるほうが素直です。

    そして、動かないときは対象より先に測り方を疑うこと。今回いちばん時間を無駄にしたのがここでした。数が合わないときは、だいたい数え方のほうが間違っています。

    最後に、計測は完全ではないと知ったうえで使うこと。0件を見て慌てるか、「そういうものだ」と分かって傾向を読むか。前提を持っているかどうかで、同じ数字から引き出せるものが変わります。

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

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

    ページを10枚見た人も、1枚見て帰った人も、同じ「1ページビュー」として記録される。

    Next.jsのようにページを読み込み直さないサイトに計測を入れると、初期状態ではこうなります。数字は出ているのに、実態とまったく合っていません。今回はGoogleタグマネージャー(GTM)を挟んで、この問題を解いた記録です。

    この記事で扱うこと

    GTMを経由させる理由、コンテナとタグとトリガーの関係、そしてページ内遷移を計測に伝える仕組み。最後に、二重計上を防ぐための設定をひとつ入れています。ここが今回いちばん判断を要した部分でした。

    なぜ計測タグを直接貼らないのか

    GA4は、測定IDを含む数行をサイトに貼れば動きます。GTMを挟むと手順が増えます。それでも挟む理由は、ひとつだけです。

    計測の内容は必ず後から変わるからです。

    今はページビューだけで十分でも、そのうち「どのリンクが押されたか」「記事をどこまで読んだか」「連絡先を見た人は何人か」を知りたくなります。タグを直接書いていると、そのたびにコードを直し、ビルドし、デプロイすることになります。

    タグを直接貼る GTMを挟む
    最初の設置 数行貼るだけ コンテナ作成+タグ設定
    計測を1つ足す コード修正 → ビルド → デプロイ GTMの画面だけ
    設定を戻す コードを戻して再デプロイ 前のバージョンを公開し直す
    変更でサイトが壊れる可能性 ある ない

    最後の行が本質です。計測を変えるたびにデプロイするということは、計測の都合でサイトが壊れる機会を作っているということでもあります。本来関係のない2つのことが、同じ操作に結びついています。

    GTMを入れると、この2つが切り離せます。コードを触らずに計測を変えられる——これがGTMを挟む唯一にして最大の理由です。

    コンテナは既存アカウントの下に作った

    GTMもGA4と同じく入れ子構造で、アカウントの下にコンテナがあります。

    GTMの階層
    アカウント(ポートフォリオ)
      └ コンテナ(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はリンクを押してもページ全体を再読み込みしません。画面の中身だけを差し替えて、アドレスバーを書き換えます。ブラウザから見れば「同じページのまま」なので、計測タグは何も気づきません。

    そこで、アドレスが変わったことを自分で検知して知らせる部品をサイト側に置きました。

    RouteChangeTracker.tsx(要点)
    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 しています。副作用だけを持つコンポーネントです。

    Suspenseで包む必要がある

    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タグの「設定」→「構成パラメータ」に、以下を追加しています。

    Googleタグの構成パラメータ
    send_page_view : false

    これでGoogleタグはGA4を読み込むだけになり、ページビューを送るのは spa_page_view 経由の1本だけになります。

    発生源をひとつにする

    同じ数字が2か所から出てくる状態は、どちらが正しいかを常に考え続けることになります。片方を止めて発生源を1本にするほうが、計測を足すたびに悩まずに済みます。設定の数を減らすことより、考える回数を減らすことのほうが効きます。

    IDが無いときは何も出力しない

    サイト側の読み込み部分には、もうひとつ仕掛けを入れています。

    Analytics.tsx(要点)
    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側で止める案がありました。前者は判定条件がコードの中に隠れます。設定として見える場所に置いたほうが、あとから読み解けると判断しました。

  • GA4を入れる前に決めたこと

    GA4を入れる前に決めたこと

    サイトを作って公開しました。ここまで、誰がどのページを見ているのかを一度も見ていません。

    見なくても作れてしまうのが、個人サイトの怖いところです。手応えの代わりになるものが何もないまま、次に何を直すかを勘で決めることになります。今回はその状態を終わらせるために、Googleアナリティクス4(GA4)を入れました。

    この記事で扱うこと

    GA4のプロパティとデータストリームの作り方、そのときに聞かれる設問への答え方、測定IDの意味。そして「計測を入れる前に決めておくべきこと」。手順そのものより、なぜその選択をしたかを中心に書きます。

    計測を入れる前に決めたこと

    最初に、数字を見て何をするつもりなのかを決めました。これを決めずに入れると、毎日ダッシュボードを眺めて満足するだけの習慣ができます。

    このサイトで知りたいことは3つでした。

    知りたいこと それが分かると何ができるか
    どの記事が読まれているか 次に書く記事の題材を、勘ではなく実績で決められる
    どこから来ているか 検索経由が少なければ、書き方ではなく見つけられ方に問題があると分かる
    途中で離脱していないか 記事の構成や長さを見直す根拠になる

    逆に、追わないと決めたものもあります。ユーザー数の絶対値です。個人のポートフォリオで、月間の訪問者数を増やすこと自体を目標にすると、内容と関係のない集客に手が伸びます。ここでは「読まれ方の質」だけを見ることにしました。

    アカウントとプロパティの関係

    GA4は入れ子構造になっていて、最初にここでつまずきます。

    GA4の階層
    アカウント(KiSAKU)
      └ プロパティ(kisaku.site)        ← データが貯まる単位。レポートはここで見る
          └ データストリーム(ウェブ)    ← 計測タグを置く対象。1つのURL群に対応する

    覚え方としては、プロパティが「1つのサービス」、データストリームが「その入り口」です。ウェブサイトとiOSアプリを両方持っているなら、プロパティは1つでストリームが2つになります。同じサービスの利用者を、端末ごとに分断せずに見られます。

    逆に、まったく別のサイトを同じプロパティに入れてはいけません。レポートが混ざって、どちらの数字も読めなくなります。今回は kisaku.site 専用のプロパティを新しく作りました。

    作成時に聞かれること

    プロパティを作ると、いくつか設問が出ます。あとから変更できるものと、しにくいものがあります。

    項目 設定した値 あとから変えられるか
    タイムゾーン 日本(GMT+09:00) 変えられるが、過去のデータは変換されない
    通貨 日本円 同上
    業種 コンピュータ、電化製品 いつでも変更可
    ビジネスの規模 小規模 いつでも変更可
    ビジネス目標 トラフィックの分析/エンゲージメントと維持率の把握 いつでも変更可
    タイムゾーンだけは最初に正しく

    初期値はアメリカの時間帯です。そのままにすると「日付の変わり目」がずれ、日別レポートが実感と合わなくなります。しかも後から直しても、すでに記録されたデータは古い時間帯のままです。最初の1回しか正しく設定する機会がありません。

    業種と規模は、初期レポートの構成を決めるためのものです。間違えても実害はほとんどありません。ビジネス目標も同様で、選んだ内容に応じて左メニューの項目が変わるだけです。ここで悩んで手が止まるくらいなら、近いものを選んで先に進むべきです。

    データストリームと測定ID

    プロパティを作ると、続けてデータストリームの作成を求められます。ここで入力するのは計測対象のURLです。

    設定した内容
    ストリーム名 : kisaku.site ウェブ
    ウェブサイトのURL : https://www.kisaku.site
    ストリームID : 15464874599
    測定ID : G-872K3PN503

    URLに www を付けたのは、このサイトの正規のアドレスが www.kisaku.site だからです。kisaku.site(wwwなし)にアクセスすると、www 付きへ転送されます。転送先のほうを登録するのが正しい選び方です。

    ここで出てくる G- から始まる文字列が測定IDです。これがサイトとGA4のプロパティを結びつける鍵になります。

    測定IDは秘密ではない

    この値はサイトのHTMLに出力され、ブラウザの開発者ツールで誰でも見られます。隠す必要はありませんし、隠せません。パスワードやAPIキーと同じ扱いをする必要はない、と最初に理解しておくと迷わずに済みます。

    タグを直接貼らなかった理由

    GA4は、測定IDを含む数行のスクリプトをサイトに貼れば動きます。それがいちばん短い道です。

    しかし今回は貼りませんでした。代わりにGoogleタグマネージャー(GTM)を経由させています。

    理由は、計測の内容は必ず後から変わるからです。今はページビューだけで十分でも、そのうち「どのリンクが押されたか」「記事をどこまで読んだか」を知りたくなります。タグを直接書いていると、そのたびにコードを直し、ビルドし、デプロイすることになります。

    デプロイが必要ということは、計測を変えるたびにサイトが壊れる可能性が生まれるということでもあります。本質的に関係のない2つのことが、同じ操作に結びついてしまっています。

    GTMを挟むと、この2つが切り離せます。計測の変更はGTMの画面だけで完結し、サイトのコードには一切触れません。GTMの構成は次の記事で扱います。

    なぜ「入れただけ」では不十分なのか

    ひとつだけ、先に書いておきます。

    このサイトはNext.jsで作られていて、リンクを押してもページ全体を読み込み直しません。画面の中身だけを差し替えて、アドレスバーの表示を書き換えます。表示は速くなりますが、計測の側から見ると「1回読み込まれたきり、ずっと同じページを見ている」ようにしか見えません。

    この状態で放置すると、記事を10本読んだ人も、トップページだけ見て帰った人も、同じ「1ページビュー」として記録されます。

    入れて安心するのがいちばん危ない

    タグを貼ってリアルタイムに自分のアクセスが出れば、動いているように見えます。しかしこの状態のGA4は、実態とかけ離れた数字を、正しい顔で出し続けます。数字が無いより、間違った数字があるほうが判断を誤ります。

    この対処が、今回いちばん手間のかかった部分です。次の記事で扱います。

    今回の学び

    まず、計測は「入れる」より「何を見るか決める」ほうが先だということ。ツールは何でも記録してくれますが、見る対象を決めていないと、記録されたものを眺めるだけで終わります。追わないものを決めておくのも同じくらい大事でした。

    次に、最初の1回しか設定できない項目があること。タイムゾーンがそれでした。あとで直せる項目とそうでない項目が同じ画面に並んでいて、見た目からは区別できません。「変えられるか」を先に調べるだけで、後戻りの多くは防げます。

    そして、短い道が最善とは限らないこと。タグを直接貼れば5分で終わりました。GTMを挟んだぶん手順は増えましたが、この先の変更が全部コードの外側で済むようになります。一度きりの作業を短くするか、繰り返す作業を短くするかという選択でした。