F2T相談してみる
広告運用

GTMカスタムHTMLタグ3つで作る広告リード追跡パイプライン──gclid取得からオフラインCV連携・ITP対策・Consent Modeまで完全解説

GTMカスタムHTMLタグ3つで作る広告リード追跡パイプライン──gclid取得からオフラインCV連携・ITP対策・Consent Modeまで完全解説

この記事で解決できること

  • 広告をクリックしたユーザーが、フォームを送信するまでの流れをGTMカスタムHTMLタグ3つで追跡できるようになる
  • gclid・fbclid・yclidやUTMパラメータを取りこぼさずフォームに渡す仕組みを、コピペで動くコード付きで実装できる
  • Safari ITPの現実的な限界を理解し、サーバーサイドGTMやGoogle Tag Gatewayとの併用を判断できる
  • Google Consent Mode v2への対応方法と、同意なしでCookieを書き込む法的リスクを把握できる
  • 取得したgclidをGoogle Adsに戻すオフラインコンバージョンインポートの完全フローを構築できる
  • 拡張コンバージョン for リードで、Cookieに依存しないマッチ率向上を実装できる
  • Jotform以外のフォーム(Contact Form 7、HubSpot Forms、Typeform、ネイティブHTML)にも応用できる

なぜ「広告クリック→フォーム送信」の追跡がこんなに面倒なのか

病院の受付を想像してほしい。患者が来院したとき、「どの広告を見て来ましたか?」と聞いても、ほとんどの人は覚えていない。だから病院では紹介状や予約番号で来院経路を管理する。

Web広告のリード追跡も同じだ。Google広告をクリックした人がフォームを送信したとき、「この人はどの広告から来たのか」を紐づけるには、紹介状にあたるID(gclid)をクリックからフォーム送信まで持ち回す仕組みが必要になる。

ところが、この「持ち回す」がとにかく厄介だ。理由は3つある。

  1. URLパラメータはページ遷移で消える。広告のランディングページにはgclidが付くが、フォームページに遷移した時点でURLから消える
  2. Cookieはブラウザに削除される。SafariのITPは、JavaScriptで書いたCookieを最短24時間で消す
  3. フォームが外部サービスのiframe。JotformやTypeformをiframeで埋め込んでいる場合、親ページのデータを直接渡せない

この記事では、GTMのカスタムHTMLタグ3つでこれらの問題を解決するパイプラインを構築する。そのうえで、ITPの現実的な限界、Consent Modeへの対応、オフラインCVインポートまで、実務で必要な全工程をカバーする。

全体アーキテクチャ: 3つのタグが担う役割

まず全体像を掴もう。病院の比喩で言えば、こうなる。

タグ

役割

病院で言えば

タグ1: UUID生成

訪問者に一意のIDを振る

初診の患者に診察券番号を発行する

タグ2: パラメータ取得

gclid/UTM等の広告情報を保存する

紹介状の内容をカルテに転記する

タグ3: フォーム注入

保存した情報をフォームに渡す

カルテの情報を問診票に添付する

3つのタグはすべてGTMのカスタムHTMLタグとして実装する。トリガーは「All Pages」(全ページ)で、ページが読み込まれるたびに動く。

タグ1: UUID生成 ── 訪問者に診察券番号を発行する

なぜUUIDが必要か

gclidはGoogle広告のクリックごとに発行されるが、同じユーザーが複数回訪問しても同一人物だと判別できない。また、Meta広告やYahoo広告にはgclidに相当するIDがあるものの、フォーマットがバラバラだ。

そこで、自社で発行する一意のID(UUID)を各訪問者に振る。これが診察券番号にあたる。

実装コード

<script>
(function() {
  'use strict';

  var COOKIE_NAME = '_lead_uuid';
  var LS_KEY = '_lead_uuid';
  var COOKIE_DAYS = 390;

  // 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;
      var v = c === 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  }

  // Cookie書き込み
  function setCookie(name, value, days) {
    var expires = '';
    if (days) {
      var date = new Date();
      date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
      expires = '; expires=' + date.toUTCString();
    }
    document.cookie = name + '=' + encodeURIComponent(value)
      + expires + '; path=/; SameSite=Lax; Secure';
  }

  // Cookie読み取り
  function getCookie(name) {
    var nameEQ = name + '=';
    var ca = document.cookie.split(';');
    for (var i = 0; i < ca.length; i++) {
      var c = ca[i].trim();
      if (c.indexOf(nameEQ) === 0) {
        return decodeURIComponent(c.substring(nameEQ.length));
      }
    }
    return null;
  }

  // localStorage読み取り(エラー時はnull)
  function getLS(key) {
    try { return localStorage.getItem(key); } catch(e) { return null; }
  }

  // localStorage書き込み(エラー時は無視)
  function setLS(key, value) {
    try { localStorage.setItem(key, value); } catch(e) {}
  }

  // 既存UUIDの取得(Cookie優先、なければlocalStorage)
  var uuid = getCookie(COOKIE_NAME) || getLS(LS_KEY);

  if (!uuid) {
    uuid = generateUUID();
  }

  // 二重保持: CookieとlocalStorageの両方に書く
  setCookie(COOKIE_NAME, uuid, COOKIE_DAYS);
  setLS(LS_KEY, uuid);

  // dataLayerに格納(GA4やGTMの他タグから参照可能にする)
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    'lead_uuid': uuid
  });
})();
</script>

