· HARUBOUYA · analytics · 13 min read
GA4移行後にCVが計上されなくなるGTMタグの直し方
タグは発火しているのにコンバージョンが記録されません。UA時代に書かれたカスタムHTMLタグが静かに失敗する仕組みと、修正前・修正後のコードを並べて直し方を整理します。

外部の計測ツールに送っているコンバージョンが、ある時期からゼロになっていました。GTM のプレビューで見るとタグは「発火」と表示されます。エラーも出ていません。それでも件数は増えません。
原因は、UA(ユニバーサルアナリティクス)時代に書かれたカスタム HTML タグが、GA4 移行後に前提を失っていたことです。同じ構造のタグは多くのサイトに残っているため、症状と直し方を整理しておきます。
なお、以下のコードは実際の実装をもとに、URL・識別子・ベンダー名を伏せた形に書き換えています。
修正前と修正後の違い(先に結論)
| 観点 | 修正前 | 修正後 |
|---|---|---|
| 値の取り方 | ページ側のグローバル変数を直接参照 | dataLayer+GTM変数を経由 |
| キー名 | UA の actionFields / products / sku | GA4 の transaction_id / items / item_id |
| 値がないとき | タグ内の if で黙って終了 | トリガー側で発火させません |
| 失敗したとき | console.log に名前だけ出力 | measurement_error を dataLayer に送信 |
| 値の確認方法 | タグの中を読むしかありません | プレビュー画面で値が見えます |
| 他タグへの依存 | タグの順序付けに依存 | 1回の push に必要な値をまとめます |
以下、修正前のコード、壊れる仕組み、修正後のコードの順に見ていきます。
修正前:問題のあるタグ
<!-- 修正前 -->
<script src="(計測ベンダーが配布するSDKのURL)"></script>
<script>
var cvParams = {};
function sendConversion() {
try {
cvParams['order_id'] = tmp_actionFields['id'];
if (customerId != null && customerId.length > 0) {
cvParams['user_id'] = customerId;
}
if (visitorId != null && visitorId.length > 0) {
cvParams['session_id'] = visitorId;
}
var productInfo = '';
for (var i = 0; i < tmp_products.length; i++) {
var row = tmp_products[i];
productInfo += (row['sku'] || row['id'] || '') + ':';
productInfo += row['price'] + ':';
productInfo += row['quantity'] + ',';
}
cvParams['product_info'] = productInfo.slice(0, -1);
vendorAnalytics.cv.sendTrackingRequest('(テナントID)', cvParams);
} catch (error) {
console.log(error.name);
}
}
if (typeof tmp_products !== 'undefined' && tmp_products != null) {
if (typeof tmp_actionFields !== 'undefined' && tmp_actionFields != null) {
sendConversion();
}
}
</script>tmp_products、tmp_actionFields、customerId、visitorId は、ページ側が定義したグローバル変数です。変数名はサイトによって異なりますが、UA の拡張eコマース実装ではこの形がよく使われていました。
なぜ気づけないのか
このタグが厄介なのは、壊れても何も言わない点にあります。理由が3つ重なっています。
1. 未定義チェックが「静かに何もしない」
最後の if で、グローバル変数が未定義なら関数を呼ばずに終わります。GA4 移行でページ側の dataLayer 設計が変わり、これらの変数が定義されなくなると、タグはエラーも出さず、送信もせずに終了します。
2. GTM のプレビューでは「発火」と表示される
GTM が判定しているのは、トリガー条件を満たしてタグが実行されたかどうかです。タグの中で処理が握りつぶされていても、画面上は正常に見えます。発火=成功ではありません。
3. エラーが console.log に出るだけ
catch の中が console.log(error.name) のみです。ブラウザのコンソールを開いている人がいなければ、誰も気づきません。しかも出るのは例外の名前だけで、原因の特定に使えません。
さらに、このタグは「タグの順序付け」で別の前処理タグに依存しています。前処理タグ側の変更でも同じように静かに壊れます。
修正後:タグの全体
先に完成形を示します。変更点はこのあと1つずつ説明します。
<!-- 修正後 -->
<script>
(function () {
var orderId = {{DLV - transaction_id}};
var items = {{DLV - items}};
// 値が揃わないときは、黙って終わらずに失敗を記録する
if (!orderId || !items || !items.length) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'measurement_error',
error_tag: 'cv_tag',
error_reason: !orderId ? 'missing_transaction_id' : 'missing_items',
});
return;
}
var productInfo = items
.map(function (item) {
return [item.item_id, item.price, item.quantity].join(':');
})
.join(',');
vendorAnalytics.cv.sendTrackingRequest('(テナントID)', {
order_id: orderId,
user_id: {{DLV - user_id}},
product_info: productInfo,
});
})();
</script>あわせて、GTM 側で次の設定を行います。
| 設定 | 内容 |
|---|---|
| データレイヤー変数 | DLV - transaction_id / DLV - items / DLV - user_id |
| トリガー | カスタムイベント purchase、条件に {{DLV - transaction_id}} が空でないことを追加 |
| SDK の読み込み | タグ内の <script src> ではなく、別タグに分けます |
何を変えたか
1. グローバル変数への直接参照をやめる
修正前
// ページ側の変数を、そのまま読んでいる
cvParams['order_id'] = tmp_actionFields['id'];修正後
// GTM の変数を経由して読む
var orderId = {{DLV - transaction_id}};GTM の「変数」でデータレイヤー変数を定義しておきます。
| 変数名の例 | データレイヤーのキー |
|---|---|
| DLV - transaction_id | ecommerce.transaction_id |
| DLV - items | ecommerce.items |
| DLV - user_id | user_id |
こうすると、値が取れているかどうかを GTM のプレビュー画面で確認できるようになります。タグの中を読まなくても分かる状態にすることが目的です。
2. GA4 の purchase イベントの構造に合わせる
GA4 の purchase では、商品は ecommerce.items の配列で渡されます。キー名も UA 時代とは違います。
| 修正前(UA の拡張eコマース) | 修正後(GA4) |
|---|---|
actionFields.id | transaction_id |
products[].sku / id | items[].item_id |
products[].price | items[].price |
products[].quantity | items[].quantity |
商品情報を組み立てる部分も書き換えます。
修正前
var productInfo = '';
for (var i = 0; i < tmp_products.length; i++) {
var row = tmp_products[i];
productInfo += (row['sku'] || row['id'] || '') + ':';
productInfo += row['price'] + ':';
productInfo += row['quantity'] + ',';
}
cvParams['product_info'] = productInfo.slice(0, -1);修正後
var productInfo = items
.map(function (item) {
return [item.item_id, item.price, item.quantity].join(':');
})
.join(',');末尾の区切り文字を slice(0, -1) で削る処理が不要になります。商品が0件のときに1文字削ってしまう不具合も、同時に消えます。
3. 失敗を握りつぶさず、検知できる形にする
修正前
} catch (error) {
console.log(error.name); // 誰も見ていない
}修正後
window.dataLayer.push({
event: 'measurement_error',
error_tag: 'cv_tag',
error_reason: 'missing_transaction_id',
});値が揃わなかったときに measurement_error イベントを dataLayer に流します。これだけで次のことができるようになります。
- GTM のプレビューで、失敗したことがイベントとして見えます
- GA4 側でこのイベントをカスタムイベントとして受け、発生件数を監視できます
- 「CVが0件」ではなく「CVタグが値を取れていない」という形で検知できます
console.log で終わらせません。気づける場所に出すことが要点です。
4. 発火条件はトリガー側に寄せる
修正前
// タグの中で判定している
if (typeof tmp_products !== 'undefined' && tmp_products != null) {
if (typeof tmp_actionFields !== 'undefined' && tmp_actionFields != null) {
sendConversion();
}
}修正後
判定をタグから外し、トリガーの条件にします。
| 項目 | 設定 |
|---|---|
| トリガーの種類 | カスタムイベント |
| イベント名 | purchase |
| 条件 | {{DLV - transaction_id}} が空でありません |
タグ内の if で判定すると、発火したのに何も起きない状態が生まれます。トリガー側に寄せると、値がないときはタグ自体が発火しません。GTM のプレビューで「発火しなかったタグ」として一覧に出るため、状態が画面の表示と一致します。
5. タグの順序付けへの依存を減らす
修正前:前処理タグでグローバル変数を組み立て、後続タグがそれを読みます。片方の変更で静かに壊れます。
修正後:サイト側で 1回の dataLayer.push に必要な値をすべて入れます。順序付けが不要になります。
// サイト側(購入完了ページ)
window.dataLayer.push({
event: 'purchase',
user_id: 'U12345',
ecommerce: {
transaction_id: 'A12345',
value: 12800,
currency: 'JPY',
items: [{ item_id: 'SKU-001', price: 6400, quantity: 2 }],
},
});実装側の改修が必要になりますが、長期的にはこれが一番効きます。
直したあとの検証手順
- GTM のプレビューモードで購入完了まで進みます
- Variables タブで、定義した各変数に値が入っているか確認します
- Data Layer タブで
ecommerceの中身を確認します - ブラウザの開発者ツールの Network タブで、計測リクエストが実際に送信され、正常なステータスが返っているか確認します
- 計測ツール側の管理画面で件数を確認します
- GA4 の
purchase件数と突き合わせ、乖離がないか見ます
5と6を飛ばすと、「タグは直ったけれど値が間違っている」状態を見逃します。
移行時のチェックリスト
UA 時代のカスタム HTML タグが残っている場合、次を確認します。
| 確認項目 | 危ないサイン |
|---|---|
| ページ側のグローバル変数を直接参照していないか | tmp_ などで始まる変数名が出てきます |
| 未定義チェックで黙って終了していないか | typeof ... !== 'undefined' で囲んだだけの if |
エラー処理が console.log で終わっていないか | catch の中が1行だけ |
| タグの順序付けに依存していないか | 「設定タグ」が指定されています |
| UA のキー名が残っていないか | actionFields、products、sku |
| 送信件数を監視しているか | 誰も件数を見ていません |
まとめ
- 発火している=送信できている、ではありません。GTM の表示だけで判断しません
- 修正前はページ側のグローバル変数に依存していました。GA4 移行で前提が変わり、静かに壊れました
- 修正後は dataLayer と GTM 変数を経由させ、プレビュー画面で値が見える状態にしました
- 失敗したら
measurement_errorを流し、検知できる形にしました - 条件はタグ内の
ifではなくトリガーに寄せました - 直したあとは件数の突き合わせまで行います
計測は、壊れても誰も困らない時間が長く続いてから発覚します。壊れたことが分かる仕組みを一緒に入れておきます。
参考になったら、いいねを押してください。





