prodnote.dev本番ノート

Index/10

公開直後のアクセスは、大半が読者ではない — 9人は自分の開発環境だった

公開5日でGA4にユーザー66人。滞在0秒、再訪ゼロ、Direct 93.8%。ボットを疑ってホスト名の列を出したら、9人はlocalhostの自分でした。GA4が消すのは既知のボットだけで、消した量は見られないと公式に書かれています。

Published
Tags
GA4アクセス解析Search ConsoleCloudflare個人開発
明るい室内の床にしゃがみ込み、片足を持ち上げて自分の靴の裏を見ている人のイラスト。床にはいくつも足跡が残っていて、手前の数個だけが、その靴とまったく同じ形で色がついている。

誰が来ているのか

サイトを公開して5日目に、GA4 のホーム画面を開きました。アクティブユーザー66人、表示回数168、平均エンゲージメント時間31秒。公開したばかりの個人サイトとしては、悪くない数字に見えます。

そこから2週間ほど同じ画面を見続けて、いま分かっていることを先に書きます。この数字の大半は、記事を読んだ人ではありません。そして、ボットを疑って内訳を開いていったら、混ざっていたもののうち9人は自分の開発環境でした。

この記事は、自分のプロパティに出た数字と、公式ドキュメントに書いてあることを並べたものです。ボットの見分け方を教える記事にはなりません。むしろ、見分けられない範囲がどこまでかを公式に確かめる記事です。数字はすべて自分で見たもので、公式の記述と自分の観測はそのつど分けて書きます。


「読者ではない」と言える5つの数字

公開5日目、2026年8月18日に取得した数字です(期間は7/21〜8/17、実質5日間)。

  • セッション 81
  • アクティブユーザー 66
  • 表示回数 168
  • 平均エンゲージメント時間 31秒

この4行だけでは何も分かりません。分かったのは、内訳を5か所ひらいたときでした。

1. 再訪がゼロ

アクティブユーザー66人のうち、新規が66人。2回目に来た人が1人もいませんでした

公開5日なら当たり前だと思うかもしれません。ただ、この項目は残り3つと同じ方向を向いています。

2. 日本語のサイトに、米国から35人

国別のアクティブユーザーは US 35 / JP 14 / PL 5 / CA 3 / DE 3 / UK 3 / KR 1 でした。日本語だけで書いた、日本国内のVPSの話しか載っていないサイトです。

その4日前、公開2日目に見たときは、もっとはっきり出ていました(2026年8月14日取得・過去28日)。

ユーザーエンゲージのあったセッション平均エンゲージメント時間
Japan12203分20秒
United States1900秒
Poland500秒
他5か国各1〜200〜3秒

ホーム画面には「アクティブユーザー44人」と出ていました。ただ、エンゲージのあったセッションを持っているのは Japan だけです。人が読んだと言えるのは、この20セッションだけでした。後述しますが、その20セッションも、全部が読者だったとは限りません。

3. Direct が93.8%

同じ8月18日のチャネル別です。

チャネルセッションエンゲージメント率平均エンゲージメント時間
Direct76(93.83%)26.32%26秒
Organic Search4(4.94%)50%2分20秒
Referral1100%1秒

Organic Search はたった4セッションですが、滞在は Direct の5倍以上あります。流入の少ないほうだけが、読まれています

4. トップは踏まれ、記事は読まれる

同じ日のページ別の滞在です。

  • /(トップ) 98表示・7秒
  • /posts/vps-first-things-after-signup/ 10表示・2分57秒
  • /posts/railway-compose-not-portable/ 14表示・1分43秒
  • /posts/conoha-vps-2gb-limits/ 20表示・57秒
  • /posts/docker-compose-ports-override-merge/ 6表示・4秒

表示回数168のうち98がトップです。全体の約58%がトップページに集まり、そこでの滞在は7秒。記事まで進んだものは分単位になります。

この非対称が、いちばん素直な境目だと思っています。トップだけ踏んで7秒で消えるものと、記事を2分読むもの。同じ言葉で数えるには無理があります。

5. 90%まで読まれた記事が7人分ある。ただし、これも数えられない

同じ日、GA4 の scroll イベントを見たら7人分ありました。GA4 の拡張計測でいう scroll は、ページの90%まで到達したときに発生するものです。

滞在時間よりは手がかりになります。タブを開きっぱなしにすれば滞在は伸びますが、90%のスクロールは、そうはなりません。66人や44人という数字と比べれば、桁も違います。