CookieとlocalStorageの二重保持にする理由

「Cookieだけでいいのでは?」と思うかもしれないが、二重に保持する理由がある。

  • Cookie: SafariのITPにより、JavaScriptで書いたCookieは最大7日で消える(後述)。ただし、サーバーサイドGTMを使えばHTTPレスポンスヘッダーでCookieを設定でき、この制限を回避できる
  • localStorage: ITPの影響を受けない。ただし、Safariのプライベートブラウジングでは使えない。また、ユーザーが手動で消せる

片方が消えても、もう片方から復元できるようにしておく。完璧ではないが、取りこぼしを減らす実務的な判断だ。

タグ2: 広告パラメータ取得 ── 紹介状の内容をカルテに転記する

取得するパラメータ一覧

パラメータ

媒体

用途

gclid

Google Ads

クリックID。オフラインCV連携に必須

gbraid / wbraid

Google Ads

iOS14.5以降のプライバシー対応クリックID

fbclid

Meta Ads

Facebookのクリック追跡ID

yclid

Yahoo広告

Yahoo広告のクリック追跡ID

msclkid

Microsoft Ads

Bingのクリック追跡ID

utm_source

共通

流入元(google, facebook等)

utm_medium

共通

メディア種別(cpc, display等)

utm_campaign

共通

キャンペーン名

utm_content

共通

広告コンテンツ

utm_term

共通

検索キーワード

実装コード

<script>
(function() {
  'use strict';

  var PARAMS = [
    'gclid', 'gbraid', 'wbraid',
    'fbclid', 'yclid', 'msclkid',
    'utm_source', 'utm_medium', 'utm_campaign',
    'utm_content', 'utm_term'
  ];
  var STORAGE_PREFIX = '_lp_';
  var COOKIE_DAYS = 390;

  function setCookie(name, value, days) {
    var date = new Date();
    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
    document.cookie = name + '=' + encodeURIComponent(value)
      + '; expires=' + date.toUTCString()
      + '; path=/; SameSite=Lax; Secure';
  }

  function getCookie(name) {
    var nameEQ = name + '=';
    var ca = document.cookie.split(';');
    for (var i = 0; i < ca.length; i++) {
      var c = ca[i].trim();
      if (c.indexOf(nameEQ) === 0) {
        return decodeURIComponent(c.substring(nameEQ.length));
      }
    }
    return null;
  }

  function getLS(key) {
    try { return localStorage.getItem(key); } catch(e) { return null; }
  }

  function setLS(key, value) {
    try { localStorage.setItem(key, value); } catch(e) {}
  }

  // URLからパラメータを取得
  var urlParams = new URLSearchParams(window.location.search);
  var dlData = {};

  PARAMS.forEach(function(param) {
    var urlValue = urlParams.get(param);

    if (urlValue) {
      // URLにパラメータがある場合: 保存して上書き
      var storageKey = STORAGE_PREFIX + param;
      setCookie(storageKey, urlValue, COOKIE_DAYS);
      setLS(storageKey, urlValue);
      dlData['lp_' + param] = urlValue;
    } else {
      // URLにない場合: 既存の保存値を読み込み
      var storageKey = STORAGE_PREFIX + param;
      var saved = getCookie(storageKey) || getLS(storageKey);
      if (saved) {
        dlData['lp_' + param] = saved;
      }
    }
  });

  // ランディングページURLも保存(初回のみ)
  var lpKey = STORAGE_PREFIX + 'landing_page';
  if (!getCookie(lpKey) && !getLS(lpKey)) {
    var lp = window.location.href.split('?')[0];
    setCookie(lpKey, lp, COOKIE_DAYS);
    setLS(lpKey, lp);
    dlData['lp_landing_page'] = lp;
  } else {
    dlData['lp_landing_page'] = getCookie(lpKey) || getLS(lpKey);
  }

  // 初回訪問日時も保存
  var ftKey = STORAGE_PREFIX + 'first_touch';
  if (!getCookie(ftKey) && !getLS(ftKey)) {
    var now = new Date().toISOString();
    setCookie(ftKey, now, COOKIE_DAYS);
    setLS(ftKey, now);
    dlData['lp_first_touch'] = now;
  } else {
    dlData['lp_first_touch'] = getCookie(ftKey) || getLS(ftKey);
  }

  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push(dlData);
})();
</script>

ポイント: 上書きルール

