自社サービスの紹介ページ(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 つです。
-
document.currentScript。MDN のとおり「現在処理中の、JavaScript モジュールではない<script>要素」を返すもので、module ではnullです(Document.currentScript)。 -
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 )で使っています。