0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloudflare Web Analytics の beacon が module と ?token= で送らない理由

0
Posted at

自社サービスの紹介ページ(Astro の静的サイト・Cloudflare Pages で配信)に、閲覧を数えるために Cloudflare Web Analytics を入れたところ、beacon は読み込まれるのに、計測が 1 件も送られない状態を踏みました。

原因は、beacon が自分の <script> 要素を document.currentScript か script[data-cf-beacon] で探すのに、こちらは type="module" の script を属性なしで後から差し込んでいたことです。

なぜ手で貼る型で入れたか

  • Cookie もブラウザの保存領域も使わない。 Cloudflare は、この計測が cookie や localStorage のようなクライアント側の状態を使わず、IP アドレスや User Agent で個人を識別しない、と公表しています(Core Web Vitals)。プライバシーポリシーの「広告やアクセス解析のための Cookie を使っていません」という約束を崩さずに足せるものを選びました(解析を使うこと自体は、同じコミットでポリシーと外部送信の公表へ書き足しています)。
  • 自動の差し込みではなく、手で貼る。 Cloudflare のプロキシを通すサイトには、配信の途中で beacon を HTML に自動で差し込む設定があります(Get started)。公開した日の本番の HTML に、自分のコードに無い beacon が差し込まれていたので、ゾーンの構成ルールで RUM を無効にし、登録は API で auto_install: false にして token だけ受け取りました。

もう 1 つ、*.pages.dev のプレビューと手元の astro preview を数えないために、実行時にホスト名を見てから beacon を足す「後から差し込む」形にしました。

最初の形: module と ?token= で後から差し込む

Cloudflare の FAQ には、data-cf-beacon の属性で token を渡す形と、Google タグマネージャー向けに ?token= の query で渡す形が載っています(どちらも type="module")。タグマネージャーも後から足す仕組みなので、この形なら後から足す script でも token が届くと考えて、後者に寄せました(コミットには残していません)。src はビルド時に組み立てて define:vars で渡していました。

if (location.hostname === host) {
  const s = document.createElement('script');
  s.type = 'module';
  s.src = src;
  document.head.appendChild(s);
}

結果は、beacon.min.js は読み込まれ、エラーも出ないのに、/cdn-cgi/rum への送信が出ない、でした。

なぜ送らなかったか: beacon は自分の <script> を探している

beacon.min.js(2026-10-30 に取得・版 2026.10.1)を整形して読むと、入口の近くにこうあります。

d = "data-cf-beacon",
f = document.currentScript
  || ("function" == typeof document.querySelector
        ? document.querySelector(`script[${d}]`) : void 0)

f が見つかったときだけ、src の ?token= と data-cf-beacon の JSON を読んで window.__cfBeacon に入れ、その直後に token が無ければ何もせず終わります。つまり、先に window.__cfBeacon を置いていない限り、token の入口は自分の <script> 要素で、そこへの道は 2 つです。

  1. document.currentScript。MDN のとおり「現在処理中の、JavaScript モジュールではない <script> 要素」を返すもので、module では null です(Document.currentScript)。
  2. document.querySelector('script[data-cf-beacon]')。属性を付けていなければ見つかりません。

最初の形は module で属性なし。2 つの道を同時に塞いでいたうえ、token が無い beacon は黙って終わるので、コンソールには何も出ません。

直した形

属性で token を渡し、module をやめました。module のままでも、FAQ のとおり属性を付ければ送ります(下の表の 5・6 行目)。本番で動いている WebAnalytics.astro の要点です。

<script is:inline define:vars={{ host, token }}>
  if (location.hostname === host) {
    const s = document.createElement('script');
    s.defer = true;
    s.src = 'https://static.cloudflareinsights.com/beacon.min.js';
    s.setAttribute('data-cf-beacon', JSON.stringify({ token }));
    document.head.appendChild(s);
  }
</script>
  • プレビューのビルド(PUBLIC_PREVIEW=1)では、この script 自体を出力しません。
  • defer は直すときに足しましたが、Cloudflare のドキュメントにある形ではありません(FAQ の形は type="module"+data-cf-beacon)。createElement で足した script には効かず(HTML Standard)、下の表の 3 行目のとおり無くても送ります。

確かめ方: 送信を手元で受け止める

Playwright(1.62・headless Chromium)の route で、beacon.min.js の読み込みと /cdn-cgi/rum への POST を受け止めました(Cloudflare には届けません)。コードは要点だけに縮めています。

def on_rum(route, req):
    posts.append(req.post_data)  # 本文を控える
    route.fulfill(status=204, headers={"Access-Control-Allow-Origin": req.headers["origin"]})

ctx.route(lambda u: "cloudflareinsights.com/cdn-cgi/rum" in u, on_rum)

貼り方を 8 通り用意して、同じ最小のページを開いた結果です(後から=createElement で足す・直書き=HTML に書く)。数はページを開いたままの値で、離れるときの送信(FAQ のとおり)は含みません。

貼り方 読込 送信
後から・module・?token=(最初の形) 1 0
後から・classic・defer・data-cf-beacon(直した形) 1 1
後から・classic・data-cf-beacon(defer なし) 1 1
後から・classic・?token= 1 1
後から・module・data-cf-beacon 1 1
直書き・module・data-cf-beacon 1 1
直書き・module・?token= 1 0
直書き・classic・defer・data-cf-beacon 1 1

送らないのは「module で、かつ属性なし」の 2 つだけで、読み解きと一致します。

受け止めた本文は JSON で、siteToken・location・pageloadId・timingsV2・firstContentfulPaint などが入っていました。項目の意味は RUM beacon の表にあります(timingsV2 は表では timingV2、siteToken・location は表にありません)。法務のページの「送られる情報」と突き合わせられます。

本番のビルドも、本番のドメインへの要求を dist/ に差し替えて開きました。トップ・/blog/・/legal/privacy/ とも同じ数え方で読込 1・送信 1、別のオリジンとして開くと 0・0 です。どの場合も document.cookie は空、localStorage・sessionStorage は 0 件のままで、beacon.min.js の中にもその 3 つの名前はありません。

まとめ

  • beacon は document.currentScript か script[data-cf-beacon] で自分の <script> を見つけ、そこから token を読む。見つからず、window.__cfBeacon も無ければ黙って終わる。
  • type="module" では currentScript が null なので、?token= だけでは成り立たない。後から差し込むなら data-cf-beacon を付ける。
  • 「送っているはず」で終わらせず、送信を手元で受け止めて本文を見る。

この仕組みは Hotori(https://hotoriplace.com )で使っています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?