それでも、これを「読者は7人だった」とは書けません。 理由が3つあります。

  • 90%のスクロールを、人間の証明にはできません。 この記事の後半で見るとおり、JavaScript を実行するクローラーは実在します
  • この7人には、私自身が混ざっている可能性があります。 自分の開発環境が計上されていたと気づくのは、この11日後です
  • 別の日の記録では、scroll17人分あるのに、日本からのユーザーは14人でした。人数として読むと、すでに合いません

手がかりは増えますが、証拠にはなりません。 ここが、この記事でいちばん言いたいところの入口です。


ホスト名の列を出したら、9人が自分だった

ここまでの5つを見て、私は「海外のクローラーだろう」と考えていました。その考えが半分外れていたと分かったのが、8月29日です。

GA4 の「ページとスクリーン」に、セカンダリ ディメンションでホスト名を足しました。期間は8/1〜8/28です。

ホスト名表示回数ユーザー平均エンゲージメント時間
prodnote.dev約228記事は1〜3分
localhost15(約6%)90秒
prodnote.pages.dev7(約2.8%)70秒

localhost が15表示、ユーザー9人。自分が開発サーバーで開いていた画面が、そのままユーザーとして数えられていました

この15表示・9ユーザーは、私です。

原因はコードにありました。GA4 の測定IDを lib/site.ts の定数として書いていて、タグを出すかどうかの条件を、何も付けていませんでした。だから本番ビルドでも開発サーバーでも、同じIDでタグが動きます。このサイトの計測は、公開初日から自分の作業を含んでいました。

prodnote.pages.dev の7表示も、同じ話の親戚です。Cloudflare Pages はカスタムドメインとは別に pages.dev のURLを持っていて、そちらでも同じHTMLが配られます。

直したのはコード側です。タグの出力に本番環境かどうかの条件を足して、開発サーバーではタグそのものを出さないようにしました。

GA4 には「デベロッパー トラフィックの除外」というデータフィルタもあります。ただし、ヘルプの説明はこれだけです。

デバッグモードを使用しているデベロッパーのアクティビティを除外します。

デバッグモードを使っていない普通の開発サーバーは、これでは消えません。GA4 の画面側にはもうひとつ、IPアドレスで除外する仕組みがあります。そちらも設定しましたが、それだけでは足りませんでした。

内部トラフィックの除外を設定したら、自分のIPではなかった

コード側を直したのと同じ8月29日に、画面側の内部トラフィック除外も設定しました。ここから先は、実際に触りながら書いています。

設定は2か所に分かれています。まず「管理 → データストリーム → タグ設定を行う → もっと見る → 内部トラフィックの定義」でルールを作ります。traffic_type は既定の internal のまま、マッチタイプは「IPアドレスが次と等しい」を選びました。

ここで終わりだと思ったのですが、定義を作っただけでは除外されません。「管理 → データ設定 → データフィルタ」に Internal Traffic という項目があり、それが有効になっているかを確認する必要があります。

確認が要る理由は、フィルタに「テスト」という状態があるからです。ヘルプにはこう書かれています。

テストデータ フィルタに一致するデータが [テストデータのフィルタ名] ディメンションに割り当てられ、フィルタ名の値が設定されます。このデータは Explore で利用できるため、データフィルタを有効にする前に検証できます。

テストのままだと、データはレポートから除外されません。印が付いて Explore で見えるようになるだけです。有効にして初めて、レポートから消えます。

では初期状態がどちらなのかというと、そこが分かりませんでした。公式ヘルプを2ページ当たりましたが、初期状態についての記載を見つけられていません。私の環境では、開いた時点で既に有効でした。だから「確認してください」としか書けません。

つまずいたのは、登録するIPアドレスのほうです。

正しい取り方は、実際に計測されるパソコンから自分で確かめることです。IPv4 と IPv6 の両方を見ます。

curl -s -4 https://ifconfig.me
curl -s -6 https://ifconfig.me

この環境では IPv6 のほうが返らず、外へは出ていませんでした。そのため、登録したのは IPv4 の1行だけです。IPv6 を持つ回線で IPv4 だけ登録すると、除外は効きません。両方を実測してから決めてください。

そして、登録したIPは、いつか変わります。多くの家庭用の回線では、契約者に固定のアドレスが割り当てられているわけではありません。変わった時点で、この除外は黙って効かなくなります。エラーは出ません。設定した記録だけが残ります。

