ブライダルフェアの予約フォームで、お客様がどの項目で止まっているかをGA4で測る。項目ごとのイベントの設計、入力した値を送らない作り、完了を数える場所と、スマホの画面で送られた中身を確かめた結果
フェア予約フォームの
この
この
この
標準の計測で分かるのは「触り始めた」と「送った」まで
GA4のform_start と form_submit のform_start はform_submit は
この
| 知りたい |
標準の計測 | この |
|---|---|---|
| フォームを |
form_start |
( |
| どの |
分からない | fair_form_field(項目の |
| どの |
分からない | fair_form_error(項目の |
| 予約が |
form_submit は |
fair_reserve_complete(受け付けの |
form_submit は
送るのは項目の名前・順番・エラーの種類だけにする
項目ごとの
Googleは、
送る
| イベント名 | 送るとき | 載せる項目 |
|---|---|---|
fair_form_field |
項目の |
field_name(例:email)、field_index(上からfair_type(フェアの |
fair_form_error |
「予約する」を |
field_name、error_type(required=未入力/format=形式のother=field_name を submit、error_type を server_500 などにfair_type |
fair_reserve_complete |
予約が |
fair_type |
イベント名と
項目ごとのイベントを送るコード
フォームのname がFIELDS)を
// フェア予約フォームの「どの項目まで進んだか」「どこでエラーが出たか」を GA4 に送る。
// 送るのは項目の名前・順番・エラーの種類だけ。入力された値(名前・メール・電話・質問)は送らない。
(() => {
const form = document.getElementById('fair-form');
if (!form) return;
const FIELDS = ['fair_date', 'guests', 'name', 'email', 'tel', 'note']; // 画面の上からの順番
const fairType = form.dataset.fairType || 'unknown'; // フェアの種類(分類だけ)
const done = new Set(); // 1回の表示で、同じ項目は1回だけ送る
const send = (name, params) => window.gtag && gtag('event', name, { fair_type: fairType, ...params });
// 値が確定し(change)、その値が正しいときだけ「この項目まで進んだ」を送る
form.addEventListener('change', (e) => {
const el = e.target;
if (!FIELDS.includes(el.name) || done.has(el.name)) return;
if (!el.value || !el.checkValidity()) return;
done.add(el.name);
send('fair_form_field', { field_name: el.name, field_index: FIELDS.indexOf(el.name) + 1 });
});
const errorType = (v) => (v.valueMissing ? 'required' : v.typeMismatch || v.patternMismatch ? 'format' : 'other');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const invalid = FIELDS.map((n) => form.elements[n]).filter((el) => !el.checkValidity());
if (invalid.length) {
for (const el of invalid) send('fair_form_error', { field_name: el.name, error_type: errorType(el.validity) });
document.getElementById('err').textContent = '入力が足りない欄か、形式の違う欄があります。';
invalid[0].focus();
return;
}
const res = await fetch(form.action, { method: 'POST', body: new FormData(form) });
if (!res.ok) {
send('fair_form_error', { field_name: 'submit', error_type: `server_${res.status}` });
document.getElementById('err').textContent = '送信できませんでした。時間をおいてもう一度お試しください。';
return;
}
// サーバーが受け付けたと返したときだけ、完了ページで数えるための印を残して移る。
// (ここで送ってすぐ画面を移すと、まとめ送りの待ちの間に捨てられることがあったため)
try { sessionStorage.setItem('fair_reserved', fairType); } catch {}
location.assign('/thanks.html');
});
})();最初は、focusout)にchange にchange の
完了は、完了ページを開いたときに1回だけ数える
完了のsessionStorage に
// 完了ページ:予約フォームから受け付けられて移ってきたときだけ、1回だけ完了を送る
(() => {
let fairType = null;
try { fairType = sessionStorage.getItem('fair_reserved'); sessionStorage.removeItem('fair_reserved'); } catch {}
if (fairType && window.gtag) gtag('event', 'fair_reserve_complete', { fair_type: fairType });
})();こうしたのは、fair_reserve_complete をevent_callback(送ったevent_timeout で
テストを
スマホの画面で操作して、GA4へ送られる中身を確かめる
イベントが/g/collect)への
// /g/collect への送信を横取りし、イベントごとに分解する(1回の送信に複数のイベントが入ることがある)
async function capture(ctx) {
const hits = [];
await ctx.route(/google-analytics\.com\/g\/collect/, async (route) => {
const req = route.request();
const url = new URL(req.url());
const lines = (req.postData() ?? '').split('\n').filter(Boolean);
for (const line of lines.length ? lines : ['']) {
const p = new URLSearchParams(`${url.search.slice(1)}&${line}`);
hits.push({ raw: `${req.url()}\n${line}`, en: p.get('en'), params: Object.fromEntries([...p].filter(([k]) => /^epn?\./.test(k))), dl: p.get('dl') });
}
await route.fulfill({ status: 204 }); // 本物の GA4 には送らない
});
return hits;
}
// テストで入力した名前・メールアドレス・電話番号が、送信のどこかに含まれていないかを数える
const pii = (hits) => hits.filter((h) => Object.values(TEST).some((v) => decodeURIComponent(h.raw).includes(v) || h.raw.includes(encodeURIComponent(v))));試した
$ node run.mjs
=== iPhone 13(WebKit) ===
A 途中で離れた人:
fair_form_field {"ep.fair_type":"tasting","ep.field_name":"fair_date","epn.field_index":"1"}
fair_form_field {"ep.fair_type":"tasting","ep.field_name":"guests","epn.field_index":"2"}
fair_form_field {"ep.fair_type":"tasting","ep.field_name":"name","epn.field_index":"3"}
fair_form_error {"ep.fair_type":"tasting","ep.field_name":"email","ep.error_type":"format"}
fair_form_error {"ep.fair_type":"tasting","ep.field_name":"tel","ep.error_type":"required"}
入力値を含む送信: 0 件
B 送信まで終えた人(完了ページを1回再読み込み):
fair_form_field {"ep.fair_type":"tasting","ep.field_name":"fair_date","epn.field_index":"1"}
fair_form_field {"ep.fair_type":"tasting","ep.field_name":"guests","epn.field_index":"2"}
fair_form_field {"ep.fair_type":"tasting","ep.field_name":"name","epn.field_index":"3"}
fair_form_field {"ep.fair_type":"tasting","ep.field_name":"email","epn.field_index":"4"}
fair_form_field {"ep.fair_type":"tasting","ep.field_name":"tel","epn.field_index":"5"}
fair_reserve_complete {"ep.fair_type":"tasting"}
完了ページの page_view: 2 件/完了: 1 件/入力値を含む送信: 0 件
C GET の版:入力値を含む送信 1 件
page_view dl=http://127.0.0.1/thanks.html?fair_date=2026-11-08+10%3A00&guests=2&name=%E5%B1%B1%E7%94%B0%E3%83%86%E3%82%B9%E3%83%88&email=test.yamada%40example.com&tel=09000000000¬e=
=== Pixel 7(Chromium) ===
A 途中で離れた人:
fair_form_field {"ep.fair_type":"tasting","ep.field_name":"fair_date","epn.field_index":"1"}
fair_form_field {"ep.fair_type":"tasting","ep.field_name":"guests","epn.field_index":"2"}
fair_form_field {"ep.fair_type":"tasting","ep.field_name":"name","epn.field_index":"3"}
fair_form_error {"ep.fair_type":"tasting","ep.field_name":"email","ep.error_type":"format"}
fair_form_error {"ep.fair_type":"tasting","ep.field_name":"tel","ep.error_type":"required"}
入力値を含む送信: 0 件
B 送信まで終えた人(完了ページを1回再読み込み):
fair_reserve_complete {"ep.fair_type":"tasting"}
完了ページの page_view: 2 件/完了: 1 件/入力値を含む送信: 0 件
C GET の版:入力値を含む送信 1 件
page_view dl=http://127.0.0.1/thanks.html?fair_date=2026-11-08+10%3A00&guests=2&name=%E5%B1%B1%E7%94%B0%E3%83%86%E3%82%B9%E3%83%88&email=test.yamada%40example.com&tel=09000000000¬e=読み取れる
- Aでは、
どこまで 3番目の進んで、 どこで 止まったかが 分かる。 名前までは 正しく 入り、 メールアドレスは 形式の 違い、 電話番号は 未入力で 止まった、と いう 記録に なりました。 テストで 入力した 名前は 送信の どこにも 含まれず、 イベントの パラメータに 載っているのは 項目の 名前と エラーの 種類だけでした (打ちかけの メールアドレスの 文字列 その ものを 送信の 中から 探す処理は、 この テストには 入れていません)。 - Bでは、
完了は 完了ページをどちらの ブラウザでも 1件だった。 開き直しても、 2回目は 数えませんでした (完了ページの 表示は 2件)。 ただし、 Chromiumでは、 完了の 前に 入れた 項目の イベントが 送られないまま 完了ページへ 移りました。 この 差は、 次の 章の 読み方に 関わります。 - Cの
GETの フォームを版では、 入力した 値が そのまま GA4へ 送られた。 method="get"で送ると、 入力した 値が 完了ページの URLの クエリ ( ?の後ろ)に 付きます。 GA4は 表示した ページの URLを page_viewと一緒に 送るので、 名前・ メールアドレス・電話番号が そのまま 載りました。 アナリティクスの ヘルプでも、 URLや ページタイトルに 個人情報が 含まれないよう 確かめる ことが 案内されています。 予約フォームは、 POSTで 送る 作りにします。
数字の読み方:止まった項目は「進んだ項目」と「エラー」を並べて見る
GA4でfield_name と error_type を、
見るfield_name ごとの fair_form_field をfield_name・error_type ごとの fair_form_error の
| 見えた形 | 疑うところ | 直し方の例 |
|---|---|---|
メールアドレスのformat の |
全角の |
type="email" と autocomplete="email"、 |
電話番号のformat の |
ハイフンの |
ハイフンありもなしも |
| 日程は |
人数の |
選択肢と、 |
| 名前の |
個人情報を |
何に |
この
フォームを
自社でやる場合と、任せる場合
自社で
- 送る
イベントと、 載せて よい 項目・載せない 項目の 表を 作る (この 記事の 表が そのまま 使えます)。 - フォームが
POSTで 送られている こと、 完了ページの URLと タイトルに 入力値が 入らない ことを 確かめる。 - 項目ごとの
イベントと 完了ページでの 完了を 実装し、 field_nameとerror_typeをカスタムディメンションに 登録する。 - 公開前に、
スマホの 実機で Aと Bの 操作を 通し、 GA4の リアルタイムや DebugViewで イベントが 届く ことを 確かめる。 - 2〜4週間ためてから、
項目ごとの 人数と エラーを 並べ、 直す項目を 1つ 決める。
任せる
動作確認した環境
確認日は
- macOS 26.6.2、Node.js 22.23.2、Playwright 1.63.0
- ブラウザ:WebKit 26.6
(端末の 設定は Playwright の iPhone 13)、 Chromium 153.0.8010.12 (同 Pixel 7) - テスト用の
サーバーは 手元の 127.0.0.1で動かし、 予約の 受け付け ( /api/reserve)は保存せずに 200を 返すだけに しました - gtag.js は
Google の 配信元から 読み込み、 GA4への 送信 ( /g/collect)はすべて テストの 中で 受け取り、 本物の GA4には 届けていません。 測定IDは 実在しない テスト用の 値です
確かめていない
- 実機の
iPhoneと Androidでの 送信。 テストは 画面サイズと タッチ操作を 再現した もので、 実機の ブラウザではありません。 Chromiumで 項目の イベントが 送られずに 移った 件が、 実機の Chromeでも 起きるかは 確かめていません - GA4の
管理画面 (DebugView・探索・カスタムディメンションの 登録)での 見え方。 測定IDが テスト用の ため、 GA4側には 何も 届いていません - gtagが
イベントを まとめて 送る 間隔 (この テストで 約5秒だった こと)が、 いつでも 同じか どうか - 拡張計測機能の
form_start・form_submitが、この フォームで どう 記録されるか - 完了ページを
開いて すぐ 閉じた 場合に、 完了の イベントが 送られるか
出典(確認日:2026年10月6日)
GA4の
form_startは「ユーザーが セッションで 初めて フォームを 操作した とき」、 form_submitは「ユーザーが フォームを 送信した とき」に 記録される こと、 その パラメータは カスタムディメンションを 作成した 場合のみ レポートで 使える こと → 拡張計測機能イベント - イベント名・パラメータ名は
40文字まで、 1イベントの パラメータは 25個まで、 パラメータの 値は 100文字まで (標準の プロパティ) → イベントの 収集に 関する 上限 - イベントスコープの
カスタムディメンションは 標準の プロパティで 50個まで、 作成から 24〜48時間後に レポートで 使えるようになる こと → カスタム ディメンションと 指標 - 個人情報を
Googleに 送る ことは ポリシーで 禁止されている こと、 URL・ページタイトル・フォームに 入力された 情報から 個人情報を 取り除く こと → 個人を 特定できる 情報 (PII)を 送信しないように する ための ヒント - ファネル データ探索で、
指定の 順序どおりに ステップを 踏んだ 場合のみ 数えられ、 途中の ステップを 抜かした 場合は 後続の ステップを 完了しても 数えられない こと → ファネルデータ探索
よくある質問
GA4の拡張計測機能の form_start と form_submit だけでは、足りませんか?
フォームを
入力エラーのイベントに、入力された値を一緒に送って原因を調べてもよいですか?
送らないでください。
完了のイベントは、送信ボタンを押したときに送ればよいですか?
サーバーが
項目ごとのイベントを送り始めたら、すぐGA4のレポートで項目別に見られますか?
項目の