【GTM – dataLayer深掘りシリーズ】第3回:gtagコマンドと送信分岐

dataLayer深掘りシリーズ(全4回)
第1回 / 第2回 / 第3回 / 第4回
本稿は第3回です。
これまで見てきたように、 gtag('event', 'purchase', {...}) と gtag('set', 'currency', 'JPY') は、どちらも最終的には dataLayer.push(arguments) を呼びます。
それでも、前者は条件を満たせば送信処理へ進み、後者はその呼び出しでは送信しません。違いを決めるのが、arguments の先にあるコマンド表です。
第2回では、dataLayer.push() が引数の型によって4つの経路に分かれ、arguments の経路だけがコマンド表へつながるところまで確認しました。今回はその先へ進み、event / config / set / get / js / consent が、送信・設定・読み出しにどう分かれるのかを追います。さらに、送信処理へ進んだリクエストの宛先と送信方法まで確認します。
目次
1. コマンド名で「送信・設定・読み出し」に分かれる
arguments を渡す形だけが、コマンド表を参照する分岐に入ります。ここから先は、第1引数に書いたコマンドによって行き先が分かれます。
公式ドキュメントから、各コマンドの役割は分かります。ただし、送信処理へ入ることと、実際に /g/collect が出ることは別です。この章では、その境目を実装と検証結果から整理します。
1-1. 早見表:送信処理へ進むコマンド、進まないコマンド
先に結論を書きます。
ここでいうconfig スコープは、宛先IDごとに保持され、後続イベントから参照される設定値のまとまりです。
1-2. 設定や初期化を行うコマンド:set / consent / js
gtag('set') は、後続の config / event から参照される値を保存するコマンドで、この呼び出し自体は計測リクエストを送りません。
set で入れた値の置き場所も押さえておきます。gtag('set', 'test', 1) の値は、イベントごとのまとまり(eventModel)の中ではなく、モデルのルート直下に入ります。
だから、GTM側ではデータレイヤー変数の名前を test と指定するだけで取れます。gtag('event', 'x', {test: 1}) の第3引数で渡した場合と置き場所が違う、という点に注意しましょう。


gtag('set', 'test', 1)

gtag('event', 'x', {test: 1})consent は同意状態を更新するコマンドです。同意の更新をきっかけに、user_engagement などの計測リクエストが出る場合もあります。更新後の状態は、その後のイベント送信にも影響します。
js は「日付を渡すだけ」と思われるかもしれませんが、そうではありません。
ハンドラが返した初期化イベントがモデルへ書き込まれ、最初の js ではトリガー評価まで走ります。
GTMプレビュー上では、これが「コンテナの読み込み」として現れます。ただし、js 自体は計測リクエストを送りません。
1-3. 送信処理に入りうるコマンド:config と event
event は、引数の形が整っていれば素直です。整っていなければ、エラーも警告も出さずに終わります。次のどれかに当てはまると何も起きません。
- イベント名を渡していない(引数が2個未満)
- イベント名が文字列でない
- 第3引数が素のオブジェクトでも
undefinedでもない(null・文字列・配列・Date・関数はここで弾かれる) - 引数が4個以上ある
- パラメータの
send_toが文字列でも配列でもない
形が整っていれば送信処理へ入ります。ただし、この時点ではまだ送信先が決まったとは限りません。
例えば、GTMのGoogleタグには、通常内部で noTargetGroup: true のフラグが付きます。この設定では、そのGoogleタグの宛先が default グループに登録されません。
その状態でページから直書きで gtag('event', 'test_a') とだけ呼ぶと、GTMプレビューにはイベントが現れても、GA4の宛先を解決できないため計測リクエストは出ません。別途 send_to を指定する必要があります。
(といっても、GoogleタグだけGTMで設定して、イベントを gtag で直書きするというケースはほとんどないと思いますが)
config はさらに条件が付きます。
以下、update: true パラメータのないものを「通常の config」と呼びます。
ここには、上流の「通常の config を重複として落とすかを決めるゲート」と、下流の「受理した config を設定と更新のどちらとして扱うかを決める処理」があります。
上流:config が来る
│
├─ update: true
│ → 上流の重複判定は対象外
│ → 下流へ進む
│
└─ updateなし(通常のconfig)
├─ 重複無視ONで、2回目以降の場合 → 上流で丸ごと捨てる
└─ それ以外 → 下流へ進む
下流:受理したconfigを処理
├─ 通常のconfig
│ → その宛先のconfigスコープを今回の設定で作り直す
│ → configの送信処理を呼ぶ
│
└─ update: true
├─ すでに構成済み → 既存設定へ差分マージ。送信処理は呼ばない
└─ まだ未構成 → 初回のconfigとして送信処理を呼ぶ要するに、上流が判断するのは「この命令を入口で捨てるか」だけです。パラメータをマージするのか、page_view の候補にするのか、などは判断しません。
下流が判断するのは、入口を通った命令を「置き換え」と「更新」のどちらで処理するかです。
このため、「すでに一度 config 済み」という事実だけでは、通常の config は止まりません。
重複無視設定がONの際に2回目が止まるのは、上流で命令ごと捨てられるからです。
重複無視設定がOFFなら上流を通るため、下流で config スコープを作り直し、送信処理にも再び入ります(通常 config の場合)。
管理画面の「ページ上の設定の重複インスタンスを無視します」をON/OFFして確認する
Googleタグの設定画面に「ページ上の設定の重複インスタンスを無視します」という項目があります。

