Claude Codeでイベントログを使った匿名アンケートを作ってCloudflareに公開する
アンケートを作りたい。回答を集めて、集計もしたい。
Cloudflare には、起きたことを記録していくだけの置き場がある。一度書いたら書き換えず、あとから SQL で数える。3か月経つと古いものから消えていく。アクセスログに近い性質だが、テキストファイルではない。この記事では、そこにアンケートの回答を貯める。
しかも最後には、回答し終わった人に「あなたは〇〇型です」と診断結果を返すところまでやる。そこまでやると、Google フォームでは作れないものになる。
本記事の位置付け
Section titled “本記事の位置付け”Webアプリは「フロントエンド」「バックエンド」「データベース」の3つに分けて作る(→ データ共有型Webアプリの基礎知識)。D1でデータ共有型Webアプリを作るでは、その3つを Cloudflare Pages・Pages Functions・D1 で組んだ。
今回も骨組みは同じ。変わるのはデータを置く場所だけ。
| 役割 | D1でデータ共有型Webアプリを作る | この記事 |
|---|---|---|
| フロントエンド(画面) | Cloudflare Pages | 同じ |
| バックエンド(API) | Cloudflare Pages Functions | 同じ |
| データを置く場所 | Cloudflare D1 | Workers Analytics Engine |
レストランの例えを続けるなら、こうなる。
- 冷蔵庫(D1):食材を出し入れする。中身を入れ替える。使えば減る
- 伝票の束(今回):書いたら綴じるだけ。書き換えない。あとで枚数を数える。古い綴りから処分していく
掲示板の投稿は、あとから消したり直したりする。だから冷蔵庫が要る。アンケートの回答は起きてしまった出来事で、書き換えることがない。だから伝票で足りる。
この使い分けが分かると、次にアプリを作るとき「これは D1 が要るのか」を自分で判断できるようになる。
1. 作るもの
Section titled “1. 作るもの”参加者のレベルをざっくり診断するアンケートを作る。仕様はこの4つ。
- 選択式の質問に答えると、その場で診断結果が出る
- 同じ人が何度答えても1票として数える
- 締切を過ぎたら受け付けない
- 主催者だけが集計を見られる
完成するとこうなる。
答え終わるとこうなる。
主催者はこれを見る。
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行変えるだけで済むので、状態を持たなくていい。
1-2. お金はかかるのか
Section titled “1-2. お金はかかるのか”かからない。今回使うものは、すべて 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 も、個人が使う範囲なら無料で足りる。
2. 事前準備
Section titled “2. 事前準備”WranglerでCloudflareに公開するまでを済ませていること。wrangler が使えて、Cloudflare にログインできていればいい。
2-1. Analytics Engine を有効にする
Section titled “2-1. Analytics Engine を有効にする”ここを飛ばすと、あとのデプロイが必ず失敗する。 しかも失敗の仕方が分かりにくいので、先にやっておく。
Cloudflare のダッシュボードで Storage & databases から Analytics Engine を開き、右上の Enable を押す。
押すとボタンの位置が Create Dataset に変わる。作業はこれだけ。
隣に Create Dataset があるので押したくなるが、押さなくていい。データを入れる箱(データセット)は、最初に書き込んだ瞬間に自動で作られる。名前はあとで設定ファイルに書くので、ここで決める必要もない。
3. アンケートのページを作る
Section titled “3. アンケートのページを作る”まずは画面から作る。この段階ではまだ Cloudflare は関係ない。 ブラウザで開けば動く HTML ファイルを1枚作るだけ。
Finder で作業用のフォルダを新しく作る。たとえば ホーム → claude → my-survey。
次に Claudeデスクトップアプリを起動し、Code(Claude Code)を選択 → New session → いま作ったフォルダを指定する(~/claude/my-survey)。
そこで次のように頼む。どんな質問にするかまで書いておくのがコツ。ここを「4問くらい適当に」と頼むと、毎回違うものができて後の章と話が合わなくなる。
アンケートのページを HTML 1枚で作って。public/index.html に置いてほしい。
- 質問と選択肢のデータは HTML の中に JSON で埋め込む(外部ファイルにしない)- ラジオボタンで選んで送信ボタンを押す形- ファイルをダブルクリックで開いても表示と選択の確認ができるようにする
タイトルは「あなたのレベル診断」。質問は4つで、それぞれ3択。選択肢は「経験が浅い順」に並べてください。
1. ターミナル(黒い画面)を使ったことは? 使ったことがない / コピペして実行したことがある / 自分でコマンドを打って操作できる2. Git や GitHub は? 名前を聞いたことがある程度 / コミットやプッシュをしたことがある / ブランチを分けて作業できる3. 作ったものを公開したことは? まだない / サービスの画面から公開した / コマンドでデプロイした4. エラーが出たときは? どうしていいか分からなくなる / エラー文を検索したり AI に聞く / ログを読んで原因を絞り込める途中でファイル操作の許可を求められたら、その都度許可する。
質問データを外部の JSON ファイルにせず HTML に埋め込むのには理由がある。外部ファイルを読みに行くと、ファイルをそのまま開いたときにブラウザが読み込みを拒否する(file: で開いたページからの読み込みが制限されているため)。埋め込んでおけば、サーバーを立てずに手元で確認できる。
できあがった JSON には、質問と選択肢それぞれに id が振られている。この id はあとから変えられない。 集計はこの id でまとめるので、回答を集め始めてから変えると数が合わなくなる。公開する前に質問を決めきっておく、と覚えておけばいい。
できあがったらファイルをダブルクリックして、ブラウザで表示を確認する。
open public/index.html見た目が気に入らなければ、この段階で直しておく。まだ公開していないので気楽に試せる。
4. Cloudflare に公開する
Section titled “4. Cloudflare に公開する”4-1. プロジェクト名を決める
Section titled “4-1. プロジェクト名を決める”公開URL(https://名前.pages.dev)になる名前を決める。*.pages.dev は Cloudflare 全体で早い者勝ちなので、すでに使われている名前だと末尾にランダムな文字が付く。決める前にブラウザで https://候補名.pages.dev を開いてみると、空いているかどうかが分かる。
この記事では my-survey として書き進めるが、そのまま使うと他の読者と衝突するので、自分で決めた名前に読み替えてほしい。
4-2. 公開する
Section titled “4-2. 公開する”名前が決まったら、設定ファイルの用意からデプロイまでまとめて頼む。2章で有効にした Analytics Engine とのつなぎ込みも、ここで一緒に済ませる。
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つのコマンド。
npx wrangler pages project create my-survey --production-branch mainnpx wrangler pages deployhttps://my-survey.pages.dev で開ける。この URL を配ることになる。
5. 回答を受け取る
Section titled “5. 回答を受け取る”ここから Cloudflare 側の処理を作る。といっても、やることは「受け取って書き込む」だけ。
回答を受け取るエンドポイントを 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票」という素朴な発想が、公開されたインターネットの上では成り立たない。自分でアプリを公開するときに何度も出てくる考え方なので、ここで覚えておくと後が楽になる。
5-3. 同じ人を見分ける
Section titled “5-3. 同じ人を見分ける”同じ人が2回答えたときに2票と数えないために、回答者を見分ける必要がある。ただし名前もメールアドレスも聞かない。サーバーが発行した意味のない文字列を cookie に入れておくだけ。
回答者を見分ける cookie をサーバー側で発行して。JavaScript から読めないようにして、ページ側には回答者を指す値を一切持たせないでほしい。ブラウザの localStorage に保存する方法もあるが、今回は cookie にした。理由が3つある。
- 勝手に消えない。 Safari は
localStorageを7日ほど使わないと消してしまう(ログインキーでユーザーアカウント管理でも出てくる話)。アンケートの途中で消えると、同じ人が別人として数えられる。サーバーが発行した cookie はこの削除の対象外 - 期限を決められる。 締切と同時に切れるようにできる(8章)
- ページ側から触れない。 サーバーが発行した cookie は、ページの JavaScript から読めないようにできる(
HttpOnlyという設定)。回答者を指す値がページ側に一切出てこないので、うっかり画面に出したり外部に送ったりする心配がない
6. 集計を見られるようにする
Section titled “6. 集計を見られるようにする”集計するには、Cloudflare の API を叩くためのトークンが要る。この記事で唯一、秘密の値を自分の手で扱うところ。
6-1. API トークンを作る
Section titled “6-1. API トークンを作る”右上のアイコンから Profile を開く。
左メニューの API Tokens を開き、右上の Create Token を押す。
テンプレートがいくつも並んでいるが、使わない。上の方にある Create Custom Token の Get started を押す。
テンプレートには Read analytics and logs という、いかにも今回に合いそうなものがある。それでも自分で作るのは、権限を必要な分だけに絞るため。テンプレートは便利な代わりに、要らない権限まで付いてくることがある。
名前を付けて、Permissions に Account / Account Analytics / Read を指定する。これ1つだけ。 他は何も足さない。Account Resources は自分のアカウントを選ぶ。
Continue to summary を押すと、与える権限が1行だけ表示される。
Create Token を押すとトークンが表示される。
この文字列が表示されるのは、この一度きり(画面にもそう書いてある)。閉じると二度と見られないので、次の手順をこの画面のまま進める。
6-2. 秘密を預ける
Section titled “6-2. 秘密を預ける”トークンを手元のファイルに保存する方法もあるが、この記事では保存しない。Cloudflare に預けてしまう。
npx wrangler pages secret put CF_API_TOKEN打つとトークンの入力を求められる。貼り付けても画面には表示されず、ターミナルの履歴にも残らない。 ファイルを作る必要もないので、「隠しファイルが Finder で見えない」「拡張子が勝手に付く」といった、本筋と関係ないところでつまずかずに済む。
残りは Claude Code に任せる。
アカウントIDと、集計ページを守るためのランダムなキーを Pages のシークレットに登録して。登録できたらデプロイしなおして、集計ページの URL を教えてほしい。ここで扱いを分けているのが大事なところ。 同じ「秘密」でも強さが違う。
| 何ができてしまうか | 扱い | |
|---|---|---|
| API トークン | アカウントの分析データを全部読める | 自分で打つ。 コマンドに書くと履歴に残る |
| 集計ページのキー | このアンケートの集計が見えるだけ | 自動でいい |
| アカウントID | ただの識別子。単体では何もできない | 自動でいい |
秘密を全部同じように厳重に扱おうとすると手間が増えて、結局どこかで手を抜くことになる。強いものだけを慎重に扱う方が続く。
7. 結果を見る
Section titled “7. 結果を見る”集計ページは、URL に推測できないキーを付けて守る。
https://my-survey.pages.dev/?r=<キー>キーが違えば 404 を返す。「権限がありません」ではなく「そんなページはない」と答えることで、ページの存在自体を隠している。
Claude Code から見ることもできる。
アンケートの集計結果を見せて7-1. 重複回答をどう消しているか
Section titled “7-1. 重複回答をどう消しているか”集計するときに、回答者ごとにまとめて、いちばん新しい回答だけを取り出してから数えている。SQL にはそのための書き方があるので、ひとつのクエリで済む。
同じ人が答え直しても、票は増えずに移動するだけになる。書き込むときには何の判定もしていないのに、これが成り立つ。
数える前にアンケートIDで絞っているのも大事なところ。この表には過去のアンケートも入っているので、絞らないと混ざる。
8. 締切を設ける
Section titled “8. 締切を設ける”アンケートには回答期間がある。締切を1つ決めるだけで、3つのことが同時に決まる。
アンケートに締切をつけて。2026年8月20日の23時59分まで受け付ける。
- 締切を過ぎたら回答を受け付けない- 回答者を見分ける cookie も、締切と同時に切れるようにする- 締切後に開いた人にはフォームを出さず、終了したことを伝える設定ファイルにこう足される。
"vars": { "SURVEY_CLOSES_AT": "2026-08-20T23:59:59+09:00" }これで決まるのが次の3つ。
- この時刻を過ぎたら回答を受け付けない
- 回答者を見分ける cookie も、この時刻に切れる
- 締切後に開いた人にはフォームを出さない
2つ目は見落としやすい。cookie の期限に締切をそのまま入れているので、次に別のアンケートを取るとき、前回の回答者と結びつかない。「N日空けてから次をやる」というルールを人が覚えておく必要がなくなる。
cookie の期限は、秒数で指定する Max-Age と、日時で指定する Expires の2つがある。締切があるなら Expires の方が素直。両方書くと Max-Age が優先されるので、片方だけにする。
9. 診断機能を足す
Section titled “9. 診断機能を足す”答え終わった人に、回答を元に何かしらの診断・判定結果を返す。
やることは、選択肢に点数を持たせて、合計で区分を決めるだけ。
アンケートに診断をつけて。
- 選択肢ごとに点数を持たせて、合計点で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 も出番がない。
両方やりたいときに、はじめてこの構成の出番になる。
10. ここから先
Section titled “10. ここから先”10-1. 自由記述を足したいとき
Section titled “10-1. 自由記述を足したいとき”選択式ではなく自由に書いてもらうこともできる。POST で送っているので URL の長さは関係なく、1問あたり日本語で5000文字ほどまで入る。ただし考えることが増える。
- 集計の形が変わる。 数えられないので一覧を出すことになる
- 画面に出すときのエスケープが必要になる。 選択肢IDは英数字だけだったので危険がなかったが、自由記述は何でも入る
- 匿名でなくなることがある。 名前や所属を書いてしまう人がいる。仕組みでは防げないので、設問文で断るしかない
自由記述が主役になるなら、素直に Google フォームを使うか、D1 を使う方がいい。今回の構成は「数えるもの」に向いている。
10-2. この構成が向かないもの
Section titled “10-2. この構成が向かないもの”- その場で結果を見せたい。 集計への反映に数十秒かかる
- 厳密な一人一票。 シークレットウィンドウや別のブラウザを使えば別人になる
- 長く残したい。 Analytics Engine のデータは3か月で消える
3つ目が引っかかるなら、D1でデータ共有型Webアプリを作るに進む。データベースが必要になる場面が、自分の手で体験した後だと分かりやすくなっているはず。
10-3. もう一度アンケートを取るとき
Section titled “10-3. もう一度アンケートを取るとき”wrangler.jsonc の SURVEY_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











