ブライダル

ブライダルフェアの予約フォームで、お客様がどの項目で止まっているかをGA4で測る。項目ごとのイベントの設計、入力した値を送らない作り、完了を数える場所と、スマホの画面で送られた中身を確かめた結果

フェア予約フォームのどの項目でお客様が止まるかを、GA4のイベントで測る実装です。入力した値を送らない作り、完了を数える場所、GETで送るフォームの落とし穴を、スマホの画面で動かして送られた中身で確かめました。

この記事の結論:フェア予約フォームのどこで人が止まっているかは、GA4の標準の計測では分かりません。項目ごとに「この項目まで正しく入れた」「この項目でエラーが出た」というイベントを自分で送り、項目の名前とエラーの種類だけを載せて、入力された値は送らないようにします。完了は、サーバーが予約を受け付けたあとに、完了ページで1回だけ数えます。スマホの画面に合わせた自動テストで送られた中身を確かめると、①受け付けの直後にイベントを送り、送信を待つ関数(待つ上限は1.5秒)で画面を移す作りでも、Chromiumで完了のイベントが送られないまま移った、②フォームをGETで送る作りでは、完了ページのURLに名前・メールアドレス・電話番号が載ってGA4へ送られる、という2つの落とし穴が実際に出ました。

この記事の内容のご相談は株式会社bundlyzeへbundlyzeのWEB戦略チーム代行について見てみる

この記事は、結婚式場やホテルの婚礼部門でフェア予約を受けている支配人・Web担当の方と、そのフォームを作る制作・開発の方に向けています。「フェアの詳細ページはよく見られているのに、予約が増えない」というとき、フォームの中のどの項目で止まっているかが分かれば、直す場所を1つに絞れます。フォームの実装の基本(日程の枠の扱い、自動入力の属性、確認メールの届け方)はフェア予約フォームのスマホ実装の記事で、予約から来館・成約までの計測の全体はGA4とCRMで追う記事で扱ったので、ここではフォームの中の計測に絞ります。コードとテストは2026年10月6日に手元のMacで動かし、出力をそのまま載せています。

標準の計測で分かるのは「触り始めた」と「送った」まで

GA4の拡張計測機能を有効にしていると、フォームの操作は form_start と form_submit の2つのイベントで記録されます。アナリティクスのヘルプでは、form_start は「ユーザーがセッションで初めてフォームを操作したとき」、form_submit は「ユーザーがフォームを送信したとき」に記録されると説明されています。

この2つで分かるのは、フォームを触り始めた人と送信した人の数までです。日程は選んだのに名前で止まったのか、メールアドレスの形式のエラーで止まったのかは分かりません。フォームの改善でいちばん知りたいのは、その間の部分です。

知りたいこと 標準の計測 この記事で足すイベント
フォームを触り始めた人の数 form_start (使わなくても数えられる)
どの項目まで正しく入れたか 分からない fair_form_field(項目の名前と順番)
どの項目でエラーが出たか 分からない fair_form_error(項目の名前とエラーの種類)
予約が成立した数 form_submit はボタンで送った数 fair_reserve_complete(受け付けのあとに完了ページで)

form_submit は送信の操作を数えるもので、予約が成立したかどうかは分かりません。満席や通信の失敗で受け付けられなかった送信も含まれ得るので、予約の数には使わず、サーバーが受け付けたあとの完了を別に数えます。

送るのは項目の名前・順番・エラーの種類だけにする

項目ごとのイベントには、何の項目か、上から何番目か、エラーなら何のエラーかだけを載せます。お客様が入力した名前・メールアドレス・電話番号・質問の文章は、どのイベントにも載せません。

Googleは、メールアドレスや個人の携帯電話番号などの個人情報をGoogleに送ることをポリシーで禁じています。アナリティクスのヘルプでは、サイトの訪問者がフォームの入力欄に個人情報を入力することがあるので、送信する前にそれを取り除くよう案内されています。「エラーの原因を調べたいから、打ち間違えた値も送る」という作りは、ここに当たります。

送るイベントの表は、次のとおりにしました。

イベント名 送るとき 載せる項目
fair_form_field 項目の値が確定し、その値が正しいとき(1回の表示で1項目1回) 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 予約が受け付けられて完了ページを開いたとき(1回だけ) fair_type

イベント名とパラメータの名前は40文字まで、1つのイベントに付けられるパラメータは25個まで、パラメータの値は100文字まで、とアナリティクスのヘルプに書かれています(標準のプロパティ)。この表の名前と値はどれも短いので、上限には届きません。

項目ごとのイベントを送るコード

