コンテンツにスキップ

Claude Codeでイベントログを使った匿名アンケートを作ってCloudflareに公開する

アンケートを作りたい。回答を集めて、集計もしたい。

Cloudflare には、起きたことを記録していくだけの置き場がある。一度書いたら書き換えず、あとから SQL で数える。3か月経つと古いものから消えていく。アクセスログに近い性質だが、テキストファイルではない。この記事では、そこにアンケートの回答を貯める。

しかも最後には、回答し終わった人に「あなたは〇〇型です」と診断結果を返すところまでやる。そこまでやると、Google フォームでは作れないものになる。

Webアプリは「フロントエンド」「バックエンド」「データベース」の3つに分けて作る(→ データ共有型Webアプリの基礎知識)。D1でデータ共有型Webアプリを作るでは、その3つを Cloudflare Pages・Pages Functions・D1 で組んだ。

今回も骨組みは同じ。変わるのはデータを置く場所だけ。

役割D1でデータ共有型Webアプリを作るこの記事
フロントエンド(画面)Cloudflare Pages同じ
バックエンド(API)Cloudflare Pages Functions同じ
データを置く場所Cloudflare D1Workers Analytics Engine

レストランの例えを続けるなら、こうなる。

  • 冷蔵庫(D1):食材を出し入れする。中身を入れ替える。使えば減る
  • 伝票の束(今回):書いたら綴じるだけ。書き換えない。あとで枚数を数える。古い綴りから処分していく

掲示板の投稿は、あとから消したり直したりする。だから冷蔵庫が要る。アンケートの回答は起きてしまった出来事で、書き換えることがない。だから伝票で足りる。

この使い分けが分かると、次にアプリを作るとき「これは D1 が要るのか」を自分で判断できるようになる。

参加者のレベルをざっくり診断するアンケートを作る。仕様はこの4つ。

  • 選択式の質問に答えると、その場で診断結果が出る
  • 同じ人が何度答えても1票として数える
  • 締切を過ぎたら受け付けない
  • 主催者だけが集計を見られる

完成するとこうなる。

4つの質問が並んだアンケートページ。上部に締切の日時が表示されている

答え終わるとこうなる。

「あなたは 自走型」と大きく表示された診断結果の画面

主催者はこれを見る。

質問ごとの回答が棒グラフで並んだ集計ページ

1-1. 「ログ」という名前だが、データベース

Section titled “1-1. 「ログ」という名前だが、データベース”

回答を貯める先は Workers Analytics Engine という。名前のとおり分析用のもので、起きたことを1件ずつ記録していく使い方をする。

ただし「ログ」と聞いて思い浮かべるテキストファイルではない。中身はれっきとしたデータベースで、grep ではなく SQL で集計できる。それでいて、D1 のように「テーブルを作る」「列の型を決める」という作業がまったくない。

イメージとしては、かなり変わったエクセルを思い浮かべるといい。

  • 行を足すことしかできない。 1人が1問答えるごとに1行増えていく
  • 一度入れたセルは書き換えられない。 修正も削除もできない
  • 列は最初から43個あって、増やせない。 使わない列は空のまま
  • 見出し行がない。 「この列は質問ID」と書いておく場所がない
  • 3か月経った行から消えていく

ずいぶん不便に見えるが、そのぶん設計する作業がゼロになる。テーブルを作らなくていい。列の型を決めなくていい。ファイルを用意することすら要らず、最初に書き込んだ瞬間にできる

そして、この表に SQL をかけて集計できる。「テーブル設計はしないが SQL で集計できる」という、少し不思議な立ち位置になっている。

43個の列は、3つの種類に分かれている。

種類中身今回入れるもの
blobs文字列(20個)アンケートID・質問ID・選択肢ID・回答者ID
doubles数値(20個)使わない
indexes特別な1個アンケートID+質問ID

アンケートIDを一緒に入れておく。 この表は消せないので、次にアンケートを取るときも同じ表に追記されていく。IDを持たせておけば、集計のときに「今回の分だけ」を取り出せる。入れ忘れると、次のアンケートで前回の回答と混ざる。

indexes だけ毛色が違う。これはデータが大量になったときに、全部ではなく一部だけを読んで全体を推定するための単位。指定は必須なので何かを入れることになる。

