<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Lab on Tarragon</title><link>https://tarrragon.github.io/blog/tags/lab/</link><description>Recent content in Lab on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 22 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/lab/index.xml" rel="self" type="application/rss+xml"/><item><title>Meta Pixel 實測頁：在瀏覽器裡觀察 fbq 呼叫、送出的請求與寫下的 cookie</title><link>https://tarrragon.github.io/blog/devops/monitoring/vendors/meta-pixel-lab/</link><pubDate>Thu, 22 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/monitoring/vendors/meta-pixel-lab/</guid><description>&lt;p>這一頁是 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/vendors/meta-pixel-conversions-api/" data-link-title="Meta Pixel 與 Conversions API：廣告平台轉換追蹤的瀏覽器端與伺服器端" data-link-desc="Meta Pixel 在頁面上送出的標準事件與它的資料消費者、瀏覽器端事件被攔截的路徑、Conversions API 從伺服器補送同一批事件的必填欄位，以及兩路並送時以 event_id 去重與用 Test Events 驗證的方式">Meta Pixel 與 Conversions API&lt;/a> 的實測頁，範圍是瀏覽器端的 Pixel：載入 &lt;code>fbevents.js&lt;/code>、用 &lt;code>fbq()&lt;/code> 送事件，以及 Pixel 寫在網站網域上的 cookie。Conversions API 是伺服器端的路徑，在瀏覽器裡看不到，不在這一頁。&lt;/p>
&lt;h2 id="這一頁載入什麼什麼時候載入">這一頁載入什麼、什麼時候載入&lt;/h2>
&lt;p>頁面打開時不載入任何 Meta 的 script。按下「載入 Meta Pixel 並送 PageView」之後，面板才把 &lt;code>fbevents.js&lt;/code> 從 &lt;code>connect.facebook.net&lt;/code> 注入頁面，並用填在欄位裡的 Pixel ID 初始化；在那之前這一頁沒有任何送往 Meta 的請求，請求清單可以直接驗證這一點。注入的 script 在重新整理或離開這一頁時就消失，而 Pixel 寫下的 cookie 會留在這個網域上，直到按「清除這一頁的追蹤 cookie」或瀏覽器自己刪除。&lt;/p>
&lt;p>Pixel ID 欄位填的是 Meta Events Manager 裡某個 Pixel 的 ID。欄位預設空白，填自己的測試 Pixel ID：在事件管理工具的資料集頁面讀得到，商家設定頁網址裡的 &lt;code>selected_asset_id&lt;/code> 是另一個編號，填錯時頁面不會報錯（見 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/vendors/tracking-request-capture/" data-link-title="用 Playwright 擷取追蹤碼請求：擷取方式、記錄欄位與各次結果" data-link-desc="以 Playwright 操作 Meta Pixel 與 GTM 實測頁、記錄各家 script 實際送出的請求與 cookie 的方式，結果檔記錄的欄位與它們的用途，擷取用的測試帳號，以及每一次擷取觀察到的事件請求形式、cookie 與請求參數的對應、無頭瀏覽器下的差異與 script 自動送出的事件">用 Playwright 擷取追蹤碼請求&lt;/a> 的指令一節）。用自己的 Pixel 實測時，事件會出現在那個 Pixel 的 Test Events 頁面，可以對照 Meta 那一端實際收到了什麼。&lt;/p>
&lt;div class="vendor-lab" data-vendor="meta-pixel">
 &lt;style>
 .vendor-lab { border: 1px solid currentColor; border-radius: 6px; padding: 1rem; margin: 1.5rem 0; }
 .vendor-lab fieldset { border: 0; padding: 0; margin: 0 0 .75rem; }
 .vendor-lab label { display: block; margin-bottom: .25rem; }
 .vendor-lab input { font: inherit; padding: .25rem .5rem; min-width: 14rem; }
 .vendor-lab button { font: inherit; margin: .25rem .5rem .25rem 0; padding: .25rem .75rem; cursor: pointer; }
 .vendor-lab button[disabled] { cursor: not-allowed; opacity: .5; }
 .vendor-lab h4 { margin: 1rem 0 .25rem; }
 .vendor-lab ol { padding-left: 1.25rem; margin: 0; font-size: .875rem; overflow-wrap: anywhere; }
 .vendor-lab pre { font-size: .8rem; max-height: 20rem; overflow: auto; }
 .vendor-lab .vendor-lab-status { font-weight: bold; }
 &lt;/style>
 &lt;fieldset>
 &lt;label for="vendor-lab-id">Meta Pixel ID（Events Manager 裡那個 Pixel 的一串數字）&lt;/label>
 &lt;input id="vendor-lab-id" inputmode="numeric" autocomplete="off" value="" placeholder="例如 123456789012345">
 &lt;/fieldset>
 &lt;div>
 &lt;button type="button" data-action="load">載入 Meta Pixel 並送 PageView&lt;/button>
 &lt;button type="button" data-action="view-content" disabled>送 ViewContent&lt;/button>
 &lt;button type="button" data-action="purchase" disabled>送 Purchase（帶 eventID）&lt;/button>
 &lt;button type="button" data-action="fbclid">帶 fbclid 參數重新開啟這一頁&lt;/button>
 &lt;/div>
 &lt;div>
 &lt;button type="button" data-action="clear-cookies">清除這一頁的追蹤 cookie&lt;/button>
 &lt;/div>

 &lt;p class="vendor-lab-status" role="status" aria-live="polite">尚未載入任何第三方 script。&lt;/p></description><content:encoded><![CDATA[<p>這一頁是 <a href="/blog/devops/monitoring/vendors/meta-pixel-conversions-api/" data-link-title="Meta Pixel 與 Conversions API：廣告平台轉換追蹤的瀏覽器端與伺服器端" data-link-desc="Meta Pixel 在頁面上送出的標準事件與它的資料消費者、瀏覽器端事件被攔截的路徑、Conversions API 從伺服器補送同一批事件的必填欄位，以及兩路並送時以 event_id 去重與用 Test Events 驗證的方式">Meta Pixel 與 Conversions API</a> 的實測頁，範圍是瀏覽器端的 Pixel：載入 <code>fbevents.js</code>、用 <code>fbq()</code> 送事件，以及 Pixel 寫在網站網域上的 cookie。Conversions API 是伺服器端的路徑，在瀏覽器裡看不到，不在這一頁。</p>
<h2 id="這一頁載入什麼什麼時候載入">這一頁載入什麼、什麼時候載入</h2>
<p>頁面打開時不載入任何 Meta 的 script。按下「載入 Meta Pixel 並送 PageView」之後，面板才把 <code>fbevents.js</code> 從 <code>connect.facebook.net</code> 注入頁面，並用填在欄位裡的 Pixel ID 初始化；在那之前這一頁沒有任何送往 Meta 的請求，請求清單可以直接驗證這一點。注入的 script 在重新整理或離開這一頁時就消失，而 Pixel 寫下的 cookie 會留在這個網域上，直到按「清除這一頁的追蹤 cookie」或瀏覽器自己刪除。</p>
<p>Pixel ID 欄位填的是 Meta Events Manager 裡某個 Pixel 的 ID。欄位預設空白，填自己的測試 Pixel ID：在事件管理工具的資料集頁面讀得到，商家設定頁網址裡的 <code>selected_asset_id</code> 是另一個編號，填錯時頁面不會報錯（見 <a href="/blog/devops/monitoring/vendors/tracking-request-capture/" data-link-title="用 Playwright 擷取追蹤碼請求：擷取方式、記錄欄位與各次結果" data-link-desc="以 Playwright 操作 Meta Pixel 與 GTM 實測頁、記錄各家 script 實際送出的請求與 cookie 的方式，結果檔記錄的欄位與它們的用途，擷取用的測試帳號，以及每一次擷取觀察到的事件請求形式、cookie 與請求參數的對應、無頭瀏覽器下的差異與 script 自動送出的事件">用 Playwright 擷取追蹤碼請求</a> 的指令一節）。用自己的 Pixel 實測時，事件會出現在那個 Pixel 的 Test Events 頁面，可以對照 Meta 那一端實際收到了什麼。</p>
<div class="vendor-lab" data-vendor="meta-pixel">
  <style>
    .vendor-lab { border: 1px solid currentColor; border-radius: 6px; padding: 1rem; margin: 1.5rem 0; }
    .vendor-lab fieldset { border: 0; padding: 0; margin: 0 0 .75rem; }
    .vendor-lab label { display: block; margin-bottom: .25rem; }
    .vendor-lab input { font: inherit; padding: .25rem .5rem; min-width: 14rem; }
    .vendor-lab button { font: inherit; margin: .25rem .5rem .25rem 0; padding: .25rem .75rem; cursor: pointer; }
    .vendor-lab button[disabled] { cursor: not-allowed; opacity: .5; }
    .vendor-lab h4 { margin: 1rem 0 .25rem; }
    .vendor-lab ol { padding-left: 1.25rem; margin: 0; font-size: .875rem; overflow-wrap: anywhere; }
    .vendor-lab pre { font-size: .8rem; max-height: 20rem; overflow: auto; }
    .vendor-lab .vendor-lab-status { font-weight: bold; }
  </style>
  <fieldset>
    <label for="vendor-lab-id">Meta Pixel ID（Events Manager 裡那個 Pixel 的一串數字）</label>
    <input id="vendor-lab-id" inputmode="numeric" autocomplete="off" value="" placeholder="例如 123456789012345">
  </fieldset>
  <div>
    <button type="button" data-action="load">載入 Meta Pixel 並送 PageView</button>
    <button type="button" data-action="view-content" disabled>送 ViewContent</button>
    <button type="button" data-action="purchase" disabled>送 Purchase（帶 eventID）</button>
    <button type="button" data-action="fbclid">帶 fbclid 參數重新開啟這一頁</button>
  </div>
  <div>
    <button type="button" data-action="clear-cookies">清除這一頁的追蹤 cookie</button>
  </div>

  <p class="vendor-lab-status" role="status" aria-live="polite">尚未載入任何第三方 script。</p>

  <h4>這一頁的操作紀錄</h4>
  <ol class="vendor-lab-calls"></ol>

  <h4>這一頁送出的跨網域請求</h4>
  <ol class="vendor-lab-requests"></ol>

  <h4>這個網域上的追蹤 cookie</h4>
  <ol class="vendor-lab-cookies"></ol>
</div>
<script>
  (function () {
    var root = document.currentScript.previousElementSibling;
    var vendor = root.getAttribute("data-vendor");
    var idInput = root.querySelector("#vendor-lab-id");
    var statusEl = root.querySelector(".vendor-lab-status");
    var callsEl = root.querySelector(".vendor-lab-calls");
    var requestsEl = root.querySelector(".vendor-lab-requests");
    var cookiesEl = root.querySelector(".vendor-lab-cookies");
    var dataLayerEl = root.querySelector(".vendor-lab-datalayer");
    var startedAt = performance.now();

    
    var COOKIE_PREFIXES = ["_fbp", "_fbc", "_ga", "_gid", "_gcl"];
    var ID_PATTERN = { "meta-pixel": /^\d{5,20}$/, "gtm": /^GTM-[A-Z0-9]{4,12}$/ };

    function item(list, text) {
      var li = document.createElement("li");
      li.textContent = text;   
      list.appendChild(li);
    }
    function seconds() { return ((performance.now() - startedAt) / 1000).toFixed(1) + "s"; }
    function logCall(text) { item(callsEl, seconds() + "  " + text); }
    function setStatus(text) { statusEl.textContent = text; }

    
    
    function showRequest(entry) {
      var url;
      try { url = new URL(entry.name); } catch (e) { return; }
      if (url.origin === location.origin) return;
      var params = [];
      url.searchParams.forEach(function (v, k) { params.push(k + "=" + v); });
      item(requestsEl, (entry.startTime / 1000).toFixed(1) + "s  [" + entry.initiatorType + "] "
        + url.origin + url.pathname + (params.length ? "\n    " + params.join("\n    ") : ""));
    }
    try {
      new PerformanceObserver(function (list) { list.getEntries().forEach(showRequest); })
        .observe({ type: "resource", buffered: true });
    } catch (e) {
      item(requestsEl, "這個瀏覽器不支援 PerformanceObserver，請改用開發者工具的 Network 面板觀察。");
    }

    function refreshCookies() {
      cookiesEl.textContent = "";
      var found = document.cookie.split(/;\s*/).filter(function (c) {
        return COOKIE_PREFIXES.some(function (p) { return c.indexOf(p) === 0; });
      });
      if (!found.length) item(cookiesEl, "（沒有）");
      found.forEach(function (c) { item(cookiesEl, c); });
    }
    function refreshDataLayer() {
      if (!dataLayerEl) return;
      if (!window.dataLayer) { dataLayerEl.textContent = "（尚未建立）"; return; }
      try { dataLayerEl.textContent = JSON.stringify(window.dataLayer, null, 2); }
      catch (e) { dataLayerEl.textContent = "（內容含無法序列化的值）"; }
    }
    function refreshAll() { refreshCookies(); refreshDataLayer(); }

    
    function clearCookies() {
      var expired = "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
      document.cookie.split(/;\s*/).forEach(function (c) {
        var name = c.split("=")[0];
        if (!COOKIE_PREFIXES.some(function (p) { return name.indexOf(p) === 0; })) return;
        document.cookie = name + expired;
        document.cookie = name + expired + "; domain=" + location.hostname;
        document.cookie = name + expired + "; domain=." + location.hostname;
      });
      logCall("清除追蹤 cookie");
      refreshAll();
    }

    function readId() {
      var id = idInput.value.trim();
      if (!ID_PATTERN[vendor].test(id)) {
        setStatus("ID 格式不符，沒有載入任何 script。");
        return null;
      }
      return id;
    }

    
    function injectScript(src, label) {
      var s = document.createElement("script");
      s.async = true;
      s.src = src;
      s.onload = function () { setStatus(label + " 已載入。"); logCall(label + " 載入完成"); refreshAll(); };
      s.onerror = function () { setStatus(label + " 載入失敗：請求被擋下（廣告攔截器、瀏覽器追蹤防護或網路政策）。"); logCall(label + " 載入失敗"); };
      document.head.appendChild(s);
    }

    var actions = {};
    var loaded = false;

    if (vendor === "meta-pixel") {
      var enable = function () {
        root.querySelectorAll('[data-action="view-content"],[data-action="purchase"]').forEach(function (b) { b.disabled = false; });
      };
      actions.load = function () {
        if (loaded) return;
        var id = readId(); if (!id) return;
        loaded = true;
        
        if (!window.fbq) {
          var n = window.fbq = function () { n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments); };
          if (!window._fbq) window._fbq = n;
          n.push = n; n.loaded = true; n.version = "2.0"; n.queue = [];
        }
        injectScript("https://connect.facebook.net/en_US/fbevents.js", "fbevents.js");
        fbq("init", id); logCall("fbq('init', '" + id + "')");
        fbq("track", "PageView"); logCall("fbq('track', 'PageView')");
        enable();
      };
      actions["view-content"] = function () {
        fbq("track", "ViewContent", { content_name: "vendor-lab" });
        logCall("fbq('track', 'ViewContent', {content_name: 'vendor-lab'})");
        refreshAll();
      };
      actions.purchase = function () {
        var eventId = "lab-" + Date.now().toString(36);
        fbq("track", "Purchase", { value: 1, currency: "TWD" }, { eventID: eventId });
        logCall("fbq('track', 'Purchase', {value: 1, currency: 'TWD'}, {eventID: '" + eventId + "'})");
        refreshAll();
      };
      actions.fbclid = function () {
        var url = new URL(location.href);
        url.searchParams.set("fbclid", "lab" + Date.now().toString(36));
        location.href = url.toString();
      };
    }

    if (vendor === "gtm") {
      actions.load = function () {
        if (loaded) return;
        var id = readId(); if (!id) return;
        loaded = true;
        
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
        logCall("dataLayer.push({'gtm.start': …, event: 'gtm.js'})");
        injectScript("https://www.googletagmanager.com/gtm.js?id=" + encodeURIComponent(id), "gtm.js");
        refreshAll();
      };
      actions["push-purchase"] = function () {
        window.dataLayer = window.dataLayer || [];
        var orderId = "lab-" + Date.now().toString(36);
        window.dataLayer.push({ event: "purchase", order_id: orderId, value: 1, currency: "TWD" });
        logCall("dataLayer.push({event: 'purchase', order_id: '" + orderId + "', value: 1, currency: 'TWD'})");
        refreshAll();
      };
    }

    actions["clear-cookies"] = clearCookies;

    root.addEventListener("click", function (evt) {
      var btn = evt.target.closest("button[data-action]");
      if (!btn || !actions[btn.getAttribute("data-action")]) return;
      actions[btn.getAttribute("data-action")]();
      
      setTimeout(refreshAll, 1500);
    });

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

<h2 id="對照面板讀出各個呼叫的效果">對照面板讀出各個呼叫的效果</h2>
<p>面板分成三塊。「操作紀錄」列出按鈕實際執行的 <code>fbq()</code> 呼叫；「這一頁送出的跨網域請求」來自瀏覽器的 Resource Timing 紀錄，列出這一頁向其他網域發出的每一個請求與它的查詢參數；「追蹤 cookie」列出這個網域上以 <code>_fbp</code>、<code>_fbc</code> 等前綴開頭的 cookie（這些是寫在網站網域上的<a href="/blog/devops/monitoring/knowledge-cards/first-party-cookie/" data-link-title="First-party Cookie（第一方 cookie）" data-link-desc="盤點網站寫下的識別碼 cookie、比對追蹤碼的 cookie 期限，或判斷某個 cookie 受不受瀏覽器的第三方 cookie 限制時">第一方 cookie</a>）。操作紀錄、請求與 cookie 並排讀，就能把一個呼叫對到它造成的網路請求與狀態變化。</p>
<ul>
<li><strong>載入 Pixel 之後</strong>：請求清單出現 <code>fbevents.js</code> 與 <code>signals/config/&lt;Pixel ID&gt;</code>（這個 Pixel 的設定檔）兩個 script 請求，cookie 清單在幾秒內出現 <code>_fbp</code>。PageView 等事件請求不會出現在這個清單裡：<code>fbevents.js</code> 以隱藏的表單把事件以 POST 提交到一個 iframe，這類提交不進這一頁的 Resource Timing。要看事件請求，打開瀏覽器開發者工具的 Network 面板，篩選 <code>facebook.com/tr</code>；表單 body 裡的 <code>ev</code> 是事件名稱，其餘參數的名稱與意義由 Meta 決定、會隨版本調整，實測的參數對應見 <a href="/blog/devops/monitoring/vendors/tracking-request-capture/" data-link-title="用 Playwright 擷取追蹤碼請求：擷取方式、記錄欄位與各次結果" data-link-desc="以 Playwright 操作 Meta Pixel 與 GTM 實測頁、記錄各家 script 實際送出的請求與 cookie 的方式，結果檔記錄的欄位與它們的用途，擷取用的測試帳號，以及每一次擷取觀察到的事件請求形式、cookie 與請求參數的對應、無頭瀏覽器下的差異與 script 自動送出的事件">用 Playwright 擷取追蹤碼請求</a> 的各次結果。</li>
<li><strong>送 Purchase</strong>：操作紀錄裡的 <code>eventID</code> 是這次按鈕產生的值，在 Network 面板那一筆 <code>/tr/</code> 請求的表單 body 裡是 <code>eid</code> 參數，這就是 Conversions API 去重時要對上的那一個。<code>value</code> 與 <code>currency</code> 在同一筆的 <code>cd[value]</code> 與 <code>cd[currency]</code>。同一時間還會多一筆<a href="/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">自動事件</a> <code>SubscribedButtonClick</code>，那是 Pixel 依按鈕點擊自動送出的事件，內容包含按鈕文字。</li>
<li><strong>帶 fbclid 參數重新開啟這一頁</strong>：按鈕把一個測試用的 <code>fbclid</code> 加進網址再重新載入。重新載入後再按一次「載入 Meta Pixel」，cookie 清單會多出 <code>_fbc</code>，值裡包含剛才的 <code>fbclid</code>。這是讀者從廣告點進網站時 Pixel 保存點擊來源的方式。</li>
<li><strong>載入失敗</strong>：狀態列顯示「載入失敗」時，代表 <code>fbevents.js</code> 的請求被擋下，常見來源是廣告攔截器、瀏覽器的追蹤防護或公司網路的政策。這時按其他按鈕，<code>fbq()</code> 只會把呼叫排進佇列，請求清單裡不會出現任何送往 Meta 的請求。這正是瀏覽器端事件損失的樣子：網站這一端沒有任何錯誤訊息。</li>
</ul>
<p>請求清單在頁面打開時就可能已經有幾筆紀錄，那些是本站每一頁都會載入的第三方資源（例如從 <code>cdn.jsdelivr.net</code> 載入的 Mermaid）與本站的流量 beacon（送往 <code>script.google.com</code>），與 Meta 無關。本站的 beacon 怎麼設計見 <a href="/blog/automation/02-analytics-beacon/" data-link-title="模組二：流量 beacon 實作" data-link-desc="把「頁面被看了就送一則事件、接收端寫進 Sheet」從零做到收到第一筆真實瀏覽紀錄時的完整實作">Automation 模組二：流量 beacon 實作</a>，拿來和 Pixel 的請求並排看，是自製追蹤與第三方追蹤碼在送出內容上的對照。</p>
<p>Resource Timing 只記錄網址、看不到 POST 請求的 body，而 Pixel 以表單提交到 iframe 的事件連網址都不會出現在這裡。要看完整的請求內容，打開瀏覽器開發者工具的 Network 面板，篩選 <code>facebook</code> 再點開那一筆。</p>
]]></content:encoded></item><item><title>Google Tag Manager 實測頁：在瀏覽器裡觀察容器載入、資料層事件與代碼送出的請求</title><link>https://tarrragon.github.io/blog/devops/monitoring/vendors/google-tag-manager-lab/</link><pubDate>Thu, 22 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/monitoring/vendors/google-tag-manager-lab/</guid><description>&lt;p>這一頁是 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/vendors/google-tag-manager/" data-link-title="Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理" data-link-desc="GTM 容器在頁面上承擔的部署責任、資料層作為前端與行銷之間的事件約定、預覽模式與版本發布的驗證方式、自訂 HTML 代碼帶來的安全與效能成本，以及伺服器端代碼管理改變的資料路徑">Google Tag Manager&lt;/a> 的實測頁，範圍是網頁容器：載入 &lt;code>gtm.js&lt;/code>、往資料層推事件，以及容器依設定載入的代碼。伺服器端代碼管理的伺服器容器在瀏覽器裡看不到，不在這一頁。&lt;/p>
&lt;h2 id="這一頁載入什麼什麼時候載入">這一頁載入什麼、什麼時候載入&lt;/h2>
&lt;p>頁面打開時不載入 GTM。按下「載入 GTM 容器」之後，面板才先往 &lt;code>window.dataLayer&lt;/code> 推一筆 &lt;code>gtm.js&lt;/code> 事件，再從 &lt;code>googletagmanager.com&lt;/code> 注入填在欄位裡的那個容器；在那之前這一頁沒有任何送往 Google 的請求。注入的容器在重新整理或離開這一頁時消失，容器裡的代碼寫下的 cookie 會留在這個網域上，直到按「清除這一頁的追蹤 cookie」。&lt;/p>
&lt;p>容器 ID 欄位填的是 GTM 後台裡某個容器的 ID（&lt;code>GTM-&lt;/code> 開頭）；欄位預設空白，填自己的測試容器 ID。容器裡有哪些代碼、它們在什麼觸發條件下執行，由那個容器目前發布的版本決定，所以同一個按鈕在不同容器上會送出完全不同的請求。用自己的測試容器實測時，在 GTM 後台開啟預覽模式再回到這一頁，就能在 Tag Assistant 裡看到每一筆資料層事件觸發了哪些代碼。&lt;/p>
&lt;div class="vendor-lab" data-vendor="gtm">
 &lt;style>
 .vendor-lab { border: 1px solid currentColor; border-radius: 6px; padding: 1rem; margin: 1.5rem 0; }
 .vendor-lab fieldset { border: 0; padding: 0; margin: 0 0 .75rem; }
 .vendor-lab label { display: block; margin-bottom: .25rem; }
 .vendor-lab input { font: inherit; padding: .25rem .5rem; min-width: 14rem; }
 .vendor-lab button { font: inherit; margin: .25rem .5rem .25rem 0; padding: .25rem .75rem; cursor: pointer; }
 .vendor-lab button[disabled] { cursor: not-allowed; opacity: .5; }
 .vendor-lab h4 { margin: 1rem 0 .25rem; }
 .vendor-lab ol { padding-left: 1.25rem; margin: 0; font-size: .875rem; overflow-wrap: anywhere; }
 .vendor-lab pre { font-size: .8rem; max-height: 20rem; overflow: auto; }
 .vendor-lab .vendor-lab-status { font-weight: bold; }
 &lt;/style>
 &lt;fieldset>
 &lt;label for="vendor-lab-id">GTM 容器 ID（GTM 後台右上角的 GTM- 開頭那一串）&lt;/label>
 &lt;input id="vendor-lab-id" autocomplete="off" value="" placeholder="例如 GTM-ABC1234">
 &lt;/fieldset>
 &lt;div>
 &lt;button type="button" data-action="load">載入 GTM 容器&lt;/button>
 &lt;button type="button" data-action="push-purchase">往資料層推 purchase 事件&lt;/button>
 &lt;/div>
 &lt;div>
 &lt;button type="button" data-action="clear-cookies">清除這一頁的追蹤 cookie&lt;/button>
 &lt;/div>

 &lt;p class="vendor-lab-status" role="status" aria-live="polite">尚未載入任何第三方 script。&lt;/p>

 &lt;h4>這一頁的操作紀錄&lt;/h4>
 &lt;ol class="vendor-lab-calls">&lt;/ol>

 &lt;h4>這一頁送出的跨網域請求&lt;/h4>
 &lt;ol class="vendor-lab-requests">&lt;/ol>

 &lt;h4>這個網域上的追蹤 cookie&lt;/h4>
 &lt;ol class="vendor-lab-cookies">&lt;/ol>
 &lt;h4>window.dataLayer 目前的內容&lt;/h4>
 &lt;pre class="vendor-lab-datalayer">（尚未建立）&lt;/pre>
&lt;/div>
&lt;script>
 (function () {
 var root = document.currentScript.previousElementSibling;
 var vendor = root.getAttribute("data-vendor");
 var idInput = root.querySelector("#vendor-lab-id");
 var statusEl = root.querySelector(".vendor-lab-status");
 var callsEl = root.querySelector(".vendor-lab-calls");
 var requestsEl = root.querySelector(".vendor-lab-requests");
 var cookiesEl = root.querySelector(".vendor-lab-cookies");
 var dataLayerEl = root.querySelector(".vendor-lab-datalayer");
 var startedAt = performance.now();

 
 var COOKIE_PREFIXES = ["_fbp", "_fbc", "_ga", "_gid", "_gcl"];
 var ID_PATTERN = { "meta-pixel": /^\d{5,20}$/, "gtm": /^GTM-[A-Z0-9]{4,12}$/ };

 function item(list, text) {
 var li = document.createElement("li");
 li.textContent = text; 
 list.appendChild(li);
 }
 function seconds() { return ((performance.now() - startedAt) / 1000).toFixed(1) + "s"; }
 function logCall(text) { item(callsEl, seconds() + " " + text); }
 function setStatus(text) { statusEl.textContent = text; }

 
 
 function showRequest(entry) {
 var url;
 try { url = new URL(entry.name); } catch (e) { return; }
 if (url.origin === location.origin) return;
 var params = [];
 url.searchParams.forEach(function (v, k) { params.push(k + "=" + v); });
 item(requestsEl, (entry.startTime / 1000).toFixed(1) + "s [" + entry.initiatorType + "] "
 + url.origin + url.pathname + (params.length ? "\n " + params.join("\n ") : ""));
 }
 try {
 new PerformanceObserver(function (list) { list.getEntries().forEach(showRequest); })
 .observe({ type: "resource", buffered: true });
 } catch (e) {
 item(requestsEl, "這個瀏覽器不支援 PerformanceObserver，請改用開發者工具的 Network 面板觀察。");
 }

 function refreshCookies() {
 cookiesEl.textContent = "";
 var found = document.cookie.split(/;\s*/).filter(function (c) {
 return COOKIE_PREFIXES.some(function (p) { return c.indexOf(p) === 0; });
 });
 if (!found.length) item(cookiesEl, "（沒有）");
 found.forEach(function (c) { item(cookiesEl, c); });
 }
 function refreshDataLayer() {
 if (!dataLayerEl) return;
 if (!window.dataLayer) { dataLayerEl.textContent = "（尚未建立）"; return; }
 try { dataLayerEl.textContent = JSON.stringify(window.dataLayer, null, 2); }
 catch (e) { dataLayerEl.textContent = "（內容含無法序列化的值）"; }
 }
 function refreshAll() { refreshCookies(); refreshDataLayer(); }

 
 function clearCookies() {
 var expired = "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
 document.cookie.split(/;\s*/).forEach(function (c) {
 var name = c.split("=")[0];
 if (!COOKIE_PREFIXES.some(function (p) { return name.indexOf(p) === 0; })) return;
 document.cookie = name + expired;
 document.cookie = name + expired + "; domain=" + location.hostname;
 document.cookie = name + expired + "; domain=." + location.hostname;
 });
 logCall("清除追蹤 cookie");
 refreshAll();
 }

 function readId() {
 var id = idInput.value.trim();
 if (!ID_PATTERN[vendor].test(id)) {
 setStatus("ID 格式不符，沒有載入任何 script。");
 return null;
 }
 return id;
 }

 
 function injectScript(src, label) {
 var s = document.createElement("script");
 s.async = true;
 s.src = src;
 s.onload = function () { setStatus(label + " 已載入。"); logCall(label + " 載入完成"); refreshAll(); };
 s.onerror = function () { setStatus(label + " 載入失敗：請求被擋下（廣告攔截器、瀏覽器追蹤防護或網路政策）。"); logCall(label + " 載入失敗"); };
 document.head.appendChild(s);
 }

 var actions = {};
 var loaded = false;

 if (vendor === "meta-pixel") {
 var enable = function () {
 root.querySelectorAll('[data-action="view-content"],[data-action="purchase"]').forEach(function (b) { b.disabled = false; });
 };
 actions.load = function () {
 if (loaded) return;
 var id = readId(); if (!id) return;
 loaded = true;
 
 if (!window.fbq) {
 var n = window.fbq = function () { n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments); };
 if (!window._fbq) window._fbq = n;
 n.push = n; n.loaded = true; n.version = "2.0"; n.queue = [];
 }
 injectScript("https://connect.facebook.net/en_US/fbevents.js", "fbevents.js");
 fbq("init", id); logCall("fbq('init', '" + id + "')");
 fbq("track", "PageView"); logCall("fbq('track', 'PageView')");
 enable();
 };
 actions["view-content"] = function () {
 fbq("track", "ViewContent", { content_name: "vendor-lab" });
 logCall("fbq('track', 'ViewContent', {content_name: 'vendor-lab'})");
 refreshAll();
 };
 actions.purchase = function () {
 var eventId = "lab-" + Date.now().toString(36);
 fbq("track", "Purchase", { value: 1, currency: "TWD" }, { eventID: eventId });
 logCall("fbq('track', 'Purchase', {value: 1, currency: 'TWD'}, {eventID: '" + eventId + "'})");
 refreshAll();
 };
 actions.fbclid = function () {
 var url = new URL(location.href);
 url.searchParams.set("fbclid", "lab" + Date.now().toString(36));
 location.href = url.toString();
 };
 }

 if (vendor === "gtm") {
 actions.load = function () {
 if (loaded) return;
 var id = readId(); if (!id) return;
 loaded = true;
 
 window.dataLayer = window.dataLayer || [];
 window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
 logCall("dataLayer.push({'gtm.start': …, event: 'gtm.js'})");
 injectScript("https://www.googletagmanager.com/gtm.js?id=" + encodeURIComponent(id), "gtm.js");
 refreshAll();
 };
 actions["push-purchase"] = function () {
 window.dataLayer = window.dataLayer || [];
 var orderId = "lab-" + Date.now().toString(36);
 window.dataLayer.push({ event: "purchase", order_id: orderId, value: 1, currency: "TWD" });
 logCall("dataLayer.push({event: 'purchase', order_id: '" + orderId + "', value: 1, currency: 'TWD'})");
 refreshAll();
 };
 }

 actions["clear-cookies"] = clearCookies;

 root.addEventListener("click", function (evt) {
 var btn = evt.target.closest("button[data-action]");
 if (!btn || !actions[btn.getAttribute("data-action")]) return;
 actions[btn.getAttribute("data-action")]();
 
 setTimeout(refreshAll, 1500);
 });

 refreshAll();
 })();
