ブログ
ショッププラットフォームを使わない e コマースイベント:自作のチェックアウトを GA4 に報告させる
公開 · 読了 7 分
ショッププラットフォームなら、少なくとも格闘する相手としてプラグインがあります。自作のチェックアウト — React アプリ、予約フロー、裏で Stripe が動く SaaS の登録画面 — には何もありません。自分のコードがプッシュするまで、イベントは 1 つも発火しないのです。良い知らせは、約束事が小さいことです。いくつかの名前、1 つのオブジェクトの形、そして各プッシュをどこに置くかについての少しのルール。この記事はそのすべてを扱います。多くのガイドが省いている部分 — 誰もページにいないときに起きる購入 — も含めて。
約束事:名前と ecommerce オブジェクト
ここで扱うものはすべて、同じ経路でアナリティクスに届きます。ページが GA4 の名前でイベントを window.dataLayer にプッシュし、GA4 形式の ecommerce オブジェクトを添えます。Tag Manager のトリガーがその名前に一致し、GA4 イベントタグがオブジェクトを読んでサーバーコンテナへ送ります。SignalHost にサイトがショップだと伝えた場合 — あるいはコンテナページの「コンバージョンイベント」で「設定する」を押した場合 — それらのトリガーとタグは、SH という接頭辞付きの名前で、すでにウェブコンテナに存在します。残っているのはプッシュだけです。
2 つのルールで、たいていの間違いは防げます。まず、前の ecommerce オブジェクトをクリアすること。そうしないと、3 ページ前の view_item が次のイベントに紛れ込みます。そして金額を伴うすべてのイベントで、value を数値として、currency を ISO コードとして送ること。文字列の value や currency の欠落は何の警告もなく受け付けられ、黙って収益から除外されます。
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'begin_checkout',
ecommerce: {
currency: 'EUR',
value: 49.00,
items: [
{ item_id: 'plan-pro', item_name: 'Pro', price: 49.00, quantity: 1 }
]
}
});どのイベントを、どこに置くか
自作のフローにも、ショッププラットフォームと同じ場面があります。それを自分のコードの中で見つければよいだけです。
- view_item_list — 複数の商品やプランを表示するページやコンポーネント。リストが表示されたときに 1 回です。スクロールのたびにではありません。
- view_item — 単一の商品またはプランのページ。
- add_to_cart と remove_from_cart — ボタンのハンドラー内で、カートが実際に変更された後に。
- begin_checkout — 訪問者がチェックアウトに入った瞬間。チェックアウトページの最初のレンダリング、またはチェックアウトのダイアログを開くクリックです。
- add_payment_info — 支払い情報が受け付けられたとき。フォームが表示されたときではありません。
- purchase — 注文が確定したときに 1 回。この記事の大半は、これを正しく扱うための話です。
- sign_up と start_trial — サブスクリプション型のビジネスにとって、お金が動く前のコンバージョンです。SignalHost はショップのイベントと並べて、この 2 つのタグも構築します。
プッシュは変更を引き起こしたハンドラーから行い、レンダリングからは行わないでください。シングルページアプリは気ままに再レンダリングしますし、React は開発環境でエフェクトを 2 回実行します。エフェクト内のプッシュは開発ビルドで二重になり、コンポーネントが再マウントされるたびに繰り返されます。
確認ページでの purchase:注文はサーバーから読む
支払い後に訪問者があなたのページに戻ってくるなら、purchase を置くべきはそのページです — ただし条件が 2 つあります。注文は URL からではなく、自分のバックエンドから取得すること。クエリパラメータは書き換えられますし、リダイレクトで削られますし、決済が失敗したときにも付いてきます。たとえば Stripe は、支払いが成功してもしなくても同じアドレスに戻し、どちらだったかは独自のパラメータで知らせます。そしてプッシュは 1 回だけにすること。再読み込み、戻るボタン、ブックマークされた領収ページ — どれもそのページに戻ってきます。
// On your confirmation page. The order comes from YOUR server, by an id the
// page already knows — never from price or status parameters in the URL.
const order = await fetch(`/api/orders/${orderId}`).then((r) => r.json());
const key = `purchase-sent-${order.id}`;
if (order.status === 'paid' && !localStorage.getItem(key)) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: order.id,
value: order.total,
tax: order.tax,
currency: order.currency,
items: order.items.map((i) => ({
item_id: i.sku, item_name: i.name, price: i.price, quantity: i.quantity
}))
}
});
localStorage.setItem(key, '1');
}transaction_id は安全網であって、計画ではありません。アナリティクスは 1 つのセッション内で重複した transaction_id を捨てますが、翌朝の領収ページの再読み込みは捨てません。
他社のページでの決済:Stripe Checkout、PayPal、銀行へのリダイレクト
自作の構成では、ホスト型の決済ページが定番の選択肢です。そしてそこには鋭い落とし穴が 1 つあります。訪問者が戻ってこないかもしれないのです。支払ってタブを閉じれば、確認ページは一度もレンダリングされません。カード決済では、それが購入の数パーセントです。銀行振込、請求書払い、後払いでは大半になることもあります。入金が数時間後、あるいは数日後になるからです。
ですから、支払い方法ごとに、事実がどこにあるかを決めてください。バックエンドが Webhook で支払いを知るなら、報告に信頼できる場所は Webhook です(次のセクションを参照)。その場合、確認ページで purchase を重ねてプッシュしてはいけません。さもないと、カード決済がすべて二重に数えられます。支払いを知る場所が戻り先のページしかないなら、そこで報告し、取りこぼしは受け入れてください。
ブラウザが立ち会わない購入:サーバーから送信する
サブスクリプションでは、これは避けられません。トライアルは 7 日後に有料化し、更新は毎月発生し、請求書は来週振込で支払われます。どれも購入であり、どれもブラウザの前では起きません。答えは、支払いを知った Webhook から、ページが使っているのと同じタギングエンドポイント — SignalHost のホスト名か独自ドメイン — へ、サーバー間でイベントを送ることです。そうすれば、他のすべてのヒットと同じようにサーバーコンテナを通過します。
チェックアウトから必要なものは 1 つだけです。訪問者のアナリティクスのクライアント ID で、これがあれば購入は、そこに至る訪問をしてきた本人に紐づきます。クライアント ID は _ga Cookie に入っており、チェックアウトのリクエストが自社のドメイン宛てであれば、その Cookie はすでにリクエストに含まれています。注文と一緒に保存してください。_ga Cookie がないということは、訪問者がアナリティクスへの同意を与えていないということです。その場合は使い捨ての ID で同意拒否のヒットとして送るか、まったく送らないかのどちらかです — その人の身元を決してでっち上げないでください。
// In your checkout request handler: keep the visitor's Analytics id with the order.
const ga = req.cookies._ga; // "GA1.1.1234567890.1700000000"
order.gaClientId = ga ? ga.split('.').slice(-2).join('.') : null;
// Later, in your payment webhook — a trial converting, an invoice paid:
const params = new URLSearchParams({
v: '2',
tid: 'G-XXXXXXXXXX', // your GA4 measurement ID
cid: order.gaClientId ?? `${Date.now()}.${Math.floor(Math.random() * 1e9)}`,
gcs: order.gaClientId ? 'G101' : 'G100', // no _ga cookie = no consent
en: 'purchase',
'ep.transaction_id': invoice.id,
'epn.value': '49.00',
cu: 'EUR',
pr1: 'idplan-pro~nmPro~pr49.00~qt1',
});
await fetch(`https://metrics.example.com/g/collect?${params}`, { method: 'POST' });イベントごとに送信元を 1 つ選び、それを守ってください。signalhost.io が自社の売上を計測している方法が、まさにこれです。ブラウザが sign_up、begin_checkout、start_trial をプッシュし、Stripe の Webhook が最初の請求書の支払い時に purchase を送信します。同じイベントを両方から送ることはありません。Google の Measurement Protocol でも機能はしますが、Google に直接届いてサーバーコンテナを迂回するため、サーバー側のタグはどれもその売上を目にすることがありません。
同意のために余分な作業は要りません
訪問者が何を選んだかにかかわらず、イベントはプッシュしてください。タグは Consent Mode に従います。同意があれば通常どおり送信し、なければ Cookie なしの ping を送信します。アナリティクスはそれをレポートするのではなく、モデリングに使います。自分でプッシュを控えても、そのモデリングからコンバージョンを隠すだけです。大事なのは順序です。Tag Manager のスニペットは同意バナーのスクリプトの後に置いてください。そうすれば、最初のイベントが送信される前に同意のデフォルト値が存在します。
動作を確認する
- SignalHost でコンテナを開きます。イベントカードには、届いたものが名前ごとに、同意付きだった割合とともに一覧表示されます。プッシュしたのにそこに見当たらない名前は、一度もタギングサーバーに届いていません。
- Tag Manager のプレビューモードで、チェックアウトを一通りたどります。各プッシュ、どのタグが発火したか、送信された ecommerce オブジェクトが表示されます。
- アナリティクスでは、DebugView にイベントが届いた時点で表示されます。標準レポートには 1 日かかることがあります。
- 確認ページを 2 回再読み込みして、購入件数を数えます。それでも 1 件のはずです。
ここでの失敗は、どれも音を立てません。大文字と小文字を間違えた名前、文字列として送られた value、1 階層深く入れ子になった items。どれも、どこにもエラーを出しません。そしてどれもが、穴の空いたレポートを生みます。
