広告クリックIDが「消える」問題
Google広告をクリックしたユーザーがフォームを送信する。その間にgclidは失われる。
広告プラットフォームはクリックIDをURLパラメータとして付与する。だがサイト内でページ遷移が起きた時点で、そのパラメータはURLから消える。外部フォームサービスを埋め込んでいる場合はなおさらだ。フォーム送信データにクリックIDが含まれなければ、CRM上のリードと広告クリックを紐付ける手段がない。
この記事では、GTM(Googleタグマネージャー)のカスタムHTMLタグ3本だけで、広告クリックからフォーム送信までを1つのIDで追跡するパイプラインを構築する方法を解説する。サーバーサイドの開発は不要。GTMの管理画面だけで完結する。
全体アーキテクチャ: 3タグ構成
パイプラインは3つのカスタムHTMLタグで構成される。
┌─────────────────────────────────────────────────┐
│ GTM Container │
│ │
│ [Tag 1] UUID生成 ← Initialization │
│ └→ localStorage + Cookie に保存 │
│ └→ dataLayer.push / window.__lead_uuid │
│ │
│ [Tag 2] クリックID・UTM取得 ← Initialization │
│ └→ gclid / fbclid / yclid / UTM を抽出 │
│ └→ localStorage + Cookie(90日)に保存 │
│ └→ ランディングページURLを初回のみ記録 │
│ │
│ [Tag 3] フォーム自動注入 ← DOM Ready │
│ └→ iframe[src] にクエリパラメータを付与 │
│ └→ MutationObserver で遅延読み込み対応 │
│ │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 外部フォームサービス │
│ hidden field で lead_uuid / gclid 等を受け取り │
│ → CRM / スプレッドシートに記録 │
└─────────────────────────────────────────────────┘
タグ1とタグ2はInitializationタイミングで発火し、タグ3はDOM Readyで発火する。タグシーケンス(発火順序)でTag 1 → Tag 2 → Tag 3の順を保証する。
タグ1: UUID生成
全訪問者に対して一意のリードIDを発行する。UUID v4形式を使う。
設計方針
- 初回訪問時にUUIDを生成し、再訪問時は既存値を再利用する
- localStorageとCookieの二重保持でITP(Intelligent Tracking Prevention)に備える
crypto.randomUUID()を優先し、非対応ブラウザにはフォールバックを用意する
コード例
<script>
(function() {
var STORAGE_KEY = 'lead_uuid';
var COOKIE_DAYS = 390;
// Cookie読み書きユーティリティ
function getCookie(name) {
var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? decodeURIComponent(match[2]) : null;
}
function setCookie(name, value, days) {
var expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = name + '=' + encodeURIComponent(value)
+ ';expires=' + expires
+ ';path=/;SameSite=Lax';
}
// UUID v4 生成(フォールバック付き)
function generateUUID() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0;
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
// 既存値を探す(localStorage優先 → Cookie)
var uuid = null;
try { uuid = localStorage.getItem(STORAGE_KEY); } catch(e) {}
if (!uuid) uuid = getCookie(STORAGE_KEY);
// なければ新規生成
if (!uuid) uuid = generateUUID();
// 二重保持で書き戻す
try { localStorage.setItem(STORAGE_KEY, uuid); } catch(e) {}
setCookie(STORAGE_KEY, uuid, COOKIE_DAYS);
// 後続タグから参照できるようにする
window.__lead_uuid = uuid;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
lead_uuid: uuid
});
})();
</script>
ポイント
- Cookieの有効期限は390日。Chromeの1st Party Cookieの上限(400日)に収まる範囲で最大化している
SameSite=Laxを指定することで、クロスサイトリクエストでは送信されない1st Party Cookieになるwindow.__lead_uuidに格納することで、後続のタグ2・タグ3からグローバル変数として参照できるdataLayer.pushしておけば、GA4のイベントパラメータとしても取得可能になる
タグ2: クリックID・UTM取得
URLパラメータから広告クリックID(gclid, fbclid, yclid)とUTMパラメータを抽出し、保存する。
設計方針
- 初回のランディング時のみパラメータを取得する(上書き防止)
- ランディングページURLも記録し、流入元の完全な情報を保持する
- Cookieの有効期限は90日。広告のアトリビューションウィンドウに合わせる
コード例
<script>
(function() {
var COOKIE_DAYS = 90;
var PARAMS = ['gclid','fbclid','yclid','utm_source','utm_medium',
'utm_campaign','utm_term','utm_content'];
function getCookie(name) {
var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? decodeURIComponent(match[2]) : null;
}
function setCookie(name, value, days) {
var expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = name + '=' + encodeURIComponent(value)
+ ';expires=' + expires
+ ';path=/;SameSite=Lax';
}
function store(key, value) {
try { localStorage.setItem(key, value); } catch(e) {}
setCookie(key, value, COOKIE_DAYS);
}
function retrieve(key) {
var v = null;
try { v = localStorage.getItem(key); } catch(e) {}
return v || getCookie(key);
}
var url = new URL(window.location.href);
var stored = {};
PARAMS.forEach(function(param) {
var val = url.searchParams.get(param);
if (val) {
store(param, val);
stored[param] = val;
} else {
var existing = retrieve(param);
if (existing) stored[param] = existing;
}
});
// ランディングページURL(初回のみ)
if (!retrieve('landing_page')) {
var lp = window.location.origin + window.location.pathname;
store('landing_page', lp);
stored.landing_page = lp;
} else {
stored.landing_page = retrieve('landing_page');
}
// グローバル変数に格納
window.__lead_params = stored;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(stored);
})();
</script>
ポイント
- URLにパラメータがある場合のみ上書きする。直接訪問やブックマーク経由では既存値を維持する
- ランディングページURLは
origin + pathnameのみ保存する。クエリパラメータは含めない(個人情報やトークンが混入するリスクを避ける) - 90日の有効期限は、Google広告のデフォルトのアトリビューションウィンドウ(90日)に合わせた設定
タグ3: フォーム自動注入
ページ上の外部フォーム(iframe埋め込み)を検出し、リードIDとクリックIDをクエリパラメータとして自動注入する。
設計方針
- iframe要素のsrc属性にクエリパラメータを追加する方式
- ページ読み込み後に動的に挿入されるiframeにも対応する(MutationObserver)
- フォームサービスのドメインで対象iframeを絞り込む
コード例
<script>
(function() {
// 注入するパラメータを収集
var params = {};
if (window.__lead_uuid) params.lead_uuid = window.__lead_uuid;
var leadParams = window.__lead_params || {};
['gclid','fbclid','yclid','utm_source','utm_medium',
'utm_campaign','landing_page'].forEach(function(key) {
if (leadParams[key]) params[key] = leadParams[key];
});
if (Object.keys(params).length === 0) return;
var queryString = Object.keys(params).map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
}).join('&');
// 対象フォームのドメインを指定(環境に合わせて変更)
var FORM_DOMAIN = 'form.example.com';
function injectParams(iframe) {
if (!iframe.src || iframe.src.indexOf(FORM_DOMAIN) === -1) return;
if (iframe.dataset.paramsInjected) return;
var separator = iframe.src.indexOf('?') === -1 ? '?' : '&';
iframe.src = iframe.src + separator + queryString;
iframe.dataset.paramsInjected = 'true';
}
// 既存のiframeに注入
document.querySelectorAll('iframe').forEach(injectParams);
// 遅延読み込みされるiframeにも対応
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
m.addedNodes.forEach(function(node) {
if (node.tagName === 'IFRAME') injectParams(node);
if (node.querySelectorAll) {
node.querySelectorAll('iframe').forEach(injectParams);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
</script>
ポイント
FORM_DOMAIN変数で対象のフォームサービスを絞り込む。無関係なiframe(YouTube埋め込み等)にパラメータが付与されるのを防ぐdataset.paramsInjectedフラグで二重注入を防止する- MutationObserverはページ全体を監視対象にしている。パフォーマンスへの影響は軽微だが、フォームが特定のコンテナ内にしか出現しない場合は監視範囲を限定してもよい
フォーム側の設定: hidden fieldの設計
フォームサービス側では、注入されたクエリパラメータをhidden field(非表示フィールド)として受け取る設定が必要になる。
設定の流れ
- フォームにhidden fieldを追加する。フィールド名は
lead_uuid、gclid、fbclid等 - 各hidden fieldの「デフォルト値をURLパラメータから取得する」オプションを有効にする
- 多くのフォームサービス(Typeform, HubSpot Forms, Jotform等)はこの機能を標準で備えている
フォームサービスがURLパラメータの自動取得に対応していない場合は、フォーム内にJavaScriptを追加してクエリパラメータを手動でhidden fieldに注入する必要がある。
テスト手順
実装後は以下の手順で動作を確認する。
1. パラメータ付きURLでアクセスする
https://example.com/lp?gclid=test123&utm_source=google&utm_medium=cpc
2. ブラウザの開発者ツールで確認する
- Console:
window.__lead_uuidとwindow.__lead_paramsを確認。値が格納されているか - Application > Local Storage:
lead_uuid,gclid,utm_source等のキーが保存されているか - Application > Cookies: 同じキーがCookieにも保存されているか
3. iframe注入を確認する
- Elementsパネルでiframeのsrc属性を確認する。
?lead_uuid=xxx&gclid=test123が付与されているか - GTMのプレビューモード(Tag Assistant)で3つのタグが正しい順序で発火しているか確認する
4. フォーム送信テスト
- テスト送信を行い、フォームサービスの送信データにlead_uuid, gclid等が含まれているか確認する
- 別のページに遷移してからフォームページに戻っても、値が維持されているか確認する
応用: GA4・BigQueryへの拡張
このパイプラインはフォーム送信の追跡だけにとどまらない。GA4やBigQueryと組み合わせることで、さらに高度な分析が可能になる。
GA4カスタムディメンション
- タグ1で
dataLayer.pushしたlead_uuidを、GA4のユーザースコープカスタムディメンションとして設定する - これにより、GA4上で特定のリードがサイト内でどのページを閲覧し、どのイベントを発火させたかを追跡できる
BigQueryエクスポートとの連携
- GA4のBigQueryエクスポートを有効にすれば、
lead_uuid付きの行動ログがBigQueryに蓄積される - CRMの成約データ(lead_uuidをキーにして)とJOINすることで、「どの広告キャンペーンから流入したリードが、最終的に成約したか」をSQLで分析できる
データフローの全体像
広告クリック → LP(UUID + gclid保存)
→ サイト回遊(GA4にlead_uuid送信)
→ フォーム送信(lead_uuid + gclid をフォームデータに含む)
→ CRM(lead_uuid + gclid で広告クリックと紐付け)
→ BigQuery(GA4行動ログ + CRM成約データをlead_uuidでJOIN)
まとめ
GTMのカスタムHTMLタグ3本で、広告クリックからフォーム送信までの追跡パイプラインを構築できる。要点を整理する。
- タグ1: UUID v4を生成し、localStorage + Cookieの二重保持でITPに対応する
- タグ2: gclid/fbclid/yclid/UTMをURLから取得し、90日間保持する
- タグ3: iframe埋め込みフォームにクエリパラメータを自動注入する。MutationObserverで動的要素にも対応
サーバーサイド開発なしで、GTMの管理画面だけで完結する点がこの設計の強みだ。広告費のROIを正確に測定するための第一歩として、試してみてほしい。