URLにパラメータがあるときは常に上書きする。これはラストクリックアトリビューションの考え方で、最後にクリックした広告が成果の起点として記録される。

もしファーストクリック(最初に接触した広告)を残したい場合は、上書き条件を「保存値が空のときだけ書き込む」に変える。この判断はビジネス要件による。

タグ3: フォームへのパラメータ注入 ── 問診票にカルテ情報を添付する

iframeフォーム(Jotform)の場合

Jotformをiframeで埋め込んでいる場合、親ページのJavaScriptからiframe内のDOMに直接アクセスできない(クロスオリジン制約)。そのため、iframeのURLにクエリパラメータとして渡す方法を使う。

<script>
(function() {
  'use strict';

  var STORAGE_PREFIX = '_lp_';
  var PARAMS = [
    'gclid', 'gbraid', 'wbraid',
    'fbclid', 'yclid', 'msclkid',
    'utm_source', 'utm_medium', 'utm_campaign',
    'utm_content', 'utm_term',
    'landing_page', 'first_touch'
  ];

  function getCookie(name) {
    var nameEQ = name + '=';
    var ca = document.cookie.split(';');
    for (var i = 0; i < ca.length; i++) {
      var c = ca[i].trim();
      if (c.indexOf(nameEQ) === 0) {
        return decodeURIComponent(c.substring(nameEQ.length));
      }
    }
    return null;
  }

  function getLS(key) {
    try { return localStorage.getItem(key); } catch(e) { return null; }
  }

  // UUIDも取得
  var uuid = getCookie('_lead_uuid') || getLS('_lead_uuid');

  // 保存済みパラメータを収集
  var queryParts = [];
  if (uuid) {
    queryParts.push('lead_uuid=' + encodeURIComponent(uuid));
  }

  PARAMS.forEach(function(param) {
    var value = getCookie(STORAGE_PREFIX + param) || getLS(STORAGE_PREFIX + param);
    if (value) {
      queryParts.push(param + '=' + encodeURIComponent(value));
    }
  });

  if (queryParts.length === 0) return;

  var queryString = queryParts.join('&');

  // iframeのsrcにパラメータを追加
  function injectToIframes() {
    var iframes = document.querySelectorAll('iframe[src*="jotform.com"]');
    iframes.forEach(function(iframe) {
      var src = iframe.getAttribute('src');
      if (src.indexOf('lead_uuid') !== -1) return; // 注入済みならスキップ
      var separator = src.indexOf('?') !== -1 ? '&' : '?';
      iframe.setAttribute('src', src + separator + queryString);
    });
  }

  // ページ読み込み時に実行
  injectToIframes();

  // SPAやJotformの遅延読み込み対応: MutationObserverで監視
  if (typeof MutationObserver !== 'undefined') {
    var observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        mutation.addedNodes.forEach(function(node) {
          if (node.nodeType === 1) {
            if (node.tagName === 'IFRAME' && node.src &&
                node.src.indexOf('jotform.com') !== -1) {
              injectToIframes();
            }
            // 子要素にiframeがある場合も検知
            var childIframes = node.querySelectorAll
              ? node.querySelectorAll('iframe[src*="jotform.com"]')
              : [];
            if (childIframes.length > 0) {
              injectToIframes();
            }
          }
        });
      });
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }
})();
</script>

Jotform側のhidden field設定

  1. Jotformのフォームビルダーで「Hidden Field」要素を追加する
  2. フィールド名を lead_uuidgclidutm_source など、渡したいパラメータ名と一致させる
  3. Jotformは、iframeのURLクエリパラメータとhidden fieldの名前が一致すると、自動的に値をセットする
  4. フォームの送信テストで、送信データにパラメータが含まれていることを確認する

Jotform以外のフォームに応用する

タグ3はJotformのiframe向けに書いたが、同じ考え方で他のフォームサービスにも対応できる。以下、主要なパターンを整理する。

Contact Form 7(WordPress)

Contact Form 7は親ページのDOMに直接レンダリングされるので、iframeの制約がない。hidden fieldに値を直接セットする。

まず、Contact Form 7のフォームテンプレートにhidden fieldを追加する。

[hidden gclid id:gclid default:""]
[hidden utm_source id:utm_source default:""]
[hidden utm_medium id:utm_medium default:""]
[hidden utm_campaign id:utm_campaign default:""]
[hidden lead_uuid id:lead_uuid default:""]

次に、GTMのカスタムHTMLタグ(タグ3の代替版)でhidden fieldに値を注入する。