もうひとつ大事な考え方がある。重複回答の排除を、書き込むときではなく集計するときにやる。

D1 のようなデータベースなら「この人はもう答えたか」を調べてから書き込む。そのために誰が答えたかを覚えておく必要があり、つまり状態を持つことになる。今回は何も調べずに書き込んで、数えるときに同じ人の回答をまとめる。集計の SQL を1行変えるだけで済むので、状態を持たなくていい。

かからない。今回使うものは、すべて Cloudflare の無料の範囲で動く。

Workers Analytics Engine の無料枠はこうなっている。

無料枠アンケートに直すと
書き込み1日10万件5問のアンケートなら1日2万人ぶんの回答
集計(読み出し)1日1万回結果ページを1日5000回開ける

数えるのは行の数。今回は質問1つにつき1行書く作りにしているので(理由は5章)、5問のアンケートに1人が答えると5行になる。つまり「回答した人数 × 質問数」。1日2万人が答えても届かない計算になる。

さらに公式ドキュメントには、現時点では Analytics Engine の利用料を請求していないと書かれている(Pricing)。将来課金する前提の書き方なので「ずっと無料」という約束ではないが、少なくとも今は無料枠を気にする必要すらない。

ページを置く Cloudflare Pages も、個人が使う範囲なら無料で足りる。

WranglerでCloudflareに公開するまでを済ませていること。wrangler が使えて、Cloudflare にログインできていればいい。

ここを飛ばすと、あとのデプロイが必ず失敗する。 しかも失敗の仕方が分かりにくいので、先にやっておく。

Cloudflare のダッシュボードで Storage & databases から Analytics Engine を開き、右上の Enable を押す。

Analytics Engine の画面。右上に Enable ボタンがある

押すとボタンの位置が Create Dataset に変わる。作業はこれだけ。

隣に Create Dataset があるので押したくなるが、押さなくていい。データを入れる箱(データセット)は、最初に書き込んだ瞬間に自動で作られる。名前はあとで設定ファイルに書くので、ここで決める必要もない。

まずは画面から作る。この段階ではまだ Cloudflare は関係ない。 ブラウザで開けば動く HTML ファイルを1枚作るだけ。

Finder で作業用のフォルダを新しく作る。たとえば ホームclaudemy-survey

次に Claudeデスクトップアプリを起動し、Code(Claude Code)を選択 → New session → いま作ったフォルダを指定する(~/claude/my-survey)。

そこで次のように頼む。どんな質問にするかまで書いておくのがコツ。ここを「4問くらい適当に」と頼むと、毎回違うものができて後の章と話が合わなくなる。

Claude
アンケートのページを HTML 1枚で作って。public/index.html に置いてほしい。
- 質問と選択肢のデータは HTML の中に JSON で埋め込む(外部ファイルにしない)
- ラジオボタンで選んで送信ボタンを押す形
- ファイルをダブルクリックで開いても表示と選択の確認ができるようにする
タイトルは「あなたのレベル診断」。質問は4つで、それぞれ3択。
選択肢は「経験が浅い順」に並べてください。
1. ターミナル(黒い画面)を使ったことは?
使ったことがない / コピペして実行したことがある / 自分でコマンドを打って操作できる
2. Git や GitHub は?
名前を聞いたことがある程度 / コミットやプッシュをしたことがある / ブランチを分けて作業できる
3. 作ったものを公開したことは?
まだない / サービスの画面から公開した / コマンドでデプロイした
4. エラーが出たときは?
どうしていいか分からなくなる / エラー文を検索したり AI に聞く / ログを読んで原因を絞り込める

途中でファイル操作の許可を求められたら、その都度許可する。

質問データを外部の JSON ファイルにせず HTML に埋め込むのには理由がある。外部ファイルを読みに行くと、ファイルをそのまま開いたときにブラウザが読み込みを拒否するfile: で開いたページからの読み込みが制限されているため)。埋め込んでおけば、サーバーを立てずに手元で確認できる。

できあがった JSON には、質問と選択肢それぞれに id が振られている。この id はあとから変えられない。 集計はこの id でまとめるので、回答を集め始めてから変えると数が合わなくなる。公開する前に質問を決めきっておく、と覚えておけばいい。