この記事で書いてきたことが、そのまま自分に返ってきます。「設定した」と「効いている」は別です。除外したはずの数字が急に増えたら、まずIPが変わっていないかを疑ってください。

そして、このフィルタは遡りません。有効にする前に記録されたものは消えないので、この記事に出てくる数字も消えていません。

それでも、これだけでは根が止まりませんでした。IPで除外できるのは、同じ回線から見たときだけです。別のパソコンや別の回線で開発サーバーを開けば、また混ざります。止まるのはコード側の分岐のほうでした。

画面の設定とコードの修正は、どちらか一方では足りません。IPでの除外は回線が変われば効かなくなり、コードの分岐は既に入ってしまった数字を消せません。両方いります。

「9人」は人数ではない

この件で、もうひとつ確認できなかったことがあります。localhost の9ユーザーが、実際に何人なのかが分かりません

私は1人で開発しています。WSL側のブラウザ、Windows側のブラウザ、シークレットウィンドウ、キャッシュを消した後。これらは GA4 から見れば別のユーザーになり得ます。画面上で内訳を分解する方法は、見つけられませんでした。

GA4 の「ユーザー数」は、人数の推定値です。9と出ていても、実際は1人かもしれません。この記事を書いていていちばん実感したのは、ここでした。


GA4 はボットを除外している。ただし「既知の」だけ

ここから公開情報です。取得はすべて2026年8月29日。

Google アナリティクス ヘルプの「既知の bot トラフィックの除外」には、こう書かれています。

In Google Analytics properties, traffic from known bots and spiders is automatically excluded.

自動で除外される、とはっきり書いてあります。ただし対象は known、つまり既知のものです。そして同じページの続きのほうが重要でした。

At this time, you cannot disable known bot traffic exclusion or see how much known bot traffic was excluded.

除外をオフにすることも、除外された量を見ることもできないと書かれています。これは「調べたけれど分からなかった」という話とは別のものです。公式が、できないと明記しています。

判定の材料は2つだとされています。

Known bot and spider traffic is identified using a combination of Google research and the International Spiders and Bots List, maintained by the Interactive Advertising Bureau.

Google 自身の調査と、IAB が管理するリストの組み合わせ。前者の中身は、どこにも書かれていません。後者は次の節の話になります。

なお、このヘルプページには掲載日も更新日も表示がありません。ここに書いたのは、2026年8月29日時点で表示されていた内容です。


そのリストは、年15,000ドルでFTPから配られている

IAB のページには、購読の条件が書かれています。

Access to the list is not available until payment and a signed data subscription agreement have been received.

支払いと署名済みの契約が済むまでアクセスできない、と。配布はFTP、購読は12か月単位です。同じページに載っていた2026年8月29日時点の価格は、IAB会員が年5,000ドル、アソシエイト会員が7,500ドル、非会員が15,000ドル。

つまり、GA4 が何を消したのかを確かめようとすると、年15,000ドル払ってFTPからリストを取るところから始まります。利用者の側から検証できる仕組みになっていません

リストの中身の説明は、IAB が2019年6月に出したベストプラクティスのPDFにあります。ただし、このPDFは本文をそのまま取り出せない形式でした。以下は引用ではなく、私が読み取った要旨です。原文との照合は済んでいません。

  • 判定は、ユーザーエージェント文字列の照合で行う
  • 含めるリストと除外するリストの2本を、順に当てる
  • 含めるリストのどれにも一致しないUAは、ボットとして扱う
  • cURLwget のようなHTTPクライアントの既定のUAは、除外側に置かれている。理由は、それらが既定でグラフィックスレンダラに繋がっておらず、人間の閲覧に由来すると仮定できないから
  • リスト自身が、誤判定は起こり得ると認めている

要旨のいちばん大事なところは、4つ目です。判定の根拠が「レンダラに繋がっていないこと」なら、レンダラに繋がっていて、UAが普通のブラウザと同じものは、この仕組みでは原理的に落ちません


「ボットはJavaScriptを実行しない」はもう成立しない

GA4 にボットが出ない理由としてよく説明されるのが、これです。GA4 はブラウザのJavaScriptで測るので、JSを実行しないボットは記録されない、という話。

Cloudflare は自社のドキュメントで、実際にそう書いています。

Google Analytics does not record threats, bots, and automated crawlers because those requests typically do not trigger JavaScript.