<script>
(function() {
  'use strict';

  var STORAGE_PREFIX = '_lp_';
  var FIELDS = {
    'gclid': 'gclid',
    'utm_source': 'utm_source',
    'utm_medium': 'utm_medium',
    'utm_campaign': 'utm_campaign'
  };

  function getCookie(name) {
    var nameEQ = name + '=';
    var ca = document.cookie.split(';');
    for (var i = 0; i < ca.length; i++) {
      var c = ca[i].trim();
      if (c.indexOf(nameEQ) === 0) {
        return decodeURIComponent(c.substring(nameEQ.length));
      }
    }
    return null;
  }

  function getLS(key) {
    try { return localStorage.getItem(key); } catch(e) { return null; }
  }

  function fillFields() {
    // UUID
    var uuid = getCookie('_lead_uuid') || getLS('_lead_uuid');
    var uuidField = document.getElementById('lead_uuid');
    if (uuid && uuidField) uuidField.value = uuid;

    // 広告パラメータ
    Object.keys(FIELDS).forEach(function(param) {
      var value = getCookie(STORAGE_PREFIX + param)
        || getLS(STORAGE_PREFIX + param);
      var field = document.getElementById(FIELDS[param]);
      if (value && field) field.value = value;
    });
  }

  // DOM読み込み完了後に実行
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', fillFields);
  } else {
    fillFields();
  }
})();
</script>

HubSpot Forms

HubSpot Formsはhidden fieldをサポートしている。ただし、HubSpotには独自のトラッキングCookie(hutk)があるため、2つのアプローチがある。

  • 方法A: HubSpotのhidden field機能を使う。フォーム作成画面でhidden fieldを追加し、GTMから値をセットする。HubSpotのembed APIの onFormReady コールバックを使う
  • 方法B: HubSpotのネイティブトラッキングに任せる。HubSpotはhutk Cookieでユーザーを追跡し、Google Adsとの連携機能もある。自前パイプラインが不要になるケースもある
<script>
// HubSpot Forms の onFormReady コールバックで注入
window.addEventListener('message', function(event) {
  if (event.data.type === 'hsFormCallback'
      && event.data.eventName === 'onFormReady') {

    var form = document.querySelector('iframe.hs-form-iframe');
    // HubSpot Formsは同一ドメインのiframeなので
    // アクセス可能な場合がある(embedモード依存)

    // Embed API方式(推奨)
    // hbspt.forms.create の onFormReady で直接操作する
  }
});

// 推奨パターン: HubSpot Embed APIのコールバック
hbspt.forms.create({
  portalId: 'YOUR_PORTAL_ID',
  formId: 'YOUR_FORM_ID',
  onFormReady: function($form) {
    var gclid = getCookie('_lp_gclid') || getLS('_lp_gclid');
    if (gclid) {
      $form.find('input[name="gclid"]').val(gclid).change();
    }
  }
});
</script>

Typeform

Typeformにはiframe埋め込み版とJavaScript embed版がある。

  • iframe版: Jotformと同じ方法でURLパラメータを付与する。ただしTypeformはhidden fieldの名前とURLパラメータの紐づけを、Typeform管理画面の「Hidden Fields」設定で行う必要がある
  • JavaScript embed版: Typeformの createWidget API で hidden オプションにオブジェクトを渡せる
<script>
// Typeform JavaScript embed版
window.tf.createWidget('YOUR_FORM_ID', {
  container: document.getElementById('typeform-container'),
  hidden: {
    gclid: getCookie('_lp_gclid') || '',
    utm_source: getCookie('_lp_utm_source') || '',
    lead_uuid: getCookie('_lead_uuid') || ''
  }
});
</script>

ネイティブHTMLフォーム

自作のHTMLフォームなら一番シンプルだ。hidden inputを追加して、JavaScriptで値をセットするだけでいい。

<!-- フォーム内にhidden inputを追加 -->
<form action="/submit" method="POST">
  <input type="hidden" name="gclid" id="form-gclid">
  <input type="hidden" name="utm_source" id="form-utm-source">
  <input type="hidden" name="lead_uuid" id="form-lead-uuid">
  <!-- 通常のフォームフィールド -->
  <input type="text" name="name" placeholder="お名前">
  <input type="email" name="email" placeholder="メールアドレス">
  <button type="submit">送信</button>
</form>

React / SPA(シングルページアプリケーション)のフォーム

SPAでは画面遷移がページ読み込みではなくJavaScriptのhistory操作で行われるため、GTMのトリガー設定に注意が必要だ。

  • GTMの「履歴の変更」トリガーを使い、画面遷移ごとにタグを発火させる
  • フォームのsubmitイベントをインターセプトし、送信データにパラメータを追加する
  • ReactのuseEffectでlocalStorageから値を読み取り、フォームのstateにセットするのが自然

Safari ITP: 2026年の現実と、正直な限界

ここからは、多くの記事が避けている「不都合な真実」を書く。

ITPの歴史と現状

AppleのIntelligent Tracking Prevention(ITP)は、2017年にSafari 11で導入されて以来、段階的に強化されてきた。2026年3月現在の状況を正確にまとめる。

制限

対象

有効期間

JavaScriptで書いたCookie(document.cookie)

Safari全般

最大7日

リンクデコレーション経由のCookie