できあがったらファイルをダブルクリックして、ブラウザで表示を確認する。

Terminal window
open public/index.html

見た目が気に入らなければ、この段階で直しておく。まだ公開していないので気楽に試せる。

公開URL(https://名前.pages.dev)になる名前を決める。*.pages.dev は Cloudflare 全体で早い者勝ちなので、すでに使われている名前だと末尾にランダムな文字が付く。決める前にブラウザで https://候補名.pages.dev を開いてみると、空いているかどうかが分かる。

この記事では my-survey として書き進めるが、そのまま使うと他の読者と衝突するので、自分で決めた名前に読み替えてほしい。

名前が決まったら、設定ファイルの用意からデプロイまでまとめて頼む。2章で有効にした Analytics Engine とのつなぎ込みも、ここで一緒に済ませる。

Claude
public フォルダを Cloudflare Pages に「my-survey」という名前で公開して。
wrangler の設定ファイル(wrangler.jsonc)も作ってほしい。
- 公開ディレクトリは public
- Analytics Engine のデータセット my_survey を SURVEY という名前でバインドする
- 環境変数 SURVEY_ID に、このアンケートを表す名前を入れる
- compatibility_date は今日の前日の日付にする

wrangler.jsonc はこうなる。

{
"name": "my-survey",
"compatibility_date": "(YYYY-MM-DD 形式の昨日の日付)",
"pages_build_output_dir": "./public",
"vars": {
"SURVEY_ID": "level-2026-08"
},
"analytics_engine_datasets": [
{
"binding": "SURVEY",
"dataset": "my_survey"
}
]
}

compatibility_date を前日にするのは、UTC 基準で「今日」が未来日と判定されてエラーになるのを避けるため。

analytics_engine_datasets の2行が、さっき有効にした Analytics Engine とつながる部分。binding はコードから呼ぶときの名前、dataset は書き込み先の名前。データセットは事前に用意しなくていい。 ここに名前を書いておけば、最初の回答が届いた時点で自動的に作られる(デプロイした時点ではまだ無い)。

SURVEY_ID は「いま受け付けているアンケートの名前」。 回答と一緒に記録され、集計もこの名前で絞る。次にアンケートを取るときはここを新しくするだけでよく、データセットはそのまま使い回せる(→ 10-3)。最初の1件目から必要なので、後から足すのではなく今のうちに入れておく。

プロジェクト名が my-survey(ハイフン)なのにデータセット名は my_survey(アンダースコア)になっているのは、データセット名がそのまま SQL のテーブル名になるため。ハイフンは使えない。7-1 で出てくる FROM my_survey がこれにあたる。

裏で動くのはこの2つのコマンド。

Terminal window
npx wrangler pages project create my-survey --production-branch main
npx wrangler pages deploy

https://my-survey.pages.dev で開ける。この URL を配ることになる。

ここから Cloudflare 側の処理を作る。といっても、やることは「受け取って書き込む」だけ。

Claude
回答を受け取るエンドポイントを Pages Functions で作って。
functions/api/vote.js に置いて、質問1つにつき1件、Analytics Engine に書き込んでほしい。
- POST だけ受ける。GET は受け付けない
- 書き込むのは 質問ID・選択肢ID・回答者ID の3つだけ
- IP アドレスは記録しない

できあがるのは、届いた回答を1問ずつ書き込むだけの短いコード。テーブルもスキーマもないので、書き込む前の準備が何も要らない。

5-1. 1人の回答が、質問の数だけ行に分かれる

Section titled “5-1. 1人の回答が、質問の数だけ行に分かれる”

書き込みは質問ごとに1回ずつ行われる。だから5問のアンケートに1人が答えると、5行書かれる。

アンケート 質問 答え 回答者
level-2026-08 q1 hard 1a2b3c…
level-2026-08 q2 db 1a2b3c…
level-2026-08 q3 gui 1a2b3c…

どの行にもアンケートIDが入る。 同じ値の繰り返しで無駄に見えるが、この表は消せないので、次のアンケートもこの下に積まれていく。IDが無いと、そのとき前回の回答と区別できない。

1人の回答を1行にまとめることもできる。「1問目の答えは1列目、2問目は2列目」と決めて詰め込めばいい。行数が5分の1になるので、書き込みの数も減る。

それでも分けたのは、集計が段違いに楽になるから

  • 質問が何問あっても、同じ SQL で数えられる。 まとめて入れると「1問目を数える」「2問目を数える」とクエリを書き分けることになる
  • 列の割り当てを覚えておく必要がない。 「3列目は質問3」という対応表を人間が管理するのは、いつか必ず間違える
  • 質問を後から足せる。 まとめて入れる方式だと列は20個までなので、21問目で詰まる

行数と引き換えに、集計の単純さを買っている。書き込みの上限(1日10万行)にはどのみち届かないので、迷わずこちらでいい。

5-2. なぜ GET ではなく POST なのか

Section titled “5-2. なぜ GET ではなく POST なのか”

最初に思いつくのは「選択肢ごとに URL を作って踏んでもらう」形だと思う。/vote/q1/yes を開いたら1票。リンクを配るだけで済むので簡単に見える。

これをやると票が壊れる。 アンケートの URL を Slack や LINE に貼った瞬間、リンクプレビューを作るボットが URL を取りに来る。ブラウザの先読み機能も同じことをする。誰も答えていないのに票が入る。

GET で受け付けなければ、これは起きない。 ボットも先読みも GET しか投げないため。あわせて、自分のサイト以外から送られたリクエストも弾くようにしてある(Sec-Fetch-Site というヘッダをブラウザは必ず送るが、外部のプログラムはたいてい送らない)。

「アクセスされたら1票」という素朴な発想が、公開されたインターネットの上では成り立たない。自分でアプリを公開するときに何度も出てくる考え方なので、ここで覚えておくと後が楽になる。

同じ人が2回答えたときに2票と数えないために、回答者を見分ける必要がある。ただし名前もメールアドレスも聞かない。サーバーが発行した意味のない文字列を cookie に入れておくだけ。

Claude
回答者を見分ける cookie をサーバー側で発行して。
JavaScript から読めないようにして、ページ側には回答者を指す値を一切持たせないでほしい。

ブラウザの localStorage に保存する方法もあるが、今回は cookie にした。理由が3つある。

  • 勝手に消えない。 Safari は localStorage を7日ほど使わないと消してしまう(ログインキーでユーザーアカウント管理でも出てくる話)。アンケートの途中で消えると、同じ人が別人として数えられる。サーバーが発行した cookie はこの削除の対象外
  • 期限を決められる。 締切と同時に切れるようにできる(8章)
  • ページ側から触れない。 サーバーが発行した cookie は、ページの JavaScript から読めないようにできるHttpOnly という設定)。回答者を指す値がページ側に一切出てこないので、うっかり画面に出したり外部に送ったりする心配がない

