· HARUBOUYA · analytics · 14 min read

ポーリングで待つCVタグは、なぜそう書かれたのか

外部ベンダーのCVタグをGTMから動かすとき、250msごとに値を待つ実装になりがちです。そう書かざるを得なかった理由と、そこから何を削れるかを整理します。

ポーリングの待機を表す図。250msごとの再確認を示す点が並び、最後に完了のチェックが付いている

外部ベンダーの計測タグを GTM のカスタム HTML から動かすと、しばしば次のような形になります。値が揃うまで 250ms ごとに待ち、諦めたら診断イベントを出します。

一見すると過剰に見えます。ただし、この形にはそれぞれ理由があります。理由を押さえたうえでないと、削ってはいけないところを削ることになります。

前回の記事では「静かに失敗するタグ」を扱いました。今回はその反省を踏まえて書き直された側のコードを見ます。以下は実際の実装をもとに、ベンダー名・サイトID・URL・変数名を伏せた形に書き換えています。

対象のコード

<div id="vendor_cv_tag"
  data-cid="(ベンダー発行のサイトID)"
  data-purchaseid=""
  data-itemnum=""
  data-itemid=""
  data-price=""
  data-size=""
  data-cuid="{{DLV - user_id}}">
</div>
<script>
(function () {
  var tries = 0;
  var MAX = 40;
  var ITEM_GRACE = 12;
  var orderSeenAt = -1;
  var DIAG = true;

  function getDLPurchase() {
    var dl = window.dataLayer || [];
    for (var i = dl.length - 1; i >= 0; i--) {
      var x = dl[i];
      if (x && x.ecommerce && x.ecommerce.purchase &&
          x.ecommerce.purchase.actionField && x.ecommerce.purchase.actionField.id) {
        return x.ecommerce.purchase;
      }
    }
    return null;
  }

  function getData() {
    if (typeof orderObject !== 'undefined' && orderObject && orderObject.orderId) {
      return {
        src: 'orderObject',
        orderId: orderObject.orderId,
        userId: orderObject.userId || '',
        items: (orderObject.items || []).map(function (it) {
          return { id: it.productId, price: it.unitPrice, quantity: it.quantity, size: it.size };
        })
      };
    }
    var p = getDLPurchase();
    if (p) {
      return {
        src: 'dataLayer',
        orderId: p.actionField.id,
        userId: '',
        items: (p.products || []).map(function (pr) {
          return { id: pr.id, price: pr.price, quantity: pr.quantity, size: pr.size || pr.variant };
        })
      };
    }
    return null;
  }

  function wait() {
    var d = getData();
    if (d && d.orderId) {
      if (orderSeenAt < 0) orderSeenAt = tries;
      if (d.items && d.items.length > 0) { send(d, 'ok'); return; }
      if ((tries - orderSeenAt) < ITEM_GRACE && tries < MAX) { tries++; setTimeout(wait, 250); return; }
      send(d, 'timeout_empty_items'); return;
    }
    if (tries++ < MAX) { setTimeout(wait, 250); return; }
    diag('give_up_no_order', null);
  }

  wait();
})();
</script>

send() は各値を data-* 属性に書き戻し、そのあとベンダー配布の JS を <script> として body に差し込みます。diag() は状況を dataLayer に push します。

なぜこう書かれたのか

1. ベンダーのインターフェースが DOM 経由だから

このベンダーのタグは、data-* 属性に値を入れた要素を置き、その後で配布 JS を読み込むと、JS が DOM を読んで送信する方式です。

JavaScript の関数を呼ぶ形なら引数で渡せば済みます。DOM 経由だと「先に値を入れる」「後で JS を読む」という順序が必須になり、その順序制御をカスタム HTML 側で書くことになります。

読み込み URL に ?v= + タイムスタンプが付いているのも同じ理由です。キャッシュされた JS が再実行されないケースを避けるためです。

2. データの到着タイミングが読めないから

トリガーがページビューになっています。購入完了ページを開いた瞬間に発火しますが、注文データはその時点でまだ確定していないことがあります。

  • 注文データを後から API で取得して描画しています
  • SPA でルート遷移後に dataLayer.push されます
  • タグマネージャの読み込みのほうが先に終わります

このため「値が来るまで待つ」しかありません。MAX = 40、250ms 間隔で最大10秒です。

3. 注文IDと商品明細の到着がずれるから

ITEM_GRACE = 12 がこれです。注文ID(orderId)は先に入りますが、商品明細(items)が数百ミリ秒遅れて入るケースがあります。

注文IDを見つけた時点で全体の待機を打ち切ると、商品情報が空のまま送信されます。かといって無限に待つわけにもいきません。そこで「注文IDを見つけてから、さらに12回(3秒)だけ待つ」という二段構えになっています。

4. データソースが2系統あるから

getData() が orderObject(ページ側のグローバル変数)を先に見て、なければ dataLayer の UA 形式(ecommerce.purchase.actionField)を見ます。

これは移行期の産物です。ページによって実装が異なる、あるいは移行が完了していないため、両方に対応せざるを得ない状態です。

5. 過去に静かに失敗した経験があるから

diag() の存在がそれです。診断用のイベントを dataLayer に流し、理由(ok / timeout_empty_items / give_up_no_order)、データの取得元、待機時間を記録しています。

この部分は素直に良い実装です。前回扱った「エラーが console.log で終わるタグ」と比べると、失敗が観測可能になっています。

ここからどう改善するか

理由があって書かれたコードなので、闇雲には削れません。効果の大きい順に並べます。

改善1:トリガーを変えてポーリング自体をなくす(最優先)