gclid等のパラメータ付きURLから書いたCookie

最大24時間

HTTPレスポンスヘッダーで書いたCookie(1st party)

サーバーサイドGTMなど

通常の有効期限(最大400日)

HTTPレスポンスヘッダーのCookie(CNAME + 異なるIP)

CDN経由のサーバーサイドGTM

最大7日

この記事のパイプラインへの影響

タグ1・タグ2で書いているCookieは、すべて document.cookie(JavaScript由来のクライアントサイドCookie)だ。つまり、Safariユーザーの場合、広告クリックから7日以内にフォーム送信しなければ、gclidが消えている。

さらに厳しいのが「リンクデコレーション」の判定だ。Google広告のURLには ?gclid=xxx というパラメータが付く。SafariのITPはこれを「トラッキング目的のリンクデコレーション」と判定し、そのページで書かれたCookieの有効期限を24時間に短縮する。

つまり現実的には、Safariユーザーがgclidでマッチングできるのは、広告クリックから24時間以内にフォーム送信した場合だけだ。

localStorageは救世主か?

localStorageはITPの直接的な制限を受けない。だからこそ、タグ1でCookieとlocalStorageの二重保持にしている。localStorageにはgclidが残っているので、Cookieが消えた後もフォームに渡せる。

ただし、Safari 26(2025年9月リリース)以降、Advanced Fingerprinting Protection(AFP)がデフォルトで有効になった。AFPはlocalStorageを直接消すわけではないが、フィンガープリンティング検出の対象範囲が広がっている。将来的にlocalStorageへの制限が強化される可能性はある。

対策: サーバーサイドGTM + Google Tag Gateway

根本的な解決策は、サーバーサイドGTM(sGTM)を使い、CookieをHTTPレスポンスヘッダーで設定することだ。サーバー側で設定された1st party Cookieは、ITPの「JavaScript由来Cookie」制限を受けない。

ただし、sGTMを自社ドメインのサブドメイン(例: sgtm.example.com)で運用していても、そのサブドメインのIPアドレスがメインサイトと異なる場合は、やはり7日間に制限される。これは、CNAME経由の「なんちゃって1st party」を検出するITPの仕組みだ。

この問題を解消するのがGoogle Tag Gatewayだ。Google Tag Gatewayを使えば、Google Cloud上にsGTMをデプロイし、自社のインフラと同一IPレンジで運用できる。詳しくはGoogle Tag Gatewayで実現するファーストパーティ計測を参照してほしい。

正直な結論

クライアントサイドだけのパイプライン(この記事のタグ1〜3)では、Safariユーザーの長期追跡は不完全だ。これは認めなければいけない。

ただし、以下の理由から「だから無意味」とはならない。

  • 日本のBtoBサイトではChromeのシェアが50〜60%を占める。Chromeユーザーに対しては問題なく動く
  • Safariユーザーでも、広告クリックから24時間以内のCV(即日CVR)は追跡できる
  • 後述する拡張コンバージョン for リードを併用すれば、Cookieに依存しないマッチングが可能になる
  • localStorageのフォールバックにより、Cookieが消えた後もある程度のカバーは維持できる

Consent Mode: 同意なしでCookieを書くリスク

法的背景を整理する

ここまでのコードは、ページが読み込まれた瞬間にCookieとlocalStorageに書き込む。しかし、ユーザーの同意なくトラッキング用のCookieを書くことは、法的リスクを伴う。

  • 日本の改正個人情報保護法(2022年施行): Cookie情報は「個人関連情報」に該当する。Cookie情報を第三者(Google Adsなど)に提供し、提供先で個人データと紐づける場合、本人の同意が必要
  • 電気通信事業法(2023年施行の外部送信規律): 電気通信事業者に該当するサイトは、Cookieによる外部送信について通知・公表または同意取得が必要
  • GDPR(EU向けサイト): トラッキングCookieの設置には事前のオプトイン同意が必須。同意バナーで「すべて拒否」を選んだユーザーにはCookieを書けない

Google Consent Mode v2

Google Consent Mode v2は、ユーザーの同意状態に応じてGoogleタグの動作を制御する仕組みだ。2024年3月以降、EEA(欧州経済領域)からのトラフィックに対してはConsent Modeの実装が必須になっている。

Consent Modeには2つのシグナルがある。

シグナル

用途

denied(拒否)時の動作

ad_storage

広告関連のCookie

広告Cookieを書き込まない。Cookieレスping(集約データ)のみ送信

analytics_storage

アナリティクスのCookie

GA4のCookieを書き込まない。Cookieレスpingのみ送信

この記事のパイプラインとConsent Modeの統合

問題は、タグ1〜3が書くCookieは「GTMのGoogleタグ」ではなく「カスタムHTMLタグ」だという点だ。Consent Modeは、Googleの公式タグ(GA4タグ、Google Adsタグなど)の動作を制御するが、カスタムHTMLタグのCookie書き込みは自動制御しない。

