Cloudflare Pagesの無料Web Analyticsを見ると、ダッシュボードの期間選択は最長30日で、それより前のアクセスは画面からは見られなくなる。
消える前に取得しておこうと思い保存スクリプトを作成した。
本記事はコピペで動く自動保存スクリプトと、途中でハマった「siteTagはHTMLのtokenではない」という罠を実測ログ付きで解説する。
TL;DR
データ自体はGraphQL API経由なら184日(約6ヶ月)残っていた。ただし半年で消えるのは本当なので、長期で残したいなら週次エクスポートを仕込む。
症状
Web Analyticsのダッシュボード(Analytics > Web analytics)では、期間の選択肢が最長30日まで。公式コミュニティでも「表示は直近30日まで」という回答が付いている。画面上のエクスポート機能も実質印刷用で、CSVで取り出す口はない。
このブログでアクセス分析の8週間レビューをやろうとすると、レビュー時点で最初の1ヶ月分が画面から消えている計算になる。そこで「本当に30日で消えるのか」をAPIで確かめた。
保存期間はAPIに直接聞ける
GraphQL Analytics APIにはsettingsという照会用のノードがあり、自分のアカウントで各データセットを何日遡れるかを返してくれる。公式ドキュメントも「正確な境界はsettingsを見ろ」という立場だ。
Web Analytics(RUM)のページビューはrumPageloadEventsAdaptiveGroupsというデータセットに入っている。次のクエリで設定値を取れる(トークンの作り方は後述)。
curl -sS https://api.cloudflare.com/client/v4/graphql \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"query { viewer { accounts(filter: {accountTag: \"アカウントID\"}) { settings { rumPageloadEventsAdaptiveGroups { enabled maxDuration notOlderThan } } } } }"}'
私のアカウントの実測値は以下
{
"rumPageloadEventsAdaptiveGroups": {
"enabled": true,
"maxDuration": 8035200,
"notOlderThan": 15897600
}
}
notOlderThan: 15897600秒 = 184日(約6ヶ月)まで遡って読めるmaxDuration: 8035200秒 = 1回のクエリで指定できる期間の幅は93日まで
つまり「30日」はダッシュボード表示の制限であって、データの寿命ではない。実際に93日を超える幅で聞くと、こういうエラーが返ってくる。
account "a451…" cannot request a time range wider than 13w2d,
but your query time range spans 15w4d14h49m48s
13w2d=93日で、settingsの値と一致する。逆に184日以内・幅93日以内なら、30日より古いデータも普通に返る。
ハマった罠: siteTagはHTMLに埋まっているtokenではない
RUM系のクエリはサイトをsiteTagで指定する。このsiteTag、Web上ではスニペットのtokenと同じものとして説明されていることがある。Google検索のAI要約にも、siteTagをスニペットのtoken欄に埋めて設置する手順が出てきた。