ポーリングはページビュートリガーで発火していることの帳尻合わせです。トリガーを変えれば根本から消えます。

  • トリガー:カスタムイベント purchase(サイト側が dataLayer.push するイベント)
  • 条件:{{DLV - transaction_id}} が空でない、かつ {{DLV - items}} の要素数が 0 でありません

こうすると、値が揃った瞬間にだけタグが発火します。wait()、MAX、ITEM_GRACE、orderSeenAt がすべて不要になり、コードは半分以下になります。

サイト側の改修が要る場合もありますが、dataLayer.push を1回追加するだけで済むことが多いです。10秒待つ処理を保守し続けるより安いです。

改善2:待ち時間中の離脱を勘定に入れる

トリガーを変えられない事情があるなら、10秒という待ち時間の意味を確認しておきます。

購入完了ページを10秒見ずに閉じる人は一定数います。その分は CV が欠損します。欠損率を把握するには、give_up_no_order の発生件数を数えるだけでは足りません(ページを閉じた場合は診断イベントも送られません)。

GA4 の purchase 件数と、ベンダー側の CV 件数を日次で突き合わせて差分を見るのが確実です。

改善3:データソースを1本に寄せる計画を持つ

2系統対応は移行期には正しいです。問題は、いつ片方を消すか決まっていないことです。

  • diag() が記録している src(orderObject / dataLayer)を GA4 のカスタムディメンションとして集計します
  • 一方の経路が数週間ゼロになったら、そのコードを削除します

記録しているだけで判断に使っていないと、2系統対応が永久に残ります。

改善4:空の catch をやめる

try { ... } catch (e) {}

例外が起きても何も残りません。diag() の仕組みがあるので、そこに流せばよいです。

} catch (e) {
  diag('exception:' + (e && e.name ? e.name : 'unknown'), d);
}

改善5:ベンダーJSの読み込み失敗を検知する

現状は <script> を差し込んだ時点で成功扱いです。ベンダー側の障害や広告ブロッカーで読み込めなくても気づけません。

jsEl.onerror = function () { diag('vendor_script_error', d); };
jsEl.onload  = function () { diag('vendor_script_loaded', d); };

「タグは動いた」と「ベンダーに届いた」を分けて観測します。

改善6:二重送信を防ぐ

SPA の遷移や、ページビュートリガーが複数回発火する構成では、同じ注文が2回送られる可能性があります。送信済みの注文IDを保持して弾きます。

window.__cvSent = window.__cvSent || {};
if (window.__cvSent[d.orderId]) { diag('duplicate_skipped', d); return; }
window.__cvSent[d.orderId] = true;

CV の水増しは、欠損より発見が遅れます。件数が「多い」ことは問題として認識されにくいためです。

改善7:値の変換仕様を確認する

2点、事故になりやすい箇所があります。

金額の丸め:Math.floor(Number(price) || 0) は小数を切り捨てます。税込・税抜のどちらを送るのか、送料や割引を含むのかをベンダー仕様と照合します。GA4 の value と定義がずれていると、後で突き合わせができません。

区切り文字:複数商品を %23(# のエンコード)で連結しつつ、各値を encodeURIComponent しています。商品IDやサイズ表記に # や % が含まれると壊れる可能性があります。実データに記号が混ざっていないか確認し、混ざるならテストケースを1つ作っておきます。

改善8:診断イベントを監視に載せる

diag() は送っているだけでは意味がありません。

  1. GA4 で vendor_cv_diag をカスタムイベントとして受けます
  2. reason(ok / timeout_empty_items / give_up_no_order / exception:*)をカスタムディメンションにします
  3. 日次で、理由別の件数と ok の比率をダッシュボード化します
  4. ok 比率が閾値を下回ったら気づける状態にします

計測の異常は、売上の異常より発見が遅れます。数字を見ている人がいないためです。閾値と通知先まで決めて、初めて監視になります。

優先順位のまとめ

優先度改善効果
高トリガーをカスタムイベントに変更ポーリング処理が丸ごと不要になります
高診断イベントを GA4 で監視壊れたことに気づけるようになります
中二重送信の防止件数の水増しを防ぎます
中ベンダーJSの読み込み失敗を検知「届いていない」を切り分けられます
中データソースの1本化計画保守対象を減らします
低空の catch をやめます例外の原因が追えます
低金額・区切り文字の仕様確認突き合わせの精度が上がります

まとめ

  • ポーリングは、ページビュートリガーで発火していることの帳尻合わせです。トリガーを変えれば消えます
  • ITEM_GRACE のような二段構えの待機は、注文IDと明細の到着差を吸収するための工夫です。理由を理解せずに消さないでください
  • 2系統のデータソース対応は移行期には正しいです。いつ消すかを決めておきます
  • 診断イベントを入れた判断は正しいです。あとは監視に載せます
  • 計測タグは、壊れても売上には影響しません。だから誰も気づきません。気づく仕組みまでを実装に含めます
Share:

参考になったら、いいねを押してください。

コメント

    お名前とコメントは、確認後にこのページで公開されます。メールアドレスや電話番号などは書かないでください。

    Back to Blog

    Related Posts

    View All Posts »
    サイズレコメンドのタグをGTMから入れる

    サイズレコメンドのタグをGTMから入れる

    外部のサイズ提案ツールを入れるとき、GTMから3種類のタグを配信します。何を送るか、複数商品を買われたときの書き方、そしてUA時代のeec.purchaseだけでは足りない項目を整理します。

    GA4移行後にCVが計上されなくなるGTMタグの直し方

    GA4移行後にCVが計上されなくなるGTMタグの直し方

    タグは発火しているのにコンバージョンが記録されません。UA時代に書かれたカスタムHTMLタグが静かに失敗する仕組みと、修正前・修正後のコードを並べて直し方を整理します。