フォームの下に読み込むスクリプトです。フォームの各項目に name が付いていれば、項目の一覧(FIELDS)を画面の上からの順に書くだけで使えます。テスト用の画面は、日程・人数・お名前・メールアドレス・電話番号・ご質問(任意)の6項目で、GA4の測定IDは実在しないテスト用の値にしています。

form-measure.jsjs
// フェア予約フォームの「どの項目まで進んだか」「どこでエラーが出たか」を 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 に印を残して完了ページへ移り、完了ページの側で印を読んで、消してから送ります。

thanks-measure.js(完了ページで読み込む)js
// 完了ページ:予約フォームから受け付けられて移ってきたときだけ、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 を送り、gtagの event_callback(送ったあとに呼ばれる関数。event_timeout で待つ上限を1.5秒にしていました)で完了ページへ移していました。この作りをスマホの画面サイズで自動テストすると、WebKit(iPhoneのSafariと同じエンジン)では完了のイベントが送られましたが、Chromium(AndroidのChromeと同じエンジン)では、完了のイベントも、その直前に入れた項目のイベントも送られないまま、完了ページへ移りました。

テストを見ていると、gtagはイベントを1件ずつすぐには送らず、数秒分をまとめて送っていました(このテストでは、最初の操作から約5秒後に送られていました)。まとめて送る前に画面を移すと、その間のイベントが送られないことがある、ということです。完了ページの側で送れば、フォームから画面を移す瞬間の取りこぼしは避けられます。ただし、完了ページを開いてすぐに閉じられた場合にも送られるかは、このテストでは確かめていません。印を消してから送るので、完了ページを開き直しても2回目は数えません。

スマホの画面で操作して、GA4へ送られる中身を確かめる

イベントが設計どおりに送られているか、入力した値が混ざっていないかは、GA4の管理画面を見る前に、送信そのものを確かめます。Playwright(ブラウザを自動で操作する道具)で、iPhone 13とPixel 7の画面サイズとタッチ操作を再現し、GA4の送信先(/g/collect)への送信を途中で受け取って中身を分解しました。受け取った送信は本物のGA4には届けず、その場で応答を返しています。

run.mjs(抜粋)js
// /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))));

試した操作は3つです。Aは、日程・人数・名前まで入れ、メールアドレスを打ちかけのまま「予約する」を押し、直さずに離れる人。Bは、最後まで入れて予約し、完了ページを1回開き直す人。Cは、比べるために作った、フォームをGETで送る版で最後まで予約する人です。gtagがまとめて送るのを待つため、A・B・Cそれぞれの終わり(送信のあと、完了ページの表示と開き直しのあと)に7秒待っています。項目を入れる合間には待っていません。

$ 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&note=

=== 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&note=

読み取れることは3つです。

  • Aでは、どこまで進んで、どこで止まったかが分かる。 3番目の名前までは正しく入り、メールアドレスは形式の違い、電話番号は未入力で止まった、という記録になりました。テストで入力した名前は送信のどこにも含まれず、イベントのパラメータに載っているのは項目の名前とエラーの種類だけでした(打ちかけのメールアドレスの文字列そのものを送信の中から探す処理は、このテストには入れていません)。
  • Bでは、完了はどちらのブラウザでも1件だった。 完了ページを開き直しても、2回目は数えませんでした(完了ページの表示は2件)。ただし、Chromiumでは、完了の前に入れた項目のイベントが送られないまま完了ページへ移りました。この差は、次の章の読み方に関わります。
  • CのGETの版では、入力した値がそのままGA4へ送られた。 フォームを method="get" で送ると、入力した値が完了ページのURLのクエリ(? の後ろ)に付きます。GA4は表示したページのURLを page_view と一緒に送るので、名前・メールアドレス・電話番号がそのまま載りました。アナリティクスのヘルプでも、URLやページタイトルに個人情報が含まれないよう確かめることが案内されています。予約フォームは、POSTで送る作りにします。

数字の読み方:止まった項目は「進んだ項目」と「エラー」を並べて見る

GA4で項目ごとに見るには、field_name と error_type を、イベントスコープのカスタムディメンションとして登録します。アナリティクスのヘルプでは、カスタムディメンションは作成から24〜48時間後にレポートで使えるようになると書かれているので、イベントを送り始める日に登録しておきます。標準のプロパティで登録できるイベントスコープのカスタムディメンションは50個までです。

見るときは、探索で「field_name ごとの fair_form_field を送った人数」と「field_name・error_type ごとの fair_form_error の数」を並べます。項目の順に人数が減っていく中で、急に減る項目と、エラーの多い項目が重なっていれば、そこが直す場所の候補です。