現在はプロパティを作成した際に、デフォルトでオンになりますが、かなり古いGA4プロパティの場合はオフになっている可能性があります。
ここでは、同一ページ・同一測定IDに対する2本のGTM Googleタグで検証します。
今回GTMのGoogleタグで実証していますが、直書きの gtag('config') も同様です。


同じ測定IDへ、1本目のGoogleタグで param1: value1、2本目で param2: value2 を設定し、その後に begin_checkout を送ってみましょう。両方とも send_page_view: true(既定値)の条件です。結果は以下です。
「重複を無視」をオンにした場合、page_view は1回送信され、後続のイベントでも param1 を引き継いでいました。
↓page_view ↓

↓後続の begin_checkout ↓

「重複を無視」がオフの場合は、page_view が2回送信され、宛先のconfigスコープは param2 だけを持つ状態に置き換わりました。後続イベントも、このconfigスコープから param2 を引き継いでいます。
↓page_view 1回目 ↓

↓page_view 2回目:パラメータが丸ごと上書き↓

↓後続の begin_checkout :上書きされたパラメータを引き継ぐ↓

ONの場合、2本目は送信だけが止まるのではありません。上流で弾かれるため、param2 自体も反映されません。
OFFの場合、通常の config が前の値へ単純にマージされるわけではありません。
対象コードは、update: true がなければ、その宛先のconfigスコープを空にしてから今回の値を書きます。
そのため、param1 の後に param2 を通常の config で渡した最終状態は、{ param1, param2 } ではなく、param2 のみになります。
update: true は「置き換え」ではなく差分更新
上の ON/OFF 比較は、update のない通常の config の話でした。update: true があると上流の重複判定の対象外で、下流へ進み、既存の config スコープへ差分をマージします。
同じキーなら上書き、別のキーなら追加です。その更新自体では page_view を再度送りません。
この使い方は、Googleの英語版 config リファレンスでも、SPAなどで既存設定を更新する方法として案内されています。記事執筆時点では、日本語版に update の項目が反映されていないため、リンク先は英語版を指定しています。
一方、まだ config されていない宛先へ最初から update: true を渡すと、対象コードでは初回の config として送信処理が呼ばれます。ただし、update: true は初期化の代用品ではなく、通常の config で構成済みの宛先を更新するために使うのが安全です。
例えば、2本目の config(Googleタグ)で update: true を設定すると、後続のイベントでは param1 と param2 の両方が送られます。
↓2本目の Googleタグに update: true のパラメータを付与↓

2回目の page_view は送られない。パラメータのみマージする。
↓後続の begin_checkout :マージされたパラメータが確認できる↓