これは Cloudflare の記述であって、Google の記述ではありません。そのうえで、この説明は自分の画面の数字と正面から食い違います。GA4 にボットらしき数字が出ているから、この記事を書いています。

食い違いのほうにも、公式の裏付けがあります。Google 検索セントラルの「JavaScript SEO の基本」には、こうあります。

Once Google's resources allow, a headless Chromium renders the page and executes the JavaScript.

Googlebot 自身がヘッドレスの Chromium でページをレンダリングしてJSを実行する、と Google が書いています。同じページには、HTTPステータス200のページを原則すべてレンダリングのキューに入れる、とも書かれています。

そのヘッドレスモードについて、Chrome の公式ドキュメントはこう説明しています。

Essentially, you can run Chrome without chrome.

外枠なしで Chrome を動かせる、という言い方です。他の機能はすべて制限なく使える、とも同じページに書かれています。

ここから先は私の推定です。ヘッドレスの Chrome は通常の Chrome と同じレンダラを持ち、JSを実行します。そうであれば、さきほどのIABの判定基準を通り抜けます。ただし、「だからGA4に計上される」とは、どの公式ドキュメントにも書かれていません。私も、自分のサイトに来た米国の35人がヘッドレスブラウザだったと確かめてはいません。

言えるのは、ここまでです。JSを実行するクローラーが実在することは、Google 自身の文書で確定しています。だから、JSを実行しないから安心という一般論は、もう使えません。


Direct が多い理由は、断定できない

Direct 93.8% を見たとき、私は理由を調べました。結果は、あまり気持ちのいいものになりませんでした。

Google のヘルプにある Direct の定義は、条件式としてはこれだけです。

参照元 - 完全一致 - 「(direct)」 AND メディアが「(not set)」または「(none)」

説明文のほうは、保存済みリンクから、またはURLを入力してアクセスする際のチャネル、となっています。そして、何が Direct に落ちるのかの列挙は、どこにもありません。

よく見かける説明、たとえばアプリ内のリンク、リファラを落とす環境、ボット。これらはこの条件から導いた推定であって、Google がそう書いているわけではありません。

ひとつ、調べて誤りだと分かった説明があります。「最近のブラウザはリファラを送らないから Direct が増える」というものです。Chrome の公式ブログによれば、既定のポリシーは strict-origin-when-cross-origin で、こう説明されています。

With this policy, only the origin is sent in the Referer header of cross-origin requests.

オリジンは送られます。リファラが丸ごと消えるわけではないので、この説明は成立しません。自分の Direct 93.8% の理由には使えませんでした。


Cloudflare に切り替えれば分かる、とも言えない

GA4 で見えないならサーバー側で見ればいい。このサイトは Cloudflare Pages に置いてあるので、私も最初にそう考えました。

ただし、ここは実機で確認していません。以下はドキュメントを読んだだけの範囲です。

まず、Pages の Web Analytics はサーバー集計ではありません。ドキュメントには、次のデプロイでJavaScriptのスニペットを自動で追加する、と書かれています。FAQには、ビーコンがページの読み込み完了時と離脱時に報告する、ともあります。GA4 と同じ、ブラウザ側のJSビーコンです。同じ盲点を持ちます。

ボットが見えるのは、ゾーン(Cloudflare でプロキシしているドメイン)側の集計のほうです。Zone Analytics のドキュメントは、無料プランで見られる数字に、正当なユーザーのリクエストに加えてクローラーと脅威が含まれる、と書いています。Security Analytics のドキュメントにはこうあります。

Security Analytics displays information about all incoming HTTP requests for your domain, including requests not handled by Cloudflare security products.

ゾーン単位の分析は全プランに含まれる、とも書かれています。一方、専用の Bot Analytics は Business と Enterprise 向けで、無料プランにはありません。

ドキュメント上の話は、ここまでです。このサイトの Cloudflare の管理画面を開いて確かめてはいませんpages.dev のほうは Cloudflare のゾーンではないので同じようには見えない可能性がありますが、それも未確認です。


A8 は12回、GA4 は1回

数字が合わなかったものが、もうひとつあります。2026年8月22日に、アフィリエイトのクリック数を両側から見ました。

  • A8 の管理画面:今月のインプレッション 309、クリック 12
  • GA4 のイベント affiliate_click1
  • 発生件数 0件、CVR 0.00%

実装のバグではありません。このサイトのアフィリエイトリンクは全部が1つのコンポーネントを通っていて、そこで必ず送信しています。コードを読み直して確認しました。