集計するには、Cloudflare の API を叩くためのトークンが要る。この記事で唯一、秘密の値を自分の手で扱うところ

右上のアイコンから Profile を開く。

右上のユーザーアイコンから開いたメニュー。Profile が選択されている

左メニューの API Tokens を開き、右上の Create Token を押す。

User API Tokens の一覧画面。右上に Create Token ボタン

テンプレートがいくつも並んでいるが、使わない。上の方にある Create Custom TokenGet started を押す。

Create API Token の画面。上部に Create Custom Token、下にテンプレート一覧

テンプレートには Read analytics and logs という、いかにも今回に合いそうなものがある。それでも自分で作るのは、権限を必要な分だけに絞るため。テンプレートは便利な代わりに、要らない権限まで付いてくることがある。

名前を付けて、PermissionsAccount / Account Analytics / Read を指定する。これ1つだけ。 他は何も足さない。Account Resources は自分のアカウントを選ぶ。

Permissions に Account / Account Analytics / Read を選んだ状態

Continue to summary を押すと、与える権限が1行だけ表示される。

サマリー画面。Account Analytics:Read が1行だけ表示されている

Create Token を押すとトークンが表示される。

トークンが作成された画面。トークン文字列は伏せてある

この文字列が表示されるのは、この一度きり(画面にもそう書いてある)。閉じると二度と見られないので、次の手順をこの画面のまま進める。