1-4. getは値を取り出すための窓口
gtag('get', 'G-XXXXXXX', 'client_id', callback) は、Googleタグが持つ値をコールバックで読み出すコマンドです。
内部では送信系と同じ振り分け関数を経由します。
ここだけを見ると送信されそうですが、直接呼び出した場合はその先で中断されます。
gtag('get', ...)
→ 振り分け関数(送信系と同じ入口)
→ イベント名が "gtag.get" かつ GTM コンテナ経由でない、と判定される
→ 内部状態から値を取得する
→ コールバックを setTimeout(..., 0) で別タスクとして実行する
→ 中断フラグを立てる
→ 送信直前のチェックで中断フラグを見て、送信関数に到達せず終了コールバックは非同期ですが、ネットワークの往復を待っているわけではありません。
タグのスクリプトが内部状態から値を取り出し、別タスクでコールバックへ渡しています。
↓例えば、GA4 の client_id を取得する目的などで使っている方も多いと思います。↓

1-5. 存在しないコマンド名は、黙って消える
gtag('evnet', 'purchase'); // タイプミス
// → エラーも警告も出ない。コンソールに何も出ない。何も起きない。未知のコマンド名は、arguments 枝の処理中に捨てられます。event や config など、知っているコマンドのどれにも当てはまらないため、その先の処理へ進みません。ただし、呼び出した内容自体は window.dataLayer 配列に残ります。
2. 送信処理の先で、どこへ・どう送るのか
2-1. 主計測リクエストはどこへ行くのか
ここで整理するのは、GA4の主計測リクエストの送信先です。同じイベントをきっかけに、同意や広告連携などの副次的なリクエストが別の宛先へ出る場合もあります。
主計測リクエストの送信先URLは、次の条件を上から順に評価して決まります。
詳しくは以下の記事も参考にしてください。
1. Merchant Center 用の設定が有効
→ GTGが無効なら https://www.merchant-center-analytics.goog/mc/collect
→GTGを有効と判定している場合は、GTGのベースURL配下の /mc/collect
2. server_container_url または transport_url が設定されている ★sGTM
→ <設定された URL>/g/collect
3. 上のどちらでもない
→ 特定の条件(Google シグナル関連の設定と、広告用・解析用ストレージの
同意状態)が揃っていれば analytics.google.com/g/collect
揃っていなければ www.google-analytics.com/g/collect
(Googleタグゲートウェイ(GTG)を
設定している場合は、自社ドメイン配下のパスに置き換わる)この順序には意味があります。GTGを設定していたとしても、sGTMの設定が見つかった時点でGTGのURLは使われません。なお、上記はイベント送信のリクエストURLであり、gtag/js の読み込みURLは別です。
2-2. どうやって送っているのか
通常は fetch で送信します。fetch が使えない、または呼び出し時に同期例外を投げるなどして送信を開始できなかった場合は、同じ送信処理内でフォールバックへ進みます。その先は、リクエストボディの有無で分かれます。
- ボディあり:
sendBeacon(url, body)を使う。この経路では、失敗しても画像GETへは進まない - ボディなし:
sendBeacon(url)を試し、送れなければ1×1ピクセルのImageによるGETへ進む
3. 第3回のまとめ
gtag() 自体は送信関数ではなく、arguments を dataLayer へ積む入口です。その先で第1引数のコマンド名が解釈され、送信・設定・読み出しへ分かれます。
大事なのは、window.dataLayer に値が残ること、内部のモデルや設定へ書かれること、送信処理へ入ること、実際のネットワークリクエストが出ることは、それぞれ別だという点です。
さて、第1回からここまで、「木側」「辞書側」という言葉が何度も登場しました。
ただ、まずは dataLayer.push() とコマンドがどう流れるかを先に追うため、2つの保存場所そのものの説明は意図的に後回しにしてきました。
次回、第4回ではいよいよ、この「木側」と「辞書側」の中身に踏み込みます。同じ値がどちらに保存され、どちらから読まれるのか。書き方によって更新結果が変わる理由や、データレイヤー変数のバージョン1・2との関係まで、ここまで点で登場してきた話をまとめて回収します。
それでは、Bye, bye.