この説明を見て同じ理解のまま、公開ページのHTMLに自動挿入される計測スクリプトのtokenをsiteTagとして使ってしまった。
<script defer src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token":"2210…"}'></script>
間違えた理由は単純で、手元にあるサイト固有のIDがこれ1つしかなく、名前も「token」と「tag」で紛らわしいからだ。
実際にはWeb Analyticsのサイトには、計測スクリプト用のsite_token(HTMLに埋まる公開値)とAPI照会用のsite_tagという2つの別の識別子がある。公式APIリファレンスのsite_infoスキーマに両フィールドが並んで定義されているのだが、2つあると知らなければ区別のしようがない。
このtokenをsiteTagに入れてクエリすると、エラーは出ないのに結果が0件になる。認証は通る、クエリ構文も正しい、でも空。これが一番気づきにくい。
{"data": {"viewer": {"accounts": [{"rumPageloadEventsAdaptiveGroups": []}]}},
"errors": null}
正しいsiteTagは、siteTagの指定を外して逆にAPIに聞くのが確実だ。
# dimensionsにsiteTagを入れて「データを持っているsiteTag」を列挙させる
curl -sS https://api.cloudflare.com/client/v4/graphql \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"query { viewer { accounts(filter: {accountTag: \"アカウントID\"}) { rumPageloadEventsAdaptiveGroups(filter: {date_geq: \"2026-07-13\", date_leq: \"2026-07-19\"}, limit: 10) { count dimensions { siteTag } } } } }"}'
{"count": 77, "dimensions": {"siteTag": "a9a9…"}}
返ってきたa9a9…が本物のsiteTagで、HTMLの2210…とは別物だった。ダッシュボードでWeb Analyticsの対象サイトを開いたときのURLに入っている値とも一致する。
解決手順: 週次エクスポートを仕込む
1. APIトークンを作る(無料)
Cloudflareダッシュボードの My Profile > API Tokens > Create Token > Custom Token で、権限にAccount Analytics : Readだけを付ける。
あわせて次の2つを控える。
- アカウントID: ログイン後のダッシュボードURL
dash.cloudflare.com/直後の32桁 - siteTag: 前節の方法で特定した値
2. スクリプトを置く
WSL/Linuxで動くようにcurlとpython3だけで書いた(jq不要)。直近7日分をパス別のPageviews/VisitsとしてCSVに追記する。
#!/usr/bin/env bash
# Cloudflare Web Analytics(無料版)の週次エクスポート
# 必要: CF_API_TOKEN / CF_ACCOUNT_TAG / CF_SITE_TAG(環境変数)
set -euo pipefail
: "${CF_API_TOKEN:?}" "${CF_ACCOUNT_TAG:?}" "${CF_SITE_TAG:?}"
OUT="${1:-cf-web-analytics.csv}"
SINCE=$(date -d '7 days ago' +%F) # macOSは date -v-7d +%F
UNTIL=$(date +%F)
QUERY=$(printf '{"query":"query { viewer { accounts(filter: {accountTag: \\"%s\\"}) { rumPageloadEventsAdaptiveGroups(filter: {siteTag: \\"%s\\", date_geq: \\"%s\\", date_leq: \\"%s\\"}, limit: 1000, orderBy: [count_DESC]) { count sum { visits } dimensions { requestPath } } } } }"}' "$CF_ACCOUNT_TAG" "$CF_SITE_TAG" "$SINCE" "$UNTIL")
RESP=$(curl -sS https://api.cloudflare.com/client/v4/graphql \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$QUERY")
python3 -c '
import csv, json, sys
since, until = sys.argv[1], sys.argv[2]
data = json.load(sys.stdin)
if data.get("errors"):
sys.exit("GraphQL errors: %s" % data["errors"])
rows = data["data"]["viewer"]["accounts"][0]["rumPageloadEventsAdaptiveGroups"]
w = csv.writer(sys.stdout)
for g in rows:
w.writerow([since, until, g["dimensions"]["requestPath"], g["count"], g["sum"]["visits"]])
' "$SINCE" "$UNTIL" <<<"$RESP" >> "$OUT"
echo "saved: $OUT ($SINCE..$UNTIL)"
実行結果(本ブログの実データ・2026-07-19時点):
2026-07-12,2026-07-19,/,86,74
2026-07-12,2026-07-19,/about/,12,1
2026-07-12,2026-07-19,/posts/bun-docx-require-error-node/,7,2
2026-07-12,2026-07-19,/posts/claude-design-system-prompt/,5,1
2026-07-12,2026-07-19,/posts/common-prompt-principles/,5,0
2026-07-12,2026-07-19,/posts/docx-cli-word-agent/,2,1
2026-07-12,2026-07-19,/cmd_sco,1,1
2026-07-12,2026-07-19,/posts/draft-post/,1,1
countがPageviews、sum.visitsがVisitsに相当する。当ブログの等身大の数字、、、
それでもbun-docxのトラブル解決記事が読まれ始めているといった変化は、この粒度でも十分に追える。
末尾の/cmd_scoのような謎パスはスキャナの痕跡で、実サイトのログにはこういうノイズも混ざる。
3. 週1で回す
cronなら月曜朝に1回。
0 5 * * 1 . $HOME/.cf-wa.env && $HOME/bin/cf-wa-export.sh $HOME/data/cf-wa.csv
トークンはcrontabに直書きせず、~/.cf-wa.env(chmod 600)にexport CF_API_TOKEN=…をまとめて読み込む形にする。週1でも直近7日を取るので抜けは出ない。多少重複しても期間列で後から整理できる。
効かなかった・使えなかった方法
- HTMLのbeacon tokenをsiteTagに使う: エラーが出ず0件になるだけ。切り分けに一番時間を使った
- ダッシュボードからのエクスポート: 印刷(Print Report)しかなく、機械可読な形式では取り出せなかった
- Logpush: 生ログの外部ストレージ常時出力はEnterprise向けの機能で、無料プランでは使えない
向いていない人
- Business/Enterpriseプランの人(Logpushで生ログをS3等へ流すほうが確実)
- すでにGA4やPlausible等で計測している人(そちらの保持期間・エクスポートに従えばよい)
- 過去半年より前のデータをいま取り戻したい人(notOlderThanを超えた分は取得手段がない)
まとめ
- 30日で全て消えるわけではなかった。画面は30日、APIは184日
- RUM系クエリが0件のときはsiteTag≠beacon tokenを疑う