トークンを手元のファイルに保存する方法もあるが、この記事では保存しない。Cloudflare に預けてしまう。

Terminal window
npx wrangler pages secret put CF_API_TOKEN

打つとトークンの入力を求められる。貼り付けても画面には表示されず、ターミナルの履歴にも残らない。 ファイルを作る必要もないので、「隠しファイルが Finder で見えない」「拡張子が勝手に付く」といった、本筋と関係ないところでつまずかずに済む。

残りは Claude Code に任せる。

Claude
アカウントIDと、集計ページを守るためのランダムなキーを Pages のシークレットに登録して。
登録できたらデプロイしなおして、集計ページの URL を教えてほしい。

ここで扱いを分けているのが大事なところ。 同じ「秘密」でも強さが違う。

何ができてしまうか扱い
API トークンアカウントの分析データを全部読める自分で打つ。 コマンドに書くと履歴に残る
集計ページのキーこのアンケートの集計が見えるだけ自動でいい
アカウントIDただの識別子。単体では何もできない自動でいい

秘密を全部同じように厳重に扱おうとすると手間が増えて、結局どこかで手を抜くことになる。強いものだけを慎重に扱う方が続く。

集計ページは、URL に推測できないキーを付けて守る。

https://my-survey.pages.dev/?r=<キー>

キーが違えば 404 を返す。「権限がありません」ではなく「そんなページはない」と答えることで、ページの存在自体を隠している

質問ごとの回答が棒グラフで並んだ集計ページ

Claude Code から見ることもできる。

Claude
アンケートの集計結果を見せて

7-1. 重複回答をどう消しているか

Section titled “7-1. 重複回答をどう消しているか”

集計するときに、回答者ごとにまとめて、いちばん新しい回答だけを取り出してから数えている。SQL にはそのための書き方があるので、ひとつのクエリで済む。

同じ人が答え直しても、票は増えずに移動するだけになる。書き込むときには何の判定もしていないのに、これが成り立つ。

数える前にアンケートIDで絞っているのも大事なところ。この表には過去のアンケートも入っているので、絞らないと混ざる。

アンケートには回答期間がある。締切を1つ決めるだけで、3つのことが同時に決まる。

Claude
アンケートに締切をつけて。2026年8月20日の23時59分まで受け付ける。
- 締切を過ぎたら回答を受け付けない
- 回答者を見分ける cookie も、締切と同時に切れるようにする
- 締切後に開いた人にはフォームを出さず、終了したことを伝える

設定ファイルにこう足される。

"vars": {
"SURVEY_CLOSES_AT": "2026-08-20T23:59:59+09:00"
}

これで決まるのが次の3つ。

  1. この時刻を過ぎたら回答を受け付けない
  2. 回答者を見分ける cookie も、この時刻に切れる
  3. 締切後に開いた人にはフォームを出さない

「受付を終了しました」と表示された画面

2つ目は見落としやすい。cookie の期限に締切をそのまま入れているので、次に別のアンケートを取るとき、前回の回答者と結びつかない。「N日空けてから次をやる」というルールを人が覚えておく必要がなくなる。

cookie の期限は、秒数で指定する Max-Age と、日時で指定する Expires の2つがある。締切があるなら Expires の方が素直。両方書くと Max-Age が優先されるので、片方だけにする。

答え終わった人に、回答を元に何かしらの診断・判定結果を返す。

「あなたは 自走型」と大きく表示された診断結果の画面

やることは、選択肢に点数を持たせて、合計で区分を決めるだけ。

Claude
アンケートに診断をつけて。
- 選択肢ごとに点数を持たせて、合計点で3つの区分に分ける
- 0〜2点は「これから型」、3〜5点は「慣れてきた型」、6〜8点は「自走型」
- 答え終わった画面に、区分の名前と説明を出す
- 診断結果も回答のひとつとして一緒に送って、集計に出るようにする

質問データに点数が足され、区分の定義が加わる。今回は4問それぞれ最大2点なので満点は8点。0点以上・3点以上・6点以上の3段階を決めておき、届いた中でいちばん高い区分が選ばれる。