つまり、自分でConsent Modeの状態を確認し、同意がない場合はCookie書き込みをスキップするロジックを追加する必要がある。

<script>
(function() {
  'use strict';

  // Consent Modeの同意状態を確認
  function hasAdStorageConsent() {
    // Google Consent Modeのデフォルトコマンドを確認
    var consentState = window.google_tag_data
      && window.google_tag_data.ics
      && window.google_tag_data.ics.entries;

    // より確実な方法: GTMのdataLayerからconsentイベントを確認
    var consentGranted = false;
    (window.dataLayer || []).forEach(function(entry) {
      if (entry[0] === 'consent' && entry[1] === 'update') {
        if (entry[2] && entry[2].ad_storage === 'granted') {
          consentGranted = true;
        }
      }
    });
    return consentGranted;
  }

  // 同意バナーの結果を待ってからCookieを書く
  // 方法1: GTMトリガーで制御する(推奨)
  //   → カスタムHTMLタグのトリガーに
  //     「Cookie同意granted」のカスタムイベントを追加

  // 方法2: コード内でチェックする
  if (!hasAdStorageConsent()) {
    // 同意がない場合はlocalStorageのみ使用し、Cookieは書かない
    // localStorageへの書き込みもGDPR下では同意が必要な場合がある
    console.log('Ad storage consent not granted. Skipping cookie write.');
    return;
  }

  // 以降、通常のCookie書き込み処理...
})();
</script>

実務的な推奨パターン

最もシンプルで確実なのは、GTMのトリガー側でConsent Modeを制御する方法だ。

  1. GTMの「タグ」設定画面で、タグ1〜3のそれぞれに「同意の設定」を追加する
  2. 「追加の同意チェック」で ad_storage を「必須」に設定する
  3. これにより、ad_storagegranted になるまでタグが発火しなくなる

この方法なら、コード自体は変更不要だ。GTMの管理画面だけで同意制御ができる。

日本国内のBtoBサイトで、EUからのアクセスがほぼないケースでは、改正個人情報保護法の「外部送信規律」への対応が優先事項になる。Cookie利用目的の公表(プライバシーポリシーへの記載)は最低限必要だ。同意バナーの要否は、電気通信事業者に該当するかどうかで変わる。判断に迷うときは法務に相談してほしい。

拡張コンバージョン for リード(Enhanced Conversions for Leads)

仕組み

ここまで構築したパイプラインは、gclid(広告クリックID)をキーにして広告とフォーム送信を紐づける。しかし、gclidはCookieやlocalStorageに保存するため、ITPやユーザーのCookieクリアで失われるリスクがある。

拡張コンバージョン for リード(ECL)は、Googleが提供するCookieに依存しない補完的なマッチング手段だ。仕組みはこうだ。

  1. ユーザーがフォームにメールアドレスを入力して送信する
  2. そのメールアドレスをSHA-256でハッシュ化してGoogleに送信する
  3. Googleは、広告クリック時にログインしていたGoogleアカウントのメールアドレスと照合する
  4. 一致すれば、Cookieがなくても広告クリックとコンバージョンが紐づく

つまり、gclidベースのマッチングと、メールアドレスベースのマッチングの二段構えにできる。

GTMでの設定手順

  1. コンバージョンリンカータグを追加する。GTMで「コンバージョンリンカー」タグを作成し、「All Pages」トリガーで発火させる。これがECLの前提条件になる
  2. Google Adsコンバージョンタグの設定。既存のGoogle Adsコンバージョンタグで「ユーザー提供データを含める」にチェックを入れる
  3. ユーザー提供データの変数を作成する。GTMの「変数」で「ユーザー提供データ」変数を新規作成し、メールアドレスのDOM要素またはdataLayerキーを指定する
  4. フォーム送信トリガーで発火させる。フォーム送信イベント(GTMのフォーム送信トリガーまたはカスタムイベント)で、コンバージョンタグが発火するように設定する

dataLayerへのユーザーデータ送信例

<script>
// フォーム送信時にdataLayerにプッシュ
// ※ハッシュ化はGTMが自動で行うため、平文で渡してよい
//   (GTM側でSHA-256ハッシュ化される)
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'event': 'form_submit',
  'user_data': {
    'email': document.getElementById('email-field').value,
    'phone_number': document.getElementById('phone-field').value
  }
});
</script>

パイプラインとの補完関係

整理すると、この記事のパイプラインとECLは以下のように補完し合う。

マッチング方法

キー

ITPの影響

用途

パイプライン(タグ1〜3)

gclid + UUID

受ける

全媒体対応。CRMへのデータ連携

拡張コンバージョン for リード

メールアドレス(ハッシュ化)

受けない

Google Adsのコンバージョン計測精度向上

両方を実装することで、Cookieが消えても、メールアドレスでマッチングできる。逆にメールアドレスが取得できない場合(電話のみのフォーム等)でも、gclidでマッチングできる。