食い違いは、2つの計測が要求するものの数で説明がつきます。GA4 側が1件を記録するには、JSが動き、実際にクリックのイベントが起き、gtag の読み込みが成功するという3つが全部そろう必要があります。しかも送信は例外を握り潰すように書いてあって、失敗しても遷移を止めません。計測が落ちても、リンクとしては成立します。

A8 側は、リンク先が a8.net のURLです。そのURLがリクエストされた時点で計上されると考えるのが自然ですが、これは推定です。A8 のヘルプはクリック数の定義を書いているだけで、サーバー側で数えているのかJSで数えているのか、ボットのアクセスをどう扱うのかは書かれていません。

どちらかが間違っている、という話でもありません。ブラウザで測る仕掛けと、URLが叩かれたことで測る仕掛けは、そもそも別のものを数えています。


GA4 が3桁と言っている裏で、検索から来たのは3クリック

いちばん短く言えるのは、この2つの数字を並べたときだと思います。

2026年8月27日に Search Console の検索パフォーマンスを見ました。過去28日(7/31〜8/24、実質12日間)で、合計クリック数 3、合計表示回数 35、平均CTR 8.6%、平均掲載順位 9.9 です。

同じころ、GA4 のアクティブユーザーは3桁に届いていました。検索から人が来た回数は、3です。

GSC が数えるのは検索結果でのクリックだけなので、GA4 のユーザー数とは別物です。それでも、公開直後にどちらを見るべきかは、この並びではっきりします。


調べても分からなかったこと

この記事で埋められなかった穴を、そのまま置いておきます。

  • GA4 の判定材料のうち、Google 自身の調査の中身。 どこにも公表されていません
  • 除外された件数。 公式に、見られないと書かれています
  • IABリストの実際の中身。 年15,000ドル払わないと見られません
  • localhost の9ユーザーが実際に何人か。 GA4 側で分解できませんでした
  • A8 のクリック計測の実装。 公式に書かれていません
  • Slack や Meta のプレビュー取得がJSを実行するか。 両社とも書いていません。リンクを貼ればプレビュー用の取得が走ることまでは公式にありますが、それがGA4に載るかは分かりません
  • Cloudflare の管理画面での実際の見え方。 まだ開いていません
  • Internal Traffic フィルタの初期状態。 内部トラフィックの除外とデータフィルタの2ページを当たりましたが、記載を見つけられませんでした。私の環境では開いた時点で有効でしたが、それが既定なのかは分かりません

最初に見る列

公開直後の GA4 で、いまの私が最初に見るものを挙げます。総ユーザー数は、いちばん後回しでいい数字でした。

  1. ホスト名。 既定のレポートでは主ディメンションに選べません。セカンダリ ディメンションか探索で足す必要があります。見えていなかったのではなく、その列を出していませんでした
  2. scroll の90%到達。 滞在時間よりは手がかりになります。ただし人数としては読めません(上に書いたとおりです)
  3. 再訪。 新規とアクティブが同じ数のあいだは、読者はまだいません
  4. ページ別の滞在。 トップと記事の差が、いちばん素直な境目です
  5. Search Console のクリック数。 GA4 の数字とは切り離して見ます

そして、自分を数えないための作業が2つあります。画面側では内部トラフィックの除外を設定して、登録するIPを自分で実測すること。コード側ではタグを出すのを本番ビルドだけに限ることです。

⚠️ ここで測定IDのほうを環境変数に移すのは勧めません。静的サイトでは、生成のときに値を渡す配線が要ります。その配線が漏れると「本番だけ計測が消える」という、気づきにくい壊れ方をします。IDは定数のままにして、タグを出す条件のほうを足すほうが安全です。私は process.env.NODE_ENV === "production" を条件に加えました。

私は2週間気づかずに、自分をユーザーとして数えていました。

このサイトでは「表示と実態がずれる」話を何度か書いてきました。本番が開発モードで動いていた話と、正常なのにコンテナが赤かった話です。

docker-compose.prod.yml が効かない — 本番が開発モード

docker ps が unhealthy なのにサイトは正常に動いていた

今回はその計測版でした。しかも、ずれを疑っていた自分の環境が、ずれの一部でした。

サイトを作るところから同じ構成でやる場合は、こちらに書いてあります。

VPSをやめてNext.js静的エクスポートをCloudflare Pagesへ