区分の名前は否定的にしない方がいい。 正直に「初心者」と出すこともできるが、答えた人がそれを見てどう感じるかを考えると、「これから型」の方がいい。診断は数を集めるためのものでもあるので、答えて損した気分にさせない。

プロンプトの最後に書いた1行がこれ。診断結果を、回答のひとつとして一緒に送っている。 質問が1つ増えたのと同じ扱いになるので、集計側は何も変えていないのに、集計ページに「診断結果の分布」が並ぶ。

集計ページの下部。診断結果の分布が棒グラフで表示されている

9-1. この構成が要るのはどんなときか

Section titled “9-1. この構成が要るのはどんなときか”

この章で足したものは、全部 public/index.html の中にある。 5章で作った functions/ は1行も変えていない。診断はイベントログを使っていないので、ファイルをダブルクリックして開くだけでも動く(送信されないだけ)。

つまり診断だけなら、この記事で作ってきた仕組みは要らない

逆に「アンケートなら Google フォームでいいのでは」とも思うかもしれない。集めるだけなら、そのとおり。 ただ診断を入れると事情が変わる。

Google フォームこの記事の構成
回答後のメッセージ1つだけ。 回答に応じて出し分けることはできない点数に応じて出し分けられる
回答による分岐あるが1問の回答による分岐。3問3択なら27通りのセクションを手で作ることになる質問が増えても点数を足すだけ
テスト機能のフィードバック設問ごとの解説であって、合計点に応じた出し分けではない合計点で区分を決める

分岐機能はあるが、分岐は「木」であって「集計」ではない。「点数を足して閾値で判定する」という診断のやり方は、分岐では表現できない。組み合わせを全部展開することになって、質問が増えると破綻する。

Apps Script を使えば判定はできる。ただし動くのは送信後のサーバー側なので、メールを送るか別のページに飛ばすかしかできない。「送信した瞬間に画面に出る」体験は作れない。

整理するとこうなる。

やりたいこと必要なもの
回答を集めるだけGoogle フォームで足りる
診断を返すだけHTML を1枚公開するだけHTMLをアップロードして公開するの範囲)
集めて、かつ返すこの記事の構成

上の2つのどちらかなら、今回の構成でなくていい。 イベントログも Pages Functions も出番がない。

両方やりたいときに、はじめてこの構成の出番になる。

選択式ではなく自由に書いてもらうこともできる。POST で送っているので URL の長さは関係なく、1問あたり日本語で5000文字ほどまで入る。ただし考えることが増える。

  • 集計の形が変わる。 数えられないので一覧を出すことになる
  • 画面に出すときのエスケープが必要になる。 選択肢IDは英数字だけだったので危険がなかったが、自由記述は何でも入る
  • 匿名でなくなることがある。 名前や所属を書いてしまう人がいる。仕組みでは防げないので、設問文で断るしかない

自由記述が主役になるなら、素直に Google フォームを使うか、D1 を使う方がいい。今回の構成は「数えるもの」に向いている。

  • その場で結果を見せたい。 集計への反映に数十秒かかる
  • 厳密な一人一票。 シークレットウィンドウや別のブラウザを使えば別人になる
  • 長く残したい。 Analytics Engine のデータは3か月で消える

3つ目が引っかかるなら、D1でデータ共有型Webアプリを作るに進む。データベースが必要になる場面が、自分の手で体験した後だと分かりやすくなっているはず。

10-3. もう一度アンケートを取るとき

Section titled “10-3. もう一度アンケートを取るとき”

wrangler.jsoncSURVEY_ID と締切を新しくするだけ。 質問を書き換えてデプロイすれば、次のアンケートが始まる。

"vars": {
"SURVEY_ID": "menu-2026-11",
"SURVEY_CLOSES_AT": "2026-11-30T23:59:59+09:00"
}

dataset は変えなくていい。Analytics Engine のデータは消せないが、回答にアンケートIDが入っているので、集計のときに今回の分だけを取り出せる。前のデータが3か月で消えるのを待つ必要はない。

締切をまたげば cookie も切れているので、前回の回答者とは結びつかない。

過去のアンケートの集計も残っている。URL にIDを足せば見られる。

https://my-survey.pages.dev/?r=<キー>&s=level-2026-08