オフラインCVインポート: gclidをGoogle Adsに戻す完全ループ

ここが、多くの記事で「BigQueryで分析できます」と軽く触れて終わる部分だ。しかし、gclidをGoogle Adsに返すことで初めて、広告の自動入札が「質の高いリード」を学習できるようになる。これが本丸だ。

全体フロー

  1. ユーザーが広告をクリックしてサイトに来る(gclidが発行される)
  2. フォームを送信する(gclidがフォームデータに含まれる)
  3. フォームデータがCRM(Salesforce、HubSpot等)やスプレッドシートに格納される
  4. 営業がリードを評価し、「商談化」「成約」などのステータスを更新する
  5. 成約したリードのgclidとコンバージョン日時をGoogle Adsにアップロードする
  6. Google Adsの自動入札が「成約につながるクリック」の特徴を学習する

方法1: CSV手動インポート

最もシンプルな方法。月間リード数が少ない(50件以下程度)場合に向いている。

  1. スプレッドシートやCRMから、成約したリードの gclidコンバージョン名コンバージョン日時コンバージョン値 をCSVで書き出す
  2. Google Ads管理画面の「ツールと設定」→「コンバージョン」→「アップロード」からCSVをアップロードする

CSVのフォーマットは以下の通り。

Parameters:TimeZone=Asia/Tokyo
Google Click ID,Conversion Name,Conversion Time,Conversion Value
EAIaIQobChMI...,成約,2026-03-15 14:30:00,50000
EAIaIQobChMI...,商談化,2026-03-10 10:00:00,0

方法2: Google Ads APIによる自動アップロード

リード数が多い場合や、リアルタイム性が求められる場合はAPIを使う。Google Ads APIの ConversionUploadService でオフラインコンバージョンをアップロードできる。

# Python例(google-ads-python クライアントライブラリ)
from google.ads.googleads.client import GoogleAdsClient

client = GoogleAdsClient.load_from_storage('google-ads.yaml')

conversion_upload_service = client.get_service('ConversionUploadService')
conversion_action_service = client.get_service('ConversionActionService')

# アップロードするコンバージョンデータ
click_conversion = client.get_type('ClickConversion')
click_conversion.gclid = 'EAIaIQobChMI...'
click_conversion.conversion_action = (
    f'customers/{CUSTOMER_ID}/conversionActions/{CONVERSION_ACTION_ID}'
)
click_conversion.conversion_date_time = '2026-03-15 14:30:00+09:00'
click_conversion.conversion_value = 50000.0
click_conversion.currency_code = 'JPY'

# アップロード実行
request = client.get_type('UploadClickConversionsRequest')
request.customer_id = CUSTOMER_ID
request.conversions.append(click_conversion)
request.partial_failure = True

response = conversion_upload_service.upload_click_conversions(
    request=request
)

# 結果確認
for result in response.results:
    print(f'Uploaded: gclid={result.gclid}')

方法3: Zapier / n8nによるノーコード自動連携

CRMとGoogle Adsの間をノーコードツールでつなぐ方法。エンジニアリソースが限られる場合に有効だ。

  • Zapier: 「HubSpotの取引ステージが変更されたら → Google Adsにオフラインコンバージョンをアップロード」というZapを作成する。Google Adsの公式Zapier連携でオフラインCVアップロードがサポートされている
  • n8n: 同様のフローをn8nで構築する。セルフホスト可能なので、データが外部サービスを経由しない利点がある。自動化ツールの選定基準については自動化の判断基準フレームワークも参考にしてほしい

方法4: CRM直接連携

SalesforceやHubSpotには、Google Adsとの直接連携機能がある。

  • Salesforce: Google Adsの「Salesforce連携」を設定すると、Salesforceのリードステージ変更が自動的にGoogle Adsにコンバージョンとしてインポートされる
  • HubSpot: HubSpotのGoogle Ads連携で、取引ステージの変更をオフラインコンバージョンとして送信できる

CRM直接連携は設定がシンプルだが、カスタマイズ性は低い。「成約」だけでなく「高単価成約」「リピート成約」などの細かいコンバージョンアクションを分けたい場合は、API連携のほうが柔軟だ。

インポートの注意点

  • gclidの有効期限は90日。広告クリックから90日以内にインポートしないと、Google Adsがgclidを認識できない
  • コンバージョン日時は、実際の成約日を使う。アップロード日ではなく、成約が確定した日時を指定する
  • 重複アップロードに注意。同じgclidと同じコンバージョンアクションの組み合わせは、Google Adsが重複として弾くことがある(設定による)
  • アップロードから反映まで最大24時間。すぐにはGoogle Adsのレポートに反映されない

デバッグとトラブルシューティング

「設定したけどデータが来ない」──この問題に遭遇する確率は高い。原因の切り分けフローを整理する。