見えた形 疑うところ 直し方の例
メールアドレスの format のエラーが多い 全角の文字や空白が入る、キーボードが英字にならない type="email" と autocomplete="email"、前後の空白を取り除いてから確かめる
電話番号の format のエラーが多い ハイフンの有無、全角の数字 ハイフンありもなしも受け付け、全角の数字を半角に直してから確かめる
日程は選ぶのに人数で減る 人数の選び方が分かりにくい、選べる人数が合わない 選択肢と、同伴できる人数の案内を見直す
名前の手前で大きく減る 個人情報を入れる前の不安 何に使うか、営業の電話をするか、をフォームの上に書く

この読み方には、2つの注意があります。1つ目は、テストのBで見たとおり、送ってから数秒のうちに画面を移ると項目のイベントが送られないことがあり、とくに最後まで予約した人の項目のイベントが少なめに出ることです。予約した人は必須の項目をすべて正しく入れているので、項目ごとの到達を見るときは、完了の人数を各項目に足して読みます。2つ目は、GA4のファネル データ探索で項目を並べる場合、ヘルプにあるとおり、決めた順序どおりに進んだ人だけが数えられ、途中のステップを抜かした人はその後のステップを完了しても数えられないことです(途中のステップから始めた人も数える「オープン ファネル」にしても、抜かしたステップの後は数えられません)。お客様は上から順に入れるとは限らないので、項目の順序に頼らない、上の表のような並べ方から始めます。

フォームを1か所ずつ直して結果を比べる進め方は、フェア予約ページの改善を小さく回す記事に書いています。予約が減った原因が、フォームの手前(検索や広告、フェアの一覧)にありそうなときは、フェア予約が減ったときの切り分けの記事の順で確かめます。

自社でやる場合と、任せる場合

自社で進める場合は、次の順にすると、途中で計測が壊れたときにも気づけます。

  1. 送るイベントと、載せてよい項目・載せない項目の表を作る(この記事の表がそのまま使えます)。
  2. フォームがPOSTで送られていること、完了ページのURLとタイトルに入力値が入らないことを確かめる。
  3. 項目ごとのイベントと完了ページでの完了を実装し、field_name と error_type をカスタムディメンションに登録する。
  4. 公開前に、スマホの実機でAとBの操作を通し、GA4のリアルタイムやDebugViewでイベントが届くことを確かめる。
  5. 2〜4週間ためてから、項目ごとの人数とエラーを並べ、直す項目を1つ決める。

任せる場合、株式会社bundlyzeでは、外部のWeb責任者として、フェア予約までの計測の設計と実装、数字の読み取り、フォームやページの直しまでを続けて受け持っています。株式会社bundlyzeでは、結婚式場のWeb集客の一部として、フェアやプランのLP運用も行っています。予約フォームの作り直しや、予約の仕組みそのもののシステム開発もあわせてお受けしています。

動作確認した環境

確認日は2026年10月6日です。

  • 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 だけでは、足りませんか?

フォームを触り始めた数と送信した数は分かりますが、どの項目で止まったかは分かりません。アナリティクスのヘルプでは、form_start は「ユーザーがセッションで初めてフォームを操作したとき」に記録されるイベントと説明されています。項目ごとの到達とエラーを見たいときは、この記事のように項目の名前を付けたイベントを自分で送ります。

入力エラーのイベントに、入力された値を一緒に送って原因を調べてもよいですか?

送らないでください。Googleは、メールアドレスや個人の携帯電話番号などの個人情報をGoogleに送ることをポリシーで禁じていて、フォームに入力された情報は送信前に取り除くよう案内しています。この記事の作りでは、項目の名前とエラーの種類(未入力か形式の違いか)だけを送り、値は送りません。

完了のイベントは、送信ボタンを押したときに送ればよいですか?

サーバーが予約を受け付けたと返したあとに数えます。ボタンを押しただけでは、満席や通信の失敗で予約が成立していないことがあるからです。この記事のテストでは、受け付けの直後にイベントを送り、送信を待つ関数(待つ上限は1.5秒)で完了ページへ移る作りにすると、Chromiumでは完了のイベントが送られないまま画面が移りました。完了ページを開いたときに1回だけ送る作りに変えると、両方のブラウザで1件ずつ数えられました。

項目ごとのイベントを送り始めたら、すぐGA4のレポートで項目別に見られますか?

項目の名前などのパラメータは、イベントスコープのカスタムディメンションとして登録してから使います。アナリティクスのヘルプでは、作成から24〜48時間後にレポートで使えるようになると説明されています。送り始める日に、あわせて登録しておきます。

この記事の書き方と確認の方針