&lt;/script>

&lt;h2 id="對照面板讀出容器的行為">對照面板讀出容器的行為&lt;/h2>
&lt;p>面板分成四塊：「操作紀錄」列出按鈕實際執行的 &lt;code>dataLayer.push()&lt;/code>；「這一頁送出的跨網域請求」列出這一頁向其他網域發出的每一個請求；「追蹤 cookie」列出 &lt;code>_ga&lt;/code>、&lt;code>_gcl&lt;/code> 等前綴開頭的&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/first-party-cookie/" data-link-title="First-party Cookie（第一方 cookie）" data-link-desc="盤點網站寫下的識別碼 cookie、比對追蹤碼的 cookie 期限，或判斷某個 cookie 受不受瀏覽器的第三方 cookie 限制時">第一方 cookie&lt;/a>；「window.dataLayer 目前的內容」是資料層這個陣列的完整內容。&lt;/p></description><content:encoded><![CDATA[<p>這一頁是 <a href="/blog/devops/monitoring/vendors/google-tag-manager/" data-link-title="Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理" data-link-desc="GTM 容器在頁面上承擔的部署責任、資料層作為前端與行銷之間的事件約定、預覽模式與版本發布的驗證方式、自訂 HTML 代碼帶來的安全與效能成本，以及伺服器端代碼管理改變的資料路徑">Google Tag Manager</a> 的實測頁，範圍是網頁容器：載入 <code>gtm.js</code>、往資料層推事件，以及容器依設定載入的代碼。伺服器端代碼管理的伺服器容器在瀏覽器裡看不到，不在這一頁。</p>
<h2 id="這一頁載入什麼什麼時候載入">這一頁載入什麼、什麼時候載入</h2>
<p>頁面打開時不載入 GTM。按下「載入 GTM 容器」之後，面板才先往 <code>window.dataLayer</code> 推一筆 <code>gtm.js</code> 事件，再從 <code>googletagmanager.com</code> 注入填在欄位裡的那個容器；在那之前這一頁沒有任何送往 Google 的請求。注入的容器在重新整理或離開這一頁時消失，容器裡的代碼寫下的 cookie 會留在這個網域上，直到按「清除這一頁的追蹤 cookie」。</p>
<p>容器 ID 欄位填的是 GTM 後台裡某個容器的 ID（<code>GTM-</code> 開頭）；欄位預設空白，填自己的測試容器 ID。容器裡有哪些代碼、它們在什麼觸發條件下執行，由那個容器目前發布的版本決定，所以同一個按鈕在不同容器上會送出完全不同的請求。用自己的測試容器實測時，在 GTM 後台開啟預覽模式再回到這一頁，就能在 Tag Assistant 裡看到每一筆資料層事件觸發了哪些代碼。</p>
<div class="vendor-lab" data-vendor="gtm">
  <style>
    .vendor-lab { border: 1px solid currentColor; border-radius: 6px; padding: 1rem; margin: 1.5rem 0; }
    .vendor-lab fieldset { border: 0; padding: 0; margin: 0 0 .75rem; }
    .vendor-lab label { display: block; margin-bottom: .25rem; }
    .vendor-lab input { font: inherit; padding: .25rem .5rem; min-width: 14rem; }
    .vendor-lab button { font: inherit; margin: .25rem .5rem .25rem 0; padding: .25rem .75rem; cursor: pointer; }
    .vendor-lab button[disabled] { cursor: not-allowed; opacity: .5; }
    .vendor-lab h4 { margin: 1rem 0 .25rem; }
    .vendor-lab ol { padding-left: 1.25rem; margin: 0; font-size: .875rem; overflow-wrap: anywhere; }
    .vendor-lab pre { font-size: .8rem; max-height: 20rem; overflow: auto; }
    .vendor-lab .vendor-lab-status { font-weight: bold; }
  </style>
  <fieldset>
    <label for="vendor-lab-id">GTM 容器 ID（GTM 後台右上角的 GTM- 開頭那一串）</label>
    <input id="vendor-lab-id" autocomplete="off" value="" placeholder="例如 GTM-ABC1234">
  </fieldset>
  <div>
    <button type="button" data-action="load">載入 GTM 容器</button>
    <button type="button" data-action="push-purchase">往資料層推 purchase 事件</button>
  </div>
  <div>
    <button type="button" data-action="clear-cookies">清除這一頁的追蹤 cookie</button>
  </div>

  <p class="vendor-lab-status" role="status" aria-live="polite">尚未載入任何第三方 script。</p>

  <h4>這一頁的操作紀錄</h4>
  <ol class="vendor-lab-calls"></ol>

  <h4>這一頁送出的跨網域請求</h4>
  <ol class="vendor-lab-requests"></ol>

  <h4>這個網域上的追蹤 cookie</h4>
  <ol class="vendor-lab-cookies"></ol>
  <h4>window.dataLayer 目前的內容</h4>
  <pre class="vendor-lab-datalayer">（尚未建立）</pre>