ステップ1: Cookie / localStorageに値が保存されているか確認

  1. Chrome DevToolsを開く(F12)
  2. 「Application」タブ → 左メニューの「Cookies」→ サイトのドメインを選択
  3. _lead_uuid_lp_gclid などのCookieが存在するか確認する
  4. 同じく「Local Storage」→ サイトのドメインで _lead_uuid_lp_gclid を確認する

値がない場合、タグ1・タグ2が発火していない。GTMプレビューモードで確認する。

ステップ2: GTMプレビューモードでタグの発火を確認

  1. GTM管理画面で「プレビュー」をクリック
  2. サイトのURLを入力して接続
  3. Tag Assistantパネルで、タグ1〜3が「Tags Fired」に表示されているか確認する
  4. 「Tags Not Fired」に表示されている場合、トリガー条件またはConsent Mode設定を確認する

ステップ3: iframeのsrcにパラメータが付与されているか確認

  1. Chrome DevToolsの「Elements」タブで、iframeタグを検索する(Ctrl+F で「iframe」)
  2. src属性に lead_uuid=gclid= が含まれているか確認する
  3. 含まれていない場合、タグ3のMutationObserverがiframeの追加より前に実行されている可能性がある

よくある原因と対策

症状

原因

対策

Cookieに値がない

Consent Modeでタグがブロックされている

GTMの「同意の設定」を確認。テスト時は一時的に同意を許可する

localStorageに値がない

プライベートブラウジングモード

通常モードでテストする

iframeのsrcにパラメータがない

iframeがJavaScriptで遅延生成されている

MutationObserverが正しく動作しているか確認。observeのターゲット要素を確認する

Jotformの送信データにパラメータがない

Jotform側のhidden field名がURLパラメータ名と不一致

フィールド名を完全一致させる(大文字小文字も区別される)

別ドメインのiframeにアクセスできない

クロスオリジン制約

iframeのDOMには直接アクセスせず、URLパラメータ方式で渡す(タグ3のアプローチ)

SPAでページ遷移後にタグが発火しない

GTMのトリガーが通常のページビューのみ

「履歴の変更」トリガーを追加する

本番環境でのデータ検証

テスト環境で動作確認ができたら、本番環境でも実際にデータが流れているかを検証する。

  • GA4 DebugView: GA4の管理画面 →「DebugView」で、リアルタイムのイベントとパラメータを確認できる。Chrome拡張機能「Google Analytics Debugger」を有効にすると、自分のアクセスがDebugViewに表示される
  • Jotform / CRMの送信データ確認: フォーム送信後、Jotformの管理画面またはCRMでhidden fieldの値が正しく記録されているか確認する
  • Google Adsのオフラインコンバージョン診断: Google Ads管理画面の「ツールと設定」→「コンバージョン」→「診断」で、アップロードしたコンバージョンの処理状況を確認できる

クロスドメイン環境での注意点

広告のランディングページとフォームページが別ドメインにある場合(例: 広告から lp.example-clinic.com に着地し、フォームが www.example-clinic.com にある場合)、ドメインをまたぐときにCookieとlocalStorageが共有されない。

この場合の対策は2つある。

  1. URLパラメータで引き継ぐ。ドメイン間のリンクに、保存済みのgclidやUUIDをクエリパラメータとして付与する。タグ2がURLパラメータから値を読み取る仕組みなので、遷移先でも自動的にCookie/localStorageに保存される
  2. GA4のクロスドメイントラッキングと併用する。GA4のクロスドメイン設定を行えば、GA4のクライアントIDがドメイン間で引き継がれる。この記事のパイプラインはGA4とは独立した仕組みだが、GA4のクロスドメイン設定も併せて行っておくとよい。詳しくはGA4クロスドメイントラッキングの設定方法を参照

まとめ: 実装チェックリスト

この記事で構築したパイプラインの全体像を振り返る。

ステップ

内容

所要時間(目安)

1

GTMでカスタムHTMLタグ3つを作成・公開

30分

2

フォーム(Jotform/CF7/HubSpot等)にhidden fieldを追加

15分

3

テスト: Cookie/localStorage、iframeパラメータ、フォーム送信データの確認

30分

4

Consent Modeの設定(GTMの同意設定 + 同意バナー連携)

1〜2時間

5

拡張コンバージョン for リードの設定

1時間

6

オフラインCVインポートのフロー構築(CSV手動 or API自動)

2〜4時間

7

sGTM / Google Tag Gatewayの導入検討(ITP対策の根本解決)

半日〜1日

ステップ1〜3だけでも基本的なリード追跡は動く。ステップ4以降は、計測精度を上げていくための段階的な改善だ。

最初から完璧を目指す必要はない。まずはタグ3つを入れて、フォームにgclidが渡っていることを確認する。それだけで、「どの広告がリードを生んでいるか」という最も基本的な問いに答えられるようになる。

あわせて読みたい

関連記事