</div>
<script>
  (function () {
    var root = document.currentScript.previousElementSibling;
    var vendor = root.getAttribute("data-vendor");
    var idInput = root.querySelector("#vendor-lab-id");
    var statusEl = root.querySelector(".vendor-lab-status");
    var callsEl = root.querySelector(".vendor-lab-calls");
    var requestsEl = root.querySelector(".vendor-lab-requests");
    var cookiesEl = root.querySelector(".vendor-lab-cookies");
    var dataLayerEl = root.querySelector(".vendor-lab-datalayer");
    var startedAt = performance.now();

    
    var COOKIE_PREFIXES = ["_fbp", "_fbc", "_ga", "_gid", "_gcl"];
    var ID_PATTERN = { "meta-pixel": /^\d{5,20}$/, "gtm": /^GTM-[A-Z0-9]{4,12}$/ };

    function item(list, text) {
      var li = document.createElement("li");
      li.textContent = text;   
      list.appendChild(li);
    }
    function seconds() { return ((performance.now() - startedAt) / 1000).toFixed(1) + "s"; }
    function logCall(text) { item(callsEl, seconds() + "  " + text); }
    function setStatus(text) { statusEl.textContent = text; }

    
    
    function showRequest(entry) {
      var url;
      try { url = new URL(entry.name); } catch (e) { return; }
      if (url.origin === location.origin) return;
      var params = [];
      url.searchParams.forEach(function (v, k) { params.push(k + "=" + v); });
      item(requestsEl, (entry.startTime / 1000).toFixed(1) + "s  [" + entry.initiatorType + "] "
        + url.origin + url.pathname + (params.length ? "\n    " + params.join("\n    ") : ""));
    }
    try {
      new PerformanceObserver(function (list) { list.getEntries().forEach(showRequest); })
        .observe({ type: "resource", buffered: true });
    } catch (e) {
      item(requestsEl, "這個瀏覽器不支援 PerformanceObserver，請改用開發者工具的 Network 面板觀察。");
    }

    function refreshCookies() {
      cookiesEl.textContent = "";
      var found = document.cookie.split(/;\s*/).filter(function (c) {
        return COOKIE_PREFIXES.some(function (p) { return c.indexOf(p) === 0; });
      });
      if (!found.length) item(cookiesEl, "（沒有）");
      found.forEach(function (c) { item(cookiesEl, c); });
    }
    function refreshDataLayer() {
      if (!dataLayerEl) return;
      if (!window.dataLayer) { dataLayerEl.textContent = "（尚未建立）"; return; }
      try { dataLayerEl.textContent = JSON.stringify(window.dataLayer, null, 2); }
      catch (e) { dataLayerEl.textContent = "（內容含無法序列化的值）"; }
    }
    function refreshAll() { refreshCookies(); refreshDataLayer(); }

    
    function clearCookies() {
      var expired = "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
      document.cookie.split(/;\s*/).forEach(function (c) {
        var name = c.split("=")[0];
        if (!COOKIE_PREFIXES.some(function (p) { return name.indexOf(p) === 0; })) return;
        document.cookie = name + expired;
        document.cookie = name + expired + "; domain=" + location.hostname;
        document.cookie = name + expired + "; domain=." + location.hostname;
      });
      logCall("清除追蹤 cookie");
      refreshAll();
    }

    function readId() {
      var id = idInput.value.trim();
      if (!ID_PATTERN[vendor].test(id)) {
        setStatus("ID 格式不符，沒有載入任何 script。");
        return null;
      }
      return id;
    }

    
    function injectScript(src, label) {
      var s = document.createElement("script");
      s.async = true;
      s.src = src;
      s.onload = function () { setStatus(label + " 已載入。"); logCall(label + " 載入完成"); refreshAll(); };
      s.onerror = function () { setStatus(label + " 載入失敗：請求被擋下（廣告攔截器、瀏覽器追蹤防護或網路政策）。"); logCall(label + " 載入失敗"); };
      document.head.appendChild(s);
    }

    var actions = {};
    var loaded = false;

    if (vendor === "meta-pixel") {
      var enable = function () {
        root.querySelectorAll('[data-action="view-content"],[data-action="purchase"]').forEach(function (b) { b.disabled = false; });
      };
      actions.load = function () {
        if (loaded) return;
        var id = readId(); if (!id) return;
        loaded = true;
        
        if (!window.fbq) {
          var n = window.fbq = function () { n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments); };
          if (!window._fbq) window._fbq = n;
          n.push = n; n.loaded = true; n.version = "2.0"; n.queue = [];
        }
        injectScript("https://connect.facebook.net/en_US/fbevents.js", "fbevents.js");
        fbq("init", id); logCall("fbq('init', '" + id + "')");
        fbq("track", "PageView"); logCall("fbq('track', 'PageView')");
        enable();
      };
      actions["view-content"] = function () {
        fbq("track", "ViewContent", { content_name: "vendor-lab" });
        logCall("fbq('track', 'ViewContent', {content_name: 'vendor-lab'})");
        refreshAll();
      };
      actions.purchase = function () {
        var eventId = "lab-" + Date.now().toString(36);
        fbq("track", "Purchase", { value: 1, currency: "TWD" }, { eventID: eventId });
        logCall("fbq('track', 'Purchase', {value: 1, currency: 'TWD'}, {eventID: '" + eventId + "'})");
        refreshAll();
      };
      actions.fbclid = function () {
        var url = new URL(location.href);
        url.searchParams.set("fbclid", "lab" + Date.now().toString(36));
        location.href = url.toString();
      };
    }

    if (vendor === "gtm") {
      actions.load = function () {
        if (loaded) return;
        var id = readId(); if (!id) return;
        loaded = true;
        
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
        logCall("dataLayer.push({'gtm.start': …, event: 'gtm.js'})");
        injectScript("https://www.googletagmanager.com/gtm.js?id=" + encodeURIComponent(id), "gtm.js");
        refreshAll();
      };
      actions["push-purchase"] = function () {
        window.dataLayer = window.dataLayer || [];
        var orderId = "lab-" + Date.now().toString(36);
        window.dataLayer.push({ event: "purchase", order_id: orderId, value: 1, currency: "TWD" });
        logCall("dataLayer.push({event: 'purchase', order_id: '" + orderId + "', value: 1, currency: 'TWD'})");
        refreshAll();
      };
    }

    actions["clear-cookies"] = clearCookies;

    root.addEventListener("click", function (evt) {
      var btn = evt.target.closest("button[data-action]");
      if (!btn || !actions[btn.getAttribute("data-action")]) return;
      actions[btn.getAttribute("data-action")]();
      
      setTimeout(refreshAll, 1500);
    });

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

<h2 id="對照面板讀出容器的行為">對照面板讀出容器的行為</h2>
<p>面板分成四塊：「操作紀錄」列出按鈕實際執行的 <code>dataLayer.push()</code>；「這一頁送出的跨網域請求」列出這一頁向其他網域發出的每一個請求；「追蹤 cookie」列出 <code>_ga</code>、<code>_gcl</code> 等前綴開頭的<a href="/blog/devops/monitoring/knowledge-cards/first-party-cookie/" data-link-title="First-party Cookie（第一方 cookie）" data-link-desc="盤點網站寫下的識別碼 cookie、比對追蹤碼的 cookie 期限，或判斷某個 cookie 受不受瀏覽器的第三方 cookie 限制時">第一方 cookie</a>；「window.dataLayer 目前的內容」是資料層這個陣列的完整內容。</p>
<ul>
<li><strong>載入容器之後</strong>：資料層除了按鈕推的 <code>gtm.js</code>，還會多出容器自己推入的事件（頁面的 DOM 就緒、視窗載入完成這類），它們是 GTM 內建觸發條件比對的對象。請求清單先出現 <code>gtm.js</code>，再出現容器裡各個代碼載入的 script 與送出的請求——容器設了 GA4 就會看到送往 Google Analytics 的請求，設了 Meta Pixel 就會看到 <code>fbevents.js</code>。</li>
<li><strong>往資料層推 purchase 事件</strong>：資料層多一筆帶 <code>order_id</code>、<code>value</code>、<code>currency</code> 的物件。容器裡有以 <code>purchase</code> 為名的自訂事件觸發條件時，以它為觸發條件的代碼會在這時執行並送出請求；沒有的話這一筆只會留在陣列裡，請求清單不會變。資料層是約定、容器決定要不要讀它，從這裡可以直接看到兩者的分工。</li>
<li>推了事件而容器沒有對應觸發條件時網路請求的實際樣子，擷取紀錄見 <a href="/blog/devops/monitoring/vendors/tracking-request-capture/" data-link-title="用 Playwright 擷取追蹤碼請求：擷取方式、記錄欄位與各次結果" data-link-desc="以 Playwright 操作 Meta Pixel 與 GTM 實測頁、記錄各家 script 實際送出的請求與 cookie 的方式，結果檔記錄的欄位與它們的用途，擷取用的測試帳號，以及每一次擷取觀察到的事件請求形式、cookie 與請求參數的對應、無頭瀏覽器下的差異與 script 自動送出的事件">用 Playwright 擷取追蹤碼請求</a> 的 GTM 那一則。</li>
<li><strong>沒有載入容器就推事件</strong>：先按「往資料層推 purchase 事件」再載入容器，容器載入後會從陣列開頭依序處理已經在裡面的事件，所以先推的那一筆仍然觸發得到代碼。網站在容器載入完成之前就推事件，靠的是這個行為。</li>
<li><strong>載入失敗</strong>：狀態列顯示「載入失敗」時，代表 <code>gtm.js</code> 的請求被擋下（廣告攔截器、瀏覽器追蹤防護或網路政策），容器裡的每一個代碼都不會執行。</li>
</ul>
<p>請求清單在頁面打開時就可能已經有幾筆紀錄，那些是本站每一頁都會載入的第三方資源（例如從 <code>cdn.jsdelivr.net</code> 載入的 Mermaid）與本站的流量 beacon（送往 <code>script.google.com</code>），與 GTM 無關。Resource Timing 只記錄網址、看不到 POST 請求的 body，要看完整內容改用瀏覽器開發者工具的 Network 面板。</p>
]]></content:encoded></item><item><title>用 Playwright 擷取追蹤碼請求：擷取方式、記錄欄位與各次結果</title><link>https://tarrragon.github.io/blog/devops/monitoring/vendors/tracking-request-capture/</link><pubDate>Thu, 22 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/monitoring/vendors/tracking-request-capture/</guid><description>&lt;p>這篇記錄用 Playwright 在本機操作 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/vendors/meta-pixel-lab/" data-link-title="Meta Pixel 實測頁：在瀏覽器裡觀察 fbq 呼叫、送出的請求與寫下的 cookie" data-link-desc="在這一頁按按鈕載入 Meta Pixel、送 PageView、ViewContent 與帶 eventID 的 Purchase，並用頁面上的面板對照每個呼叫送出的請求參數，以及 _fbp 與 fbclid 帶出的 _fbc cookie">Meta Pixel 實測頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/vendors/google-tag-manager-lab/" data-link-title="Google Tag Manager 實測頁：在瀏覽器裡觀察容器載入、資料層事件與代碼送出的請求" data-link-desc="在這一頁按按鈕載入 GTM 容器、往資料層推 purchase 事件，並用頁面上的面板對照 window.dataLayer 的內容、容器載入的代碼與它們送出的請求">Google Tag Manager 實測頁&lt;/a>、擷取各家追蹤碼實際送出內容的方式與結果。下文的「擷取腳本」指本站寫的 Playwright 程式 &lt;code>scripts/vendor-capture/capture.mjs&lt;/code>（在本站的公開 repo &lt;a href="https://github.com/tarrragon/blog">tarrragon/blog&lt;/a> 裡），「script」指服務商（vendor）提供、在頁面上執行的 JavaScript（&lt;code>fbevents.js&lt;/code>、&lt;code>gtm.js&lt;/code>）。各家 script 由服務商自己的伺服器提供、隨時改版，所以本篇的每一份結果都只代表擷取當下的行為，引用時要連同擷取時間與版本資訊一起看。&lt;/p>
&lt;h2 id="擷取方式讓瀏覽器自動化操作實測頁">擷取方式：讓瀏覽器自動化操作實測頁&lt;/h2>
&lt;p>擷取腳本不直接把服務商的 script 載入一個空白頁，而是開啟實測頁、照讀者的操作順序按按鈕。這樣擷取到的是和讀者在實測頁上觸發的同一組 JavaScript 呼叫：同樣的 &lt;code>fbq()&lt;/code> 呼叫、同樣的資料層（dataLayer，頁面上供 GTM 讀取事件的全域陣列 &lt;code>window.dataLayer&lt;/code>，定義見 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/vendors/google-tag-manager/" data-link-title="Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理" data-link-desc="GTM 容器在頁面上承擔的部署責任、資料層作為前端與行銷之間的事件約定、預覽模式與版本發布的驗證方式、自訂 HTML 代碼帶來的安全與效能成本，以及伺服器端代碼管理改變的資料路徑">Google Tag Manager&lt;/a> 的資料層一節）內容，結果可以拿來對照實測頁的面板（實測頁上列出操作紀錄、跨網域請求與追蹤 cookie 的區塊）。&lt;/p>
&lt;p>擷取腳本在 Meta Pixel 實測頁上依序做的動作是：載入 Pixel 並送 PageView、送 ViewContent、送帶 &lt;code>eventID&lt;/code> 的 Purchase，再把 &lt;code>fbclid&lt;/code> 參數加進網址重新開啟頁面並再載入一次 Pixel，觀察 &lt;code>_fbc&lt;/code> cookie 的產生。在 GTM 實測頁上是載入容器、按實測頁的「往資料層推 purchase 事件」按鈕。每個動作之後等三秒，讓 script 送完請求、寫完 cookie。&lt;/p>
&lt;p>實測頁的面板用瀏覽器的 Resource Timing 列出跨網域請求，Playwright 則直接監看瀏覽器送出的每一個請求，所以擷取結果多了面板拿不到的資訊：POST 請求的 body、回應的狀態碼與標頭、帶 &lt;code>HttpOnly&lt;/code> 旗標而 &lt;code>document.cookie&lt;/code> 讀不到的 cookie，以及面板根本列不出來的請求。Meta Pixel 的事件請求就是最後一種：&lt;code>fbevents.js&lt;/code> 以隱藏的表單把事件提交到一個 iframe，這類提交不進父頁面的 Resource Timing，所以實測頁面板上只看得到 &lt;code>fbevents.js&lt;/code> 與它的設定檔，看不到任何一筆事件；在 Playwright 的紀錄與瀏覽器開發者工具的 Network 面板裡看得到。&lt;/p>
&lt;p>擷取腳本預設以&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/headless-browser/" data-link-title="Headless Browser（無頭瀏覽器）" data-link-desc="用 Playwright 或 Puppeteer 測追蹤碼、分析報表裡的機器流量，或自建統計與商業追蹤碼的訪問數對不上時">無頭模式&lt;/a>（headless）啟動 Chromium，這時瀏覽器的 User-Agent 帶 &lt;code>HeadlessChrome&lt;/code> 字樣。實測時 Meta Pixel 在無頭模式下照樣載入、照樣寫 cookie，事件請求卻一筆都不送出，原因在 Pixel 的設定檔（&lt;code>signals/config/&amp;lt;Pixel ID&amp;gt;&lt;/code>）載入的 &lt;code>botblocking&lt;/code> 模組（plugin，&lt;code>fbevents.js&lt;/code> 依 Pixel 設定檔載入的程式片段）：依擷取當時那一版設定檔的原始碼，這個模組拿 &lt;code>navigator.userAgent&lt;/code> 比對一份機器人規則清單，清單裡有 &lt;code>HeadlessChrome/&lt;/code>，比對成功時由這個模組在事件送出之前把它攔下（模組程式碼裡對應的訊息字串是 &lt;code>[Meta pixel] Bot traffic detected and blocked&lt;/code>）。&lt;code>fbevents.js&lt;/code> 本體也偵測 &lt;code>HeadlessChrome&lt;/code>，但只把結果寫進事件請求的 &lt;code>o&lt;/code> 參數。&lt;/p>
&lt;p>兩組對照支持這個歸因，都沒有存成結果檔。作者在 Playwright 的瀏覽器設定裡只把 User-Agent 換成一般 Chrome 的字串、其餘照舊以無頭模式執行，事件就送出了，這證明結果取決於 User-Agent，但 &lt;code>fbevents.js&lt;/code> 本體同樣讀 User-Agent，單靠這一組分不出攔截的是模組還是本體。另一組來自一次填錯 Pixel ID 的擷取：同樣是無頭模式，不存在的 ID 拿到的設定檔裡沒有 &lt;code>botblocking&lt;/code>（只有幾個不分 ID 的通用模組，該 ID 專屬的那一段是空的），向 &lt;code>www.facebook.com/tr/&lt;/code> 的事件請求卻照樣送出（那次是 GET，&lt;code>o&lt;/code> 參數含 &lt;code>IS_HEADLESS&lt;/code>），所以本體偵測到無頭模式之後並沒有攔截，攔截來自設定檔載入的模組。那份結果檔因為 ID 填錯而刪除了。GTM 容器裡的 GA4 代碼在無頭模式下照常送出事件請求。依 User-Agent 辨識自動化流量是通用的做法，本站自己的流量統計怎麼分辨機器流量見 &lt;a href="https://tarrragon.github.io/blog/automation/06-reading-the-data/automated-traffic/" data-link-title="辨識自動化流量" data-link-desc="接收端讀不到 HTTP 標頭時在前端蒐集訊號分辨機器與真人；多數 AI 訓練爬蟲不執行 JavaScript，因此不會出現在這份統計裡">辨識自動化流量&lt;/a>。&lt;/p>
&lt;p>所以擷取 Meta Pixel 要加 &lt;code>--headed&lt;/code>，讓 Playwright 開一個有視窗的瀏覽器操作實測頁。擷取腳本不改寫 User-Agent，有視窗的瀏覽器仍然是自動化控制的瀏覽器，Meta 之後若加入其他偵測條件，同一個指令的結果可能改變，這也是每份結果要記下擷取模式的理由。&lt;/p></description><content:encoded><![CDATA[<p>這篇記錄用 Playwright 在本機操作 <a href="/blog/devops/monitoring/vendors/meta-pixel-lab/" data-link-title="Meta Pixel 實測頁：在瀏覽器裡觀察 fbq 呼叫、送出的請求與寫下的 cookie" data-link-desc="在這一頁按按鈕載入 Meta Pixel、送 PageView、ViewContent 與帶 eventID 的 Purchase，並用頁面上的面板對照每個呼叫送出的請求參數，以及 _fbp 與 fbclid 帶出的 _fbc cookie">Meta Pixel 實測頁</a> 與 <a href="/blog/devops/monitoring/vendors/google-tag-manager-lab/" data-link-title="Google Tag Manager 實測頁：在瀏覽器裡觀察容器載入、資料層事件與代碼送出的請求" data-link-desc="在這一頁按按鈕載入 GTM 容器、往資料層推 purchase 事件，並用頁面上的面板對照 window.dataLayer 的內容、容器載入的代碼與它們送出的請求">Google Tag Manager 實測頁</a>、擷取各家追蹤碼實際送出內容的方式與結果。下文的「擷取腳本」指本站寫的 Playwright 程式 <code>scripts/vendor-capture/capture.mjs</code>（在本站的公開 repo <a href="https://github.com/tarrragon/blog">tarrragon/blog</a> 裡），「script」指服務商（vendor）提供、在頁面上執行的 JavaScript（<code>fbevents.js</code>、<code>gtm.js</code>）。各家 script 由服務商自己的伺服器提供、隨時改版，所以本篇的每一份結果都只代表擷取當下的行為，引用時要連同擷取時間與版本資訊一起看。</p>
<h2 id="擷取方式讓瀏覽器自動化操作實測頁">擷取方式：讓瀏覽器自動化操作實測頁</h2>
<p>擷取腳本不直接把服務商的 script 載入一個空白頁，而是開啟實測頁、照讀者的操作順序按按鈕。這樣擷取到的是和讀者在實測頁上觸發的同一組 JavaScript 呼叫：同樣的 <code>fbq()</code> 呼叫、同樣的資料層（dataLayer，頁面上供 GTM 讀取事件的全域陣列 <code>window.dataLayer</code>，定義見 <a href="/blog/devops/monitoring/vendors/google-tag-manager/" data-link-title="Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理" data-link-desc="GTM 容器在頁面上承擔的部署責任、資料層作為前端與行銷之間的事件約定、預覽模式與版本發布的驗證方式、自訂 HTML 代碼帶來的安全與效能成本，以及伺服器端代碼管理改變的資料路徑">Google Tag Manager</a> 的資料層一節）內容，結果可以拿來對照實測頁的面板（實測頁上列出操作紀錄、跨網域請求與追蹤 cookie 的區塊）。</p>
<p>擷取腳本在 Meta Pixel 實測頁上依序做的動作是：載入 Pixel 並送 PageView、送 ViewContent、送帶 <code>eventID</code> 的 Purchase，再把 <code>fbclid</code> 參數加進網址重新開啟頁面並再載入一次 Pixel，觀察 <code>_fbc</code> cookie 的產生。在 GTM 實測頁上是載入容器、按實測頁的「往資料層推 purchase 事件」按鈕。每個動作之後等三秒，讓 script 送完請求、寫完 cookie。</p>
<p>實測頁的面板用瀏覽器的 Resource Timing 列出跨網域請求，Playwright 則直接監看瀏覽器送出的每一個請求，所以擷取結果多了面板拿不到的資訊：POST 請求的 body、回應的狀態碼與標頭、帶 <code>HttpOnly</code> 旗標而 <code>document.cookie</code> 讀不到的 cookie，以及面板根本列不出來的請求。Meta Pixel 的事件請求就是最後一種：<code>fbevents.js</code> 以隱藏的表單把事件提交到一個 iframe，這類提交不進父頁面的 Resource Timing，所以實測頁面板上只看得到 <code>fbevents.js</code> 與它的設定檔，看不到任何一筆事件；在 Playwright 的紀錄與瀏覽器開發者工具的 Network 面板裡看得到。</p>
<p>擷取腳本預設以<a href="/blog/devops/monitoring/knowledge-cards/headless-browser/" data-link-title="Headless Browser（無頭瀏覽器）" data-link-desc="用 Playwright 或 Puppeteer 測追蹤碼、分析報表裡的機器流量，或自建統計與商業追蹤碼的訪問數對不上時">無頭模式</a>（headless）啟動 Chromium，這時瀏覽器的 User-Agent 帶 <code>HeadlessChrome</code> 字樣。實測時 Meta Pixel 在無頭模式下照樣載入、照樣寫 cookie，事件請求卻一筆都不送出，原因在 Pixel 的設定檔（<code>signals/config/&lt;Pixel ID&gt;</code>）載入的 <code>botblocking</code> 模組（plugin，<code>fbevents.js</code> 依 Pixel 設定檔載入的程式片段）：依擷取當時那一版設定檔的原始碼，這個模組拿 <code>navigator.userAgent</code> 比對一份機器人規則清單，清單裡有 <code>HeadlessChrome/</code>，比對成功時由這個模組在事件送出之前把它攔下（模組程式碼裡對應的訊息字串是 <code>[Meta pixel] Bot traffic detected and blocked</code>）。<code>fbevents.js</code> 本體也偵測 <code>HeadlessChrome</code>，但只把結果寫進事件請求的 <code>o</code> 參數。</p>
<p>兩組對照支持這個歸因，都沒有存成結果檔。作者在 Playwright 的瀏覽器設定裡只把 User-Agent 換成一般 Chrome 的字串、其餘照舊以無頭模式執行，事件就送出了，這證明結果取決於 User-Agent，但 <code>fbevents.js</code> 本體同樣讀 User-Agent，單靠這一組分不出攔截的是模組還是本體。另一組來自一次填錯 Pixel ID 的擷取：同樣是無頭模式，不存在的 ID 拿到的設定檔裡沒有 <code>botblocking</code>（只有幾個不分 ID 的通用模組，該 ID 專屬的那一段是空的），向 <code>www.facebook.com/tr/</code> 的事件請求卻照樣送出（那次是 GET，<code>o</code> 參數含 <code>IS_HEADLESS</code>），所以本體偵測到無頭模式之後並沒有攔截，攔截來自設定檔載入的模組。那份結果檔因為 ID 填錯而刪除了。GTM 容器裡的 GA4 代碼在無頭模式下照常送出事件請求。依 User-Agent 辨識自動化流量是通用的做法，本站自己的流量統計怎麼分辨機器流量見 <a href="/blog/automation/06-reading-the-data/automated-traffic/" data-link-title="辨識自動化流量" data-link-desc="接收端讀不到 HTTP 標頭時在前端蒐集訊號分辨機器與真人；多數 AI 訓練爬蟲不執行 JavaScript，因此不會出現在這份統計裡">辨識自動化流量</a>。</p>
<p>所以擷取 Meta Pixel 要加 <code>--headed</code>，讓 Playwright 開一個有視窗的瀏覽器操作實測頁。擷取腳本不改寫 User-Agent，有視窗的瀏覽器仍然是自動化控制的瀏覽器，Meta 之後若加入其他偵測條件，同一個指令的結果可能改變，這也是每份結果要記下擷取模式的理由。</p>
<h2 id="擷取結果檔json的欄位">擷取結果檔（JSON）的欄位</h2>
<p>擷取腳本每次執行寫出一份 JSON，檔名是服務商名稱加上 UTC 擷取時間（例如 <code>meta-pixel-20261022T1726Z.json</code>），放在 <code>scripts/vendor-capture/results/</code>。欄位分成三組：</p>
<ul>
<li><strong>重現條件</strong>：<code>capturedAt</code>（UTC）與 <code>capturedAtTaipei</code>、<code>browser</code>（Chromium 版本）、<code>userAgent</code>、<code>headless</code>（這次是否以無頭模式執行）、<code>labUrl</code>、使用的 Pixel 或容器 ID。<code>headless</code> 欄位從 2026-10-23 01:26 那一次擷取起才有，01:23 與 00:45 兩份結果沒有這個欄位，看 <code>userAgent</code> 有沒有 <code>HeadlessChrome</code>。結果和這些條件綁在一起才有意義，換一個瀏覽器版本或服務商改版之後，同一個動作可能送出不同的參數。</li>
<li><strong>script 的版本線索</strong>：<code>requests</code> 裡 <code>fbevents.js</code>、<code>gtm.js</code> 那幾筆的 <code>body</code> 欄位（script 本體的位元組數與 SHA-256）與回應標頭，以及 <code>vendorRuntime.fbqVersion</code>（頁面上 <code>fbq.version</code> 的值）。實測兩家的回應都不帶 <code>last-modified</code> 與 <code>etag</code>，所以讀結果的人判斷 script 有沒有改版，以本體雜湊為主。<code>fbq.version</code> 在網站安裝的 Pixel 基本碼（base code）裡先被設成 <code>2.0</code>，<code>fbevents.js</code> 載入後覆寫成實際版本。<code>gtm.js</code> 的內容由容器設定與 Google 提供的 GTM 執行時程式碼（runtime）一起產生，雜湊變了之後要再到 GTM 後台的版本紀錄查容器那段期間有沒有發布，才分得出是哪一邊改的。</li>
<li><strong>觀察對象</strong>：<code>steps</code>（每個動作之後實測頁狀態訊息列的文字）、<code>requests</code>（每一筆跨網域請求的方法、網址、查詢參數、POST body、狀態碼）、<code>cookies</code>（追蹤 cookie 的名稱、值、網域、到期時間與旗標）、<code>vendorRuntime.dataLayer</code>（GTM 實測時資料層的完整內容）。</li>
</ul>
<p>判讀 <code>requests</code> 時，<code>status</code> 是主要訊號。瀏覽器收到回應之後才被中止的請求，Playwright 會觸發失敗事件，擷取腳本在失敗事件裡也讀取已收到的回應，所以結果檔同時記下 <code>status</code> 與 <code>failure</code>，這時以 <code>status</code> 為準：請求送達了。本篇的擷取裡只有 GA4 的 <code>g/collect</code> 出現這個組合，00:45 那一則記錄了它與作者用 Chrome DevTools Protocol 做的對照。<code>status</code> 為 <code>null</code> 而帶 <code>failure</code> 的，是沒有收到回應的請求。</p>
<h2 id="擷取用的測試帳號">擷取用的測試帳號</h2>
<p>擷取用的 Pixel 與 GTM 容器都是測試專用的資產，各自放在只放實測資產的擁有者底下：Meta 另開一個商家資產管理組合，GTM 與 GA4 各另開一個帳戶。這樣安排的理由見 <a href="/blog/devops/monitoring/vendors/tracking-account-ownership/" data-link-title="追蹤資產的歸屬：Meta、GTM 與 GA4 的擁有層級、權限分級與移交方式" data-link-desc="Meta Pixel 的資料集、GTM 容器與 GA4 資源（property）各自屬於哪一層的擁有者，三家的權限怎麼分級，哪些資產能換到另一個擁有者底下，以及公司自營、委託代理商、換代理商與事業轉手時的歸屬安排">追蹤資產的歸屬</a>。建立時三家的檢查與預設值會影響擷取結果，記在這裡：</p>
<ul>
<li><strong>Meta 對組合名稱有檢查</strong>：<code>vendor-lab-test</code> 與 <code>Tarragon</code> 兩個名稱都被退回，錯誤訊息是組合名稱必須如實反映商家，<code>Tarragon Tech Notes</code> 通過。Meta 的說明頁只寫組合名稱要和商家對外使用的名稱一致、不能有特殊字元，沒有列出退件的判斷條件；錯誤訊息附的連結是粉絲專頁的命名守則，其中一條是名稱不能只由一個統稱用詞組成（守則舉的例子是「披薩」），<code>Tarragon</code> 是香草的英文名稱，與這一條相符。GTM 與 GA4 這次以 <code>vendor-lab-test</code> 當帳戶名稱，都直接建立成功。</li>
<li><strong>Meta 的建立流程詢問要不要把既有的粉絲專頁加入新組合</strong>，預設不勾選。Meta 資料集不需要粉絲專頁，這次略過。</li>
<li><strong>Meta 資料集的建立表單有一個預設勾選的選項</strong>，表單上的說明是勾選之後，組合裡現有與日後新建的每一個資料集都會連結到 Meta 的 Conversions API（轉換 API），搭配 Pixel 把網站事件也送到 Meta 的伺服器端。這個選項由 Meta 那一端代為處理伺服器端的傳送，與 <a href="/blog/devops/monitoring/vendors/meta-pixel-conversions-api/" data-link-title="Meta Pixel 與 Conversions API：廣告平台轉換追蹤的瀏覽器端與伺服器端" data-link-desc="Meta Pixel 在頁面上送出的標準事件與它的資料消費者、瀏覽器端事件被攔截的路徑、Conversions API 從伺服器補送同一批事件的必填欄位，以及兩路並送時以 event_id 去重與用 Test Events 驗證的方式">Meta Pixel 與 Conversions API</a> 描述的、由網站自己的伺服器呼叫 Conversions API 是不同的設置方式。擷取要觀察的是瀏覽器端送出的內容，這次取消勾選，所以測試 Pixel 沒有伺服器端的事件來源。</li>
<li><strong>GA4 建立帳戶時，四個資料共用選項有三個預設勾選</strong>（「根據輸入內容和業務洞察資料建立模型」「技術支援」「依據您業務性質提供的建議」），這次全部取消；GTM 帳戶的匿名資料共用選項預設不勾選。</li>
</ul>
<p>Pixel ID 在事件管理工具（Events Manager）的資料集頁面讀，商家設定頁網址裡的 <code>selected_asset_id</code> 是設定頁內部的資產編號，與 Pixel ID 不同；GTM 容器 ID（<code>GTM-</code> 開頭）顯示在 GTM 後台工作區頁面的上方。</p>
<h2 id="執行擷取的指令">執行擷取的指令</h2>
<p>指令都在 repo 的根目錄執行。第一次執行前，先裝好 Node.js，安裝 repo 的相依套件（<code>@playwright/test</code> 與 <code>http-server</code>）與 Playwright 用的 Chromium：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 取得擷取腳本與實測頁的原始碼</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git clone https://github.com/tarrragon/blog.git <span class="o">&amp;&amp;</span> <span class="nb">cd</span> blog
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 安裝相依套件與 Playwright 的 Chromium；沒有裝 Chromium 時，第一次執行會出現 Executable doesn&#39;t exist</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">npm ci
</span></span><span class="line"><span class="ln">6</span><span class="cl">npx playwright install chromium</span></span></code></pre></div><p>擷取指令本身：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 擷取 Meta Pixel：--id 填測試用 Pixel 的 ID，--headed 讓事件請求送得出去</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">node scripts/vendor-capture/capture.mjs --vendor meta-pixel --id &lt;Pixel ID&gt; --headed
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 擷取 GTM：--id 填測試用容器的 ID（GTM- 開頭）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">node scripts/vendor-capture/capture.mjs --vendor gtm --id &lt;GTM-容器ID&gt;
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 改測本機建置的網站：先建置網站並啟動靜態伺服器，再把 --url 指到本機的實測頁</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">make site
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">mkdir -p .test-www <span class="o">&amp;&amp;</span> ln -sfn ../public .test-www/blog
</span></span><span class="line"><span class="ln">10</span><span class="cl">npx http-server .test-www -p <span class="m">4173</span> -s <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">node scripts/vendor-capture/capture.mjs --vendor meta-pixel --id &lt;Pixel ID&gt; --headed <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --url http://localhost:4173/blog/devops/monitoring/vendors/meta-pixel-lab/
</span></span><span class="line"><span class="ln">13</span><span class="cl">node scripts/vendor-capture/capture.mjs --vendor gtm --id &lt;GTM-容器ID&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --url http://localhost:4173/blog/devops/monitoring/vendors/google-tag-manager-lab/</span></span></code></pre></div><p>擷取腳本依 <code>--vendor</code>（<code>meta-pixel</code> 或 <code>gtm</code>）決定開哪一個實測頁、照哪一組動作操作。<code>--id</code> 是要填進實測頁欄位的 ID，用測試專用的 Pixel 與容器，擷取送出的事件才不會混進正式的廣告報表；兩個 ID 去哪裡讀，見〈擷取用的測試帳號〉一節的最後一段。省略 <code>--url</code> 時，擷取腳本開正式站的實測頁；加 <code>--out &lt;目錄&gt;</code> 時，擷取腳本把結果檔寫到那個目錄；<code>--headed</code> 不帶值，加上它就以有視窗的瀏覽器執行，執行期間螢幕上會開一個 Chromium 視窗、跑完自動關閉。本篇的擷取都在有桌面的 macOS 上執行，沒有顯示器的環境（例如沒有桌面的 Linux 伺服器）開不了有視窗的瀏覽器，本篇沒有驗證那種環境的做法。</p>
<p>擷取腳本啟動的 Chromium 由 <code>@playwright/test</code> 的版本決定；沒有跑過 <code>npx playwright install chromium</code>、或 Playwright 升級之後，執行時會出現 <code>Executable doesn't exist at …</code> 這個錯誤。這時重跑安裝指令，或用環境變數 <code>PW_CHROMIUM_PATH</code> 指定本機已有的 Chromium 執行檔，例如 <code>PW_CHROMIUM_PATH=&lt;Chromium 執行檔路徑&gt; node scripts/vendor-capture/capture.mjs …</code>。</p>
<p>改測本機那幾行指令裡，<code>make site</code> 用 Hugo 把網站建置到 <code>public/</code>，接著用 <code>npx pagefind</code> 產生站內搜尋的索引（這一步要連網下載 pagefind）；建置前要先裝好 Hugo，並以 <code>git submodule update --init --recursive</code> 拉下網站的佈景主題。本站的網址前綴是 <code>/blog/</code>，所以把 <code>public/</code> 以符號連結掛在 <code>.test-www/blog</code> 底下，再用 <code>http-server</code> 從 <code>.test-www</code> 提供檔案，網址才會和正式站的路徑結構一致。<code>4173</code> 是 <code>http-server</code> 監聽的連接埠，與 <code>--url</code> 裡的埠號要一致，換成其他未被占用的埠號時兩處一起改；網站的網址前綴不是 <code>/blog/</code> 時，<code>.test-www</code> 底下的資料夾名稱與 <code>--url</code> 的路徑一起改。<code>-s</code> 讓 <code>http-server</code> 不輸出每一筆存取紀錄，結尾的 <code>&amp;</code> 讓它在背景執行，擷取完用 <code>kill %1</code> 停掉它。</p>
<p>Pixel ID 填錯時沒有任何錯誤訊息：Meta 的伺服器對不存在的 ID 也回應 <code>signals/config/&lt;ID&gt;</code> 的請求（狀態碼 200），設定檔裡仍有幾個不分 ID 的通用模組，而該 ID 專屬的那一段只有 <code>/* empty plugin */</code>，<code>fbevents.js</code> 照樣送出事件。確認 ID 的方法是看 <code>signals/config</code> 回來的設定檔裡有沒有以這個 ID 開頭的 <code>config.set(&quot;&lt;Pixel ID&gt;&quot;, …)</code> 設定（存在的 Pixel 有好幾行，不存在的一行都沒有），或直接在事件管理工具的資料集頁面讀 ID。</p>
<p>執行環境（跑擷取腳本的那台機器）要能連到 <code>connect.facebook.net</code>、<code>www.facebook.com</code>、<code>www.googletagmanager.com</code> 與各家 script 用到的網域。連不到的時候擷取腳本照樣跑完並寫出結果檔，<code>steps</code> 會顯示 script 載入失敗，<code>requests</code> 裡向那些網域的請求帶 <code>failure</code> 欄位；這時結果檔反映的是網路環境，不是服務商 script 的行為。</p>
<h2 id="各次擷取的結果">各次擷取的結果</h2>
<p>這一節收錄服務商的 script 實際載入、請求有回應的擷取，依時間由新到舊排列，每一則寫明擷取時間、瀏覽器版本、script 的版本線索，以及和實測頁、<a href="/blog/devops/monitoring/vendors/meta-pixel-conversions-api/" data-link-title="Meta Pixel 與 Conversions API：廣告平台轉換追蹤的瀏覽器端與伺服器端" data-link-desc="Meta Pixel 在頁面上送出的標準事件與它的資料消費者、瀏覽器端事件被攔截的路徑、Conversions API 從伺服器補送同一批事件的必填欄位，以及兩路並送時以 event_id 去重與用 Test Events 驗證的方式">Meta Pixel 與 Conversions API</a>、<a href="/blog/devops/monitoring/vendors/google-tag-manager/" data-link-title="Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理" data-link-desc="GTM 容器在頁面上承擔的部署責任、資料層作為前端與行銷之間的事件約定、預覽模式與版本發布的驗證方式、自訂 HTML 代碼帶來的安全與效能成本，以及伺服器端代碼管理改變的資料路徑">Google Tag Manager</a> 兩篇機制文對照之後的觀察。網路被擋下的擷取不收，理由在〈執行擷取的指令〉一節的最後一段。</p>
<h3 id="2026-10-23-0126台北meta-pixel有視窗的瀏覽器">2026-10-23 01:26（台北）：Meta Pixel，有視窗的瀏覽器</h3>
<p>擷取時間是 UTC 2026-10-22 17:26:10，瀏覽器是 Playwright 提供的 Chromium 148.0.7778.96，在 macOS 上以 <code>--headed</code> 執行，User-Agent 不帶 <code>HeadlessChrome</code>；實測頁開在本機建置的網站。Pixel 是〈擷取用的測試帳號〉一節建立的測試 Meta 資料集。結果檔是 <code>scripts/vendor-capture/results/meta-pixel-20261022T1726Z.json</code>。</p>
<p>版本線索：<code>fbevents.js</code> 本體 435,058 位元組，SHA-256 開頭是 <code>6f3aa7a0d8d9</code>；<code>fbq.version</code> 是 <code>2.9.427</code>，<code>signals/config</code> 請求的查詢參數同樣帶 <code>v=2.9.427</code>、<code>r=stable</code>。這個 Pixel 的設定檔裡出現 26 個模組名稱，與本篇觀察相關的是 <code>cookie</code>（寫 <code>_fbp</code> 與 <code>_fbc</code>）、<code>inferredevents</code>（<a href="/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">自動事件</a>）與 <code>botblocking</code>（機器人流量）；不存在的 ID 拿到的設定檔裡沒有 <code>botblocking</code>，也沒有任何 <code>config.set(&quot;&lt;ID&gt;&quot;, …)</code> 設定。</p>
<p><strong>事件請求的形式。</strong> 每一筆事件都是向 <code>www.facebook.com/tr/</code> 送出的 POST，參數放在表單 body 裡，<code>rqm=formPOST</code>。<code>fbevents.js</code> 建立一個隱藏的表單，把它提交到一個 iframe，所以 Playwright 把這些請求記成 <code>document</code> 類型（<code>resourceType</code>），實測頁面板也因此列不出它們。<code>ev</code> 是事件名稱；<code>fbevents.js</code> 把 <code>fbq('track', …)</code> 的第三個引數（事件參數物件）轉成 <code>cd[...]</code> 參數，例如 ViewContent 的 <code>cd[content_name]</code>、Purchase 的 <code>cd[value]</code> 與 <code>cd[currency]</code>，第四個引數帶的 <code>eventID</code> 轉成 <code>eid</code>。網站同時從瀏覽器與伺服器送同一筆事件時，Meta 用 <code>eid</code> 把兩邊去重，機制見 <a href="/blog/devops/monitoring/vendors/meta-pixel-conversions-api/" data-link-title="Meta Pixel 與 Conversions API：廣告平台轉換追蹤的瀏覽器端與伺服器端" data-link-desc="Meta Pixel 在頁面上送出的標準事件與它的資料消費者、瀏覽器端事件被攔截的路徑、Conversions API 從伺服器補送同一批事件的必填欄位，以及兩路並送時以 event_id 去重與用 Test Events 驗證的方式">Meta Pixel 與 Conversions API</a> 的去重一節；這次的測試 Pixel 沒有伺服器端的事件來源，<code>eid</code> 只出現在瀏覽器端這一路。</p>
<p><strong>Pixel 自動加送的事件。</strong> Pixel 載入之後，每按一次實測頁的按鈕，<code>fbevents.js</code> 都在實測頁呼叫的事件之外多送一筆 <code>SubscribedButtonClick</code>，依兩筆請求的 <code>ts</code> 參數，相差不到 10 毫秒。這筆事件由 <code>fbevents.js</code> 讀取被點的按鈕之後自動產生，內容是按鈕文字（<code>cd[buttonText]</code> 是「送 ViewContent」）、按鈕的 DOM 特徵（<code>cd[buttonFeatures]</code>）與頁面特徵（<code>cd[pageFeatures]</code>）。網站開發者沒有為按鈕寫任何追蹤程式，按鈕文字就由 <code>fbevents.js</code> 送到了 Meta；盤點一個網站送出哪些資料時，要把 Pixel 自動加送的事件算進去。載入 Pixel 的那一下按鈕沒有對應的 <code>SubscribedButtonClick</code>，那時 <code>fbevents.js</code> 還沒載入。</p>
<p><strong>cookie 與參數的對應。</strong> <code>_fbp</code> 的值是 <code>fb.0.</code> 接毫秒時間戳與一串隨機數，每一筆事件的 <code>fbp</code> 參數都帶著這個值。帶 <code>fbclid</code> 重新開啟頁面之後多了 <code>_fbc</code>，值是 <code>fb.0.</code> 接毫秒時間戳與 <code>fbclid</code> 的原值，那一次的 PageView 同時帶 <code>fbp</code> 與 <code>fbc</code> 兩個參數，另外還有一個 <code>fbcs</code> 參數，原始碼裡它與 <code>fbc</code> 寫在同一組點擊 ID 的處理裡，本篇沒有進一步確認它記錄的內容。兩個 cookie 都由 <code>fbevents.js</code> 以 JavaScript 寫入（<code>httpOnly</code> 為 false）、<code>SameSite=Lax</code>，到期時間是擷取時間起算 90 天。</p>
<p><strong><code>o</code> 參數。</strong> 這一次的每一筆事件都帶 <code>o=4126</code>。<code>fbevents.js</code> 原始碼裡把 <code>o</code> 定義成一組旗標的位元和，4126 拆開是 <code>FIRST_PARTY_COOKIES</code>、<code>AUTO_CONFIG</code>、<code>CONFIG_LOADING</code>、<code>SUPPORTS_DEFINE_PROPERTY</code> 與 <code>SUPPORTS_SEND_BEACON</code>。原始碼裡另有一個 <code>IS_HEADLESS</code> 旗標，在無頭模式下加進 <code>o</code>；01:23 那次無頭擷取沒有送出任何事件，所以沒有 <code>o</code> 值可以對照。另一個參數 <code>coo</code> 在原始碼裡記錄的是免程式碼事件設定（codeless）是否被關閉，不是 cookie 的設定。</p>
<h3 id="2026-10-23-0123台北meta-pixel無頭模式">2026-10-23 01:23（台北）：Meta Pixel，無頭模式</h3>
<p>擷取時間是 UTC 2026-10-22 17:23:23，瀏覽器是 Chromium 148.0.7778.96 的無頭模式；Pixel、實測頁與操作順序與 01:26 那次以 <code>--headed</code> 執行的擷取相同，差別只在沒有加 <code>--headed</code>。結果檔是 <code>scripts/vendor-capture/results/meta-pixel-20261022T1723Z.json</code>。</p>
<p>版本線索：<code>fbevents.js</code> 本體的位元組數與 SHA-256、<code>fbq.version</code> 都與 01:26 那一次相同，兩次擷取用的是同一版 script，差別只來自擷取模式。</p>
<p><code>fbevents.js</code> 與 <code>signals/config</code> 都載入成功，<code>_fbp</code> 與 <code>_fbc</code> 照樣寫入，向 <code>www.facebook.com/tr/</code> 的事件請求一筆都沒有：PageView、ViewContent、Purchase 都沒有送出。差別來自無頭模式的 User-Agent 帶 <code>HeadlessChrome</code>，攔下事件的是 Pixel 設定檔載入的 <code>botblocking</code> 模組，機制見〈擷取方式〉一節。實測頁狀態訊息列顯示「fbevents.js 已載入」，從頁面上看不出事件沒送。<a href="/blog/devops/monitoring/vendors/meta-pixel-conversions-api/" data-link-title="Meta Pixel 與 Conversions API：廣告平台轉換追蹤的瀏覽器端與伺服器端" data-link-desc="Meta Pixel 在頁面上送出的標準事件與它的資料消費者、瀏覽器端事件被攔截的路徑、Conversions API 從伺服器補送同一批事件的必填欄位，以及兩路並送時以 event_id 去重與用 Test Events 驗證的方式">Meta Pixel 與 Conversions API</a> 列的瀏覽器端損失路徑裡，script 被廣告攔截器擋下時網站還有機會察覺載入失敗；這一次是 script 載入成功、cookie 也寫了，事件在送出之前被設定檔載入的模組攔下。</p>
<h3 id="2026-10-23-0045台北gtm-容器只帶一個-ga4-代碼">2026-10-23 00:45（台北）：GTM 容器只帶一個 GA4 代碼</h3>
<p>擷取時間是 UTC 2026-10-22 16:45:30，瀏覽器是 Playwright 提供的 Chromium 148.0.7778.96 的無頭模式（collect 請求的 <code>uaa=arm</code>、<code>uab=64</code> 顯示是 macOS arm64），實測頁開在本機建置的網站（<code>http://localhost:4173/blog/monitoring/vendors/google-tag-manager-lab/</code>）。GTM 容器是〈擷取用的測試帳號〉一節建立的測試容器，發布的版本裡只有一個 Google Analytics 代碼（tag），觸發條件（trigger）是 Initialization - All Pages，沒有任何以 <code>purchase</code> 為名的自訂事件觸發條件。代碼與觸發條件的定義見 <a href="/blog/devops/monitoring/vendors/google-tag-manager/" data-link-title="Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理" data-link-desc="GTM 容器在頁面上承擔的部署責任、資料層作為前端與行銷之間的事件約定、預覽模式與版本發布的驗證方式、自訂 HTML 代碼帶來的安全與效能成本，以及伺服器端代碼管理改變的資料路徑">Google Tag Manager</a> 講 GTM 責任的那一節。結果檔是 <code>scripts/vendor-capture/results/gtm-20261022T1645Z.json</code>。</p>
<p>版本線索：<code>gtm.js</code> 的回應本體 546,027 位元組，SHA-256 開頭是 <code>b970456b1ca6</code>；回應標頭只有 <code>content-type</code>、<code>date</code> 與 <code>cache-control: private, max-age=900</code>。</p>
<p><strong>載入容器之後送出的請求。</strong> 瀏覽器先向 <code>www.googletagmanager.com</code> 取得 <code>gtm.js?id=&lt;容器 ID&gt;</code>（200），約 0.1 秒後由容器裡的 GA4 代碼向 <code>www.google-analytics.com/g/collect</code> 送出一筆 POST（204）。這一筆 collect 請求的事件資料全部放在網址的查詢參數裡，POST body 是空的：<code>tid</code> 是評估 ID（Measurement ID），<code>en=page_view</code> 是事件名稱，<code>dl</code> 與 <code>dt</code> 是頁面網址與標題。<code>dl</code> 記錄的網址沒有連接埠號，實測頁實際開在 4173 埠，所以在本機用不同埠號測試時，GA4 報表上的頁面網址看不出是哪一個埠號的伺服器送來的。</p>
<p><strong>GA4 代碼寫下的 cookie。</strong> 一筆 <code>page_view</code> 之後，實測頁所在的網域（這次是 <code>localhost</code>）多出兩個 cookie。<code>_ga</code> 的值是 <code>GA1.1.</code> 接兩段數字，這兩段數字與 collect 請求的 <code>cid</code> 參數相同，GA4 用它辨識同一個瀏覽器。另一個 cookie 的名稱是 <code>_ga_</code> 接上評估 ID 去掉 <code>G-</code> 的部分，值記錄這個瀏覽器在這個 GA4 資源（property）下的工作階段，<code>s</code> 之後的時間戳與 collect 請求的 <code>sid</code> 參數相同。兩個 cookie 都由 GA4 代碼以 JavaScript 寫入（<code>httpOnly</code> 為 false）、<code>SameSite=Lax</code>，到期時間是擷取時間起算 400 天（這是 Chrome 對 cookie 到期時間的上限，GA4 代碼自己設的預設是兩年，見 <a href="/blog/devops/monitoring/knowledge-cards/first-party-cookie/" data-link-title="First-party Cookie（第一方 cookie）" data-link-desc="盤點網站寫下的識別碼 cookie、比對追蹤碼的 cookie 期限，或判斷某個 cookie 受不受瀏覽器的第三方 cookie 限制時">First-party Cookie（第一方 cookie）</a>）。</p>
<p><strong>往資料層推 purchase 事件之後。</strong> <code>window.dataLayer</code> 依序是實測頁推的 <code>gtm.js</code>、容器自己推的 <code>gtm.dom</code> 與 <code>gtm.load</code>，以及實測頁的「往資料層推 purchase 事件」按鈕推的 <code>purchase</code>（帶 <code>order_id</code>、<code>value</code>、<code>currency</code>）。推入 <code>purchase</code> 之後沒有新的請求，成因是這個容器裡沒有以名稱為 <code>purchase</code> 的自訂事件觸發條件啟動的 GA4 事件代碼，那一筆只留在陣列裡。<a href="/blog/devops/monitoring/vendors/google-tag-manager/" data-link-title="Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理" data-link-desc="GTM 容器在頁面上承擔的部署責任、資料層作為前端與行銷之間的事件約定、預覽模式與版本發布的驗證方式、自訂 HTML 代碼帶來的安全與效能成本，以及伺服器端代碼管理改變的資料路徑">Google Tag Manager</a> 的資料層一節描述的分工，在網路請求上就是這個樣子：網站推什麼事件由前端決定，事件是否變成送往某一家的請求由容器的設定決定。</p>
<p><strong>Playwright 對 collect 請求的記錄。</strong> Playwright 把 <code>g/collect</code> 那一筆記成 <code>requestfailed</code>（<code>net::ERR_ABORTED</code>）。作者在這次擷取之後另外用 Chrome DevTools Protocol 的網路事件對照同一個操作，瀏覽器在中止之前已收到 204，事件有送達；那次對照沒有存成結果檔。結果檔因此同時帶 <code>status: 204</code> 與 <code>failure</code>，判讀以 <code>status</code> 為準，見〈擷取結果檔（JSON）的欄位〉。請求清單裡另有一筆 <code>cdn.jsdelivr.net</code> 的 Mermaid script，那是本站每一頁都會載入的資源，與 GTM 無關。</p>
]]></content:encoded></item></channel></rss>