<?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>Google-Tag-Manager on Tarragon</title><link>https://tarrragon.github.io/blog/tags/google-tag-manager/</link><description>Recent content in Google-Tag-Manager on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 23 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/google-tag-manager/index.xml" rel="self" type="application/rss+xml"/><item><title>Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理</title><link>https://tarrragon.github.io/blog/devops/monitoring/vendors/google-tag-manager/</link><pubDate>Thu, 22 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/monitoring/vendors/google-tag-manager/</guid><description>&lt;p>這篇整理 Google Tag Manager（GTM）在網站追蹤架構裡的位置：它在頁面上做什麼、前端工程與行銷人員之間靠什麼約定交接事件、發布前怎麼驗證，以及它帶進來的治理成本。範圍是網頁容器（web container）與伺服器端代碼管理（server-side tagging）的資料路徑；GTM 後台每個選單怎麼操作不在這篇，介面以 Google 的說明文件為準。&lt;/p>
&lt;h2 id="gtm-承擔的責任追蹤碼的部署">GTM 承擔的責任：追蹤碼的部署&lt;/h2>
&lt;p>GTM 是一個在頁面上執行的追蹤碼載入器，分析資料由它載入的各家代碼收集：網站放一段容器片段（container snippet），頁面載入時從 &lt;code>googletagmanager.com&lt;/code> 下載這個容器目前發布的設定，再依設定決定在什麼時機載入哪些追蹤碼。設定由三種元件組成：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>代碼（tag）&lt;/strong>：要執行的追蹤碼，例如 GA4 的設定、Meta Pixel、一段自訂的 JavaScript。&lt;/li>
&lt;li>&lt;strong>觸發條件（trigger）&lt;/strong>：代碼在什麼時候執行，例如每一頁載入時、某個按鈕被點擊時、資料層 出現某個事件時。&lt;/li>
&lt;li>&lt;strong>變數（variable）&lt;/strong>：代碼執行時要讀的值，例如目前頁面的網址、資料層 裡的訂單金額。&lt;/li>
&lt;/ul>
&lt;p>GTM 存在的理由是部署權的轉移。沒有 GTM 時，行銷要加一個廣告平台的像素，得請前端改程式、走一次發版；有了 GTM，行銷在後台加代碼、按發布，網站程式不用動。這個轉移同時是 GTM 所有成本的來源：GTM 容器裡的代碼從此也在網站上執行，而它們不經過工程團隊的程式碼審查與版本控制。&lt;/p>
&lt;h2 id="資料層頁面與容器之間的事件約定">資料層：頁面與容器之間的事件約定&lt;/h2>
&lt;p>GTM 讀取網站狀態有兩種方式：從頁面的 DOM 抓（某個 CSS class 的按鈕被點擊、某個元素的文字），或讀網站主動推進來的資料。網站主動推送資料的載體是資料層（資料層）——頁面上的一個全域陣列 &lt;code>window.dataLayer&lt;/code>，網站在事件發生時往裡面推一個物件：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 訂單建立成功之後由前端程式推入；event 是 GTM 觸發條件比對的事件名稱
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">dataLayer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">dataLayer&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">dataLayer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">event&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;purchase&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">order_id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;order-20261022-0042&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 送往各家時當去重 ID 用
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">value&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">1280&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nx">currency&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;TWD&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>event&lt;/code> 這個鍵的值會被 GTM 的自訂事件觸發條件（Custom Event trigger）比對，名稱對上的代碼就執行，沒有任何觸發條件對上的事件只留在陣列裡、不產生請求（擷取紀錄見 &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> 的 GTM 那一則）；其餘的鍵可以在 GTM 裡建成 資料層 變數，讓代碼讀取。&lt;code>purchase&lt;/code>、&lt;code>order_id&lt;/code> 這些名稱由網站自己決定，GTM 不規定，所以它們要被當成一份 schema 管理：誰新增了哪個事件、欄位的型別與意義、改名時哪些代碼會受影響。&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/01-mental-model/event-naming-convention/" data-link-title="事件命名規範" data-link-desc="namespace.action 格式的事件命名、命名一致性的工程價值、和商業方案命名慣例的對應">事件命名規範&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/02-log-schema/schema-versioning/" data-link-title="Schema 版本演進策略" data-link-desc="Backward compatible 的增量變更 — 新增欄位不改版、改名或改型別才改版、collector 同時支援多版本">Schema 版本演進策略&lt;/a> 的做法同樣適用在這份約定上。&lt;/p>
&lt;p>兩種讀法的差別在前端改版的時候浮現。靠 DOM 抓的觸發條件綁在 CSS selector 上，前端重構把按鈕的 class 改名，觸發條件就不再命中，GTM 不報錯、事件安靜地消失，通常要等行銷發現報表的數字掉下來才會有人查。資料層 的推送寫在前端程式裡，改版時跟著程式碼一起被審查，所以分工會落成：前端工程負責在對的時機推對的事件，行銷負責把事件接到哪幾家代碼。&lt;/p>
&lt;p>&lt;code>order_id&lt;/code> 在這裡同時服務 &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> 的去重：GTM 裡的 Meta Pixel 代碼把它填進 &lt;code>eventID&lt;/code>，伺服器端用同一個值送 Conversions API。&lt;/p>
&lt;h2 id="預覽模式與版本發布的驗證">預覽模式與版本發布的驗證&lt;/h2>
&lt;p>GTM 的變更要先驗證再發布，驗證工具是預覽模式（Preview，在 Tag Assistant 裡開啟）。開啟後，這個瀏覽器載入網站時會用工作區裡還沒發布的設定，並在旁邊列出這次瀏覽依序發生的 資料層 事件；點選任一個事件，可以看到那個時間點哪些代碼執行了、哪些沒執行，以及每個變數當時的值。預覽模式靠 cookie 運作，只影響開啟它的那個瀏覽器，其他讀者看到的仍是已發布的版本。&lt;/p>
&lt;p>驗證一個購買事件時，操作一次測試購買，在預覽模式裡確認 &lt;code>purchase&lt;/code> 事件出現、該執行的代碼都在「已觸發」那一欄、資料層 變數讀到的金額與訂單相符。代碼送出的內容再到各家自己的驗證工具確認（GA4 的 DebugView、Meta 的 Test Events），因為預覽模式只看得到代碼有沒有執行，看不到對方的後台有沒有收到。&lt;/p>
&lt;p>GTM 有自己的版本紀錄：每次發布產生一個版本，可以比對兩個版本的差異、退回前一版。這份紀錄在 GTM 的後台裡，不在網站的 git 裡，所以網站程式的變更與追蹤設定的變更分住兩處。出事時要對時間軸的人得兩邊都查：某天轉換數掉了，可能是前端那天發版改了 資料層，也可能是有人那天在 GTM 發布了一個新版本。&lt;/p>
&lt;h2 id="自訂-html-代碼的安全與效能成本">自訂 HTML 代碼的安全與效能成本&lt;/h2>
&lt;p>GTM 的代碼類型裡有一種自訂 HTML（Custom HTML），內容是任意的 HTML 與 JavaScript，發布後會在每一個符合觸發條件的頁面上執行。這代表 GTM 容器的發布權限等同於在網站上執行任意程式碼的權限：一個帳號被盜、或一段從廣告商複製來的程式碼帶有錯誤或惡意行為，影響的是所有讀者，而這段程式碼從來沒有進過程式碼審查。治理上要控制的因此是權限：誰能發布（GTM 把編輯與發布分成不同的權限等級）、發布前誰審過、自訂 HTML 的內容有沒有人讀過。&lt;/p>
&lt;p>網站有設內容安全政策（Content Security Policy，CSP）時，GTM 要求的放行範圍也是這個成本的一部分：容器本身要放行 &lt;code>googletagmanager.com&lt;/code>，自訂 HTML 注入的 inline script 要靠 nonce 或放寬 &lt;code>script-src&lt;/code> 才能執行，每多接一家代碼還要再放行那一家的網域。放寬 CSP 讓 GTM 正常運作，同時也放寬了它原本要擋的那一類注入。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理 Google Tag Manager（GTM）在網站追蹤架構裡的位置：它在頁面上做什麼、前端工程與行銷人員之間靠什麼約定交接事件、發布前怎麼驗證，以及它帶進來的治理成本。範圍是網頁容器（web container）與伺服器端代碼管理（server-side tagging）的資料路徑；GTM 後台每個選單怎麼操作不在這篇，介面以 Google 的說明文件為準。</p>
<h2 id="gtm-承擔的責任追蹤碼的部署">GTM 承擔的責任：追蹤碼的部署</h2>
<p>GTM 是一個在頁面上執行的追蹤碼載入器，分析資料由它載入的各家代碼收集：網站放一段容器片段（container snippet），頁面載入時從 <code>googletagmanager.com</code> 下載這個容器目前發布的設定，再依設定決定在什麼時機載入哪些追蹤碼。設定由三種元件組成：</p>
<ul>
<li><strong>代碼（tag）</strong>：要執行的追蹤碼，例如 GA4 的設定、Meta Pixel、一段自訂的 JavaScript。</li>
<li><strong>觸發條件（trigger）</strong>：代碼在什麼時候執行，例如每一頁載入時、某個按鈕被點擊時、資料層 出現某個事件時。</li>
<li><strong>變數（variable）</strong>：代碼執行時要讀的值，例如目前頁面的網址、資料層 裡的訂單金額。</li>
</ul>
<p>GTM 存在的理由是部署權的轉移。沒有 GTM 時，行銷要加一個廣告平台的像素，得請前端改程式、走一次發版；有了 GTM，行銷在後台加代碼、按發布，網站程式不用動。這個轉移同時是 GTM 所有成本的來源：GTM 容器裡的代碼從此也在網站上執行，而它們不經過工程團隊的程式碼審查與版本控制。</p>
<h2 id="資料層頁面與容器之間的事件約定">資料層：頁面與容器之間的事件約定</h2>
<p>GTM 讀取網站狀態有兩種方式：從頁面的 DOM 抓（某個 CSS class 的按鈕被點擊、某個元素的文字），或讀網站主動推進來的資料。網站主動推送資料的載體是資料層（資料層）——頁面上的一個全域陣列 <code>window.dataLayer</code>，網站在事件發生時往裡面推一個物件：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 訂單建立成功之後由前端程式推入；event 是 GTM 觸發條件比對的事件名稱
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nb">window</span><span class="p">.</span><span class="nx">dataLayer</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">dataLayer</span> <span class="o">||</span> <span class="p">[];</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">window</span><span class="p">.</span><span class="nx">dataLayer</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">event</span><span class="o">:</span> <span class="s1">&#39;purchase&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">order_id</span><span class="o">:</span> <span class="s1">&#39;order-20261022-0042&#39;</span><span class="p">,</span>  <span class="c1">// 送往各家時當去重 ID 用
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">value</span><span class="o">:</span> <span class="mi">1280</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="nx">currency</span><span class="o">:</span> <span class="s1">&#39;TWD&#39;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p><code>event</code> 這個鍵的值會被 GTM 的自訂事件觸發條件（Custom Event trigger）比對，名稱對上的代碼就執行，沒有任何觸發條件對上的事件只留在陣列裡、不產生請求（擷取紀錄見 <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 那一則）；其餘的鍵可以在 GTM 裡建成 資料層 變數，讓代碼讀取。<code>purchase</code>、<code>order_id</code> 這些名稱由網站自己決定，GTM 不規定，所以它們要被當成一份 schema 管理：誰新增了哪個事件、欄位的型別與意義、改名時哪些代碼會受影響。<a href="/blog/devops/monitoring/01-mental-model/event-naming-convention/" data-link-title="事件命名規範" data-link-desc="namespace.action 格式的事件命名、命名一致性的工程價值、和商業方案命名慣例的對應">事件命名規範</a> 與 <a href="/blog/devops/monitoring/02-log-schema/schema-versioning/" data-link-title="Schema 版本演進策略" data-link-desc="Backward compatible 的增量變更 — 新增欄位不改版、改名或改型別才改版、collector 同時支援多版本">Schema 版本演進策略</a> 的做法同樣適用在這份約定上。</p>
<p>兩種讀法的差別在前端改版的時候浮現。靠 DOM 抓的觸發條件綁在 CSS selector 上，前端重構把按鈕的 class 改名，觸發條件就不再命中，GTM 不報錯、事件安靜地消失，通常要等行銷發現報表的數字掉下來才會有人查。資料層 的推送寫在前端程式裡，改版時跟著程式碼一起被審查，所以分工會落成：前端工程負責在對的時機推對的事件，行銷負責把事件接到哪幾家代碼。</p>
<p><code>order_id</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> 的去重：GTM 裡的 Meta Pixel 代碼把它填進 <code>eventID</code>，伺服器端用同一個值送 Conversions API。</p>
<h2 id="預覽模式與版本發布的驗證">預覽模式與版本發布的驗證</h2>
<p>GTM 的變更要先驗證再發布，驗證工具是預覽模式（Preview，在 Tag Assistant 裡開啟）。開啟後，這個瀏覽器載入網站時會用工作區裡還沒發布的設定，並在旁邊列出這次瀏覽依序發生的 資料層 事件；點選任一個事件，可以看到那個時間點哪些代碼執行了、哪些沒執行，以及每個變數當時的值。預覽模式靠 cookie 運作，只影響開啟它的那個瀏覽器，其他讀者看到的仍是已發布的版本。</p>
<p>驗證一個購買事件時，操作一次測試購買，在預覽模式裡確認 <code>purchase</code> 事件出現、該執行的代碼都在「已觸發」那一欄、資料層 變數讀到的金額與訂單相符。代碼送出的內容再到各家自己的驗證工具確認（GA4 的 DebugView、Meta 的 Test Events），因為預覽模式只看得到代碼有沒有執行，看不到對方的後台有沒有收到。</p>
<p>GTM 有自己的版本紀錄：每次發布產生一個版本，可以比對兩個版本的差異、退回前一版。這份紀錄在 GTM 的後台裡，不在網站的 git 裡，所以網站程式的變更與追蹤設定的變更分住兩處。出事時要對時間軸的人得兩邊都查：某天轉換數掉了，可能是前端那天發版改了 資料層，也可能是有人那天在 GTM 發布了一個新版本。</p>
<h2 id="自訂-html-代碼的安全與效能成本">自訂 HTML 代碼的安全與效能成本</h2>
<p>GTM 的代碼類型裡有一種自訂 HTML（Custom HTML），內容是任意的 HTML 與 JavaScript，發布後會在每一個符合觸發條件的頁面上執行。這代表 GTM 容器的發布權限等同於在網站上執行任意程式碼的權限：一個帳號被盜、或一段從廣告商複製來的程式碼帶有錯誤或惡意行為，影響的是所有讀者，而這段程式碼從來沒有進過程式碼審查。治理上要控制的因此是權限：誰能發布（GTM 把編輯與發布分成不同的權限等級）、發布前誰審過、自訂 HTML 的內容有沒有人讀過。</p>
<p>網站有設內容安全政策（Content Security Policy，CSP）時，GTM 要求的放行範圍也是這個成本的一部分：容器本身要放行 <code>googletagmanager.com</code>，自訂 HTML 注入的 inline script 要靠 nonce 或放寬 <code>script-src</code> 才能執行，每多接一家代碼還要再放行那一家的網域。放寬 CSP 讓 GTM 正常運作，同時也放寬了它原本要擋的那一類注入。</p>
<p>效能面的成本來自每一個代碼都是一段第三方 script。GTM 讓加代碼變得很便宜，所以容器裡的代碼數量只增不減，停用的廣告活動留下的像素很少有人回頭刪。定期盤點的做法是在預覽模式裡走一次主要頁面，列出實際執行的代碼與它們載入的網域，對照目前還在使用的服務。</p>
<h2 id="伺服器端代碼管理改變的資料路徑">伺服器端代碼管理改變的資料路徑</h2>
<p>網頁容器的每個代碼都從讀者的瀏覽器直接送資料給各家 vendor。伺服器端代碼管理在中間多放一個伺服器容器（server container），通常架在網站自己的子網域上：瀏覽器只把事件送到這個子網域一次，伺服器容器再轉送給 GA4、Meta Conversions API 等目的地。</p>
<p>這條路徑帶來三個變化。事件送往網站自己的網域，避開以已知追蹤網域為對象的攔截清單；資料送出去之前會先經過自己控制的伺服器，可以在那裡拿掉不該送給第三方的欄位（作法同 <a href="/blog/devops/monitoring/03-sdk-design/redaction-helper/" data-link-title="SDK redaction helper" data-link-desc="在事件離開 SDK 前移除敏感資訊 — 預設 redaction rule 處理常見 pattern，自訂 rule 處理業務特定的 secret">SDK redaction helper</a> 在 SDK 送出前做的遮蔽）；cookie 由自己的伺服器以 HTTP header 寫入，比 JavaScript 寫入的 cookie 受瀏覽器追蹤防護的限制少；這一項有前提：伺服器所在的子網域，它的 CNAME 或 IP 位址與網站主站不同、而指向第三方時（例如子網域以 CNAME 指到雲端服務的網域，而主站不在那裡），Safari 會把它寫的 cookie 期限壓到 7 天；與主站解析到同一個 CNAME 或 IP 範圍時不受這條限制（規則見 <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>）。代價是那台伺服器要自己跑：GTM 的伺服器容器部署在 Cloud Run 或自己的主機上，流量、費用與可用性都回到自架 collector 要面對的那些事，見 <a href="/blog/devops/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling</a>。</p>
<p>伺服器容器的預覽模式驗證的是另一段：它列出伺服器收到的請求、解析出的事件資料，以及轉送給各家的請求。網頁容器的預覽模式確認瀏覽器有送，伺服器容器的預覽模式確認轉送的內容對，兩段要分別驗。</p>
<p>容器載入、資料層事件與代碼送出的請求，可以在 <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> 填入測試容器的 ID 實際觸發一次。容器屬於哪一個帳戶、帳戶與容器的權限分級，以及把容器移到另一個帳戶時要匯出匯入、網站上的容器 ID 跟著換的處理，見 <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>
]]></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><item><title>追蹤資產的歸屬：Meta、GTM 與 GA4 的擁有層級、權限分級與移交方式</title><link>https://tarrragon.github.io/blog/devops/monitoring/vendors/tracking-account-ownership/</link><pubDate>Fri, 23 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/monitoring/vendors/tracking-account-ownership/</guid><description>&lt;p>這篇整理 Meta、Google Tag Manager（GTM）與 Google Analytics 4（GA4）三家服務商（vendor）的帳號結構：網站上的追蹤資產屬於哪一層的擁有者、誰能對它做什麼、它能不能換到另一個擁有者底下。範圍是擁有關係與權限的結構，各家後台的操作步驟以官方說明為準。內容依據 2026 年 10 月讀取的各家說明頁，帳號規則由服務商決定、會改，引用時要回原頁確認；文中由權限設計推出、說明頁沒有直接寫的做法，會在句子裡標明。&lt;/p>
&lt;h2 id="網站程式碼引用的-id-與它所屬的擁有者">網站程式碼引用的 ID 與它所屬的擁有者&lt;/h2>
&lt;p>三家的追蹤碼放進網站時，程式碼裡都寫著一個 ID，而這個 ID 指向一個由某一層擁有者持有的資產：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Meta&lt;/strong>：網站程式碼裡寫的是 Pixel ID（一串數字）。Pixel 收到的事件在 Meta 那一端整理成一個資料集（dataset），本文稱它為 &lt;strong>Meta 資料集&lt;/strong>，事件管理工具（Events Manager）的資料集頁面顯示的 ID 就是網站要填的 Pixel ID。Meta 資料集的擁有者是商家資產管理組合（business portfolio，舊名 Business Manager，下文簡稱&lt;strong>組合&lt;/strong>）；廣告帳號與粉絲專頁也是組合底下的資產。Facebook 的個人帳號不擁有組合底下的資產，它是組合裡的一位成員，透過組合取得權限。另一條路徑是還沒有建組合、直接在某個人的廣告帳號底下建立 Pixel，這時 Pixel 跟著那個廣告帳號，而那個廣告帳號由建立它的人以自己的 Facebook 帳號管理。&lt;/li>
&lt;li>&lt;strong>GTM&lt;/strong>：網站程式碼裡寫的是 &lt;code>GTM-&lt;/code> 開頭的容器 ID。&lt;strong>GTM 容器&lt;/strong>（container）是存放代碼（tag）、觸發條件（trigger）與變數的設定單位，網站上的一段片段載入它；容器屬於一個 GTM 帳戶（account），一個帳戶底下可以有多個容器。Google 的說明寫一家公司通常只需要一個帳戶。&lt;/li>
&lt;li>&lt;strong>GA4&lt;/strong>：直接安裝 GA4 時，網站程式碼裡寫的是 &lt;code>G-&lt;/code> 開頭的評估 ID（Measurement ID）。評估 ID 屬於網站資料串流（data stream），資料串流屬於一個 &lt;strong>GA4 資源&lt;/strong>（property，一個網站或 App 的報表與設定單位），資源屬於一個 GA4 帳戶。Google 對帳戶的定義是「一個法律實體擁有其資料的一組資源」。&lt;/li>
&lt;/ul>
&lt;p>判斷一個資產的歸屬時要確認兩件事：它屬於哪一層的擁有者，以及它能不能換到另一個擁有者底下。資產換得了擁有者時，網站程式碼不必動；換不了時，要讓新擁有者持有它就只能建一個新資產，網站上的 ID 跟著改，舊資產累積的資料留在原擁有者那裡。三家的資產各屬於哪一種，在〈追蹤資產換擁有者的條件〉一節逐家說明。&lt;/p>
&lt;p>Meta 的服務條款規定每個人只建立一個自己的個人帳號，所以「另開一個個人帳號給公司用」這條路在 Meta 不成立。組合是公司資產與員工個人帳號之間的分隔：資產屬於組合，個人帳號只是進出組合的身分。&lt;/p>
&lt;h2 id="metagtm-與-ga4-的權限分級">Meta、GTM 與 GA4 的權限分級&lt;/h2>
&lt;p>三家都把權限分成兩層：管理擁有者本身（組合或帳戶）的權限，與操作底下個別資產的權限。&lt;/p>
&lt;p>&lt;strong>Meta 的組合&lt;/strong>分成完整存取權限（full access，舊名完整控制權）與部分存取權限（partial access）。部分存取權限再分成基本（Basic，舊名員工）與應用程式和整合（Apps and integrations，舊名開發人員）；財務相關的權限是另外加上的附加權限。持有完整存取權限的成員也可以給其他人 3 到 75 天的暫時存取權限，暫時存取只有基本權限、到期自動失效，適合只需要短期協助的外部人員。持有完整存取權限的成員可以新增與移除成員、變更組合的設定，Meta 建議至少兩位成員擁有完整存取權限。成員對個別資產（某個 Meta 資料集、某個廣告帳號）的權限另外指派。&lt;/p>
&lt;p>&lt;strong>GTM&lt;/strong> 在帳戶層有管理員（Admin）與使用者（User）兩種角色，在容器層有五級權限：無權限、讀取（Read）、編輯（Edit）、核准（Approve）、發布（Publish）。持有編輯權限的成員可以修改容器裡的代碼、觸發條件與變數，但不能建立版本也不能發布；持有核准權限的成員另外可以建立版本與工作區（workspace，容器尚未發布的草稿），但不能發布。&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> 的自訂 HTML 一節說明過發布權限等同於在網站上執行任意程式碼的權限，容器層的分級讓這個權限可以只交給少數人。GTM 沒有「擁有者」這個角色，帳戶只有管理員，後台不允許把所有管理員移除；Google 建議至少有兩位管理員。&lt;/p>
&lt;p>&lt;strong>GA4&lt;/strong> 的角色是管理員（Administrator）、編輯者（Editor）、行銷人員（Marketer）、分析師（Analyst）、檢視者（Viewer），可以指派在帳戶層或資源層。分析師在檢視者的權限之上，可以把自己建立的探索分析分享給資源的其他成員；行銷人員在分析師的權限之上，可以建立與編輯目標對象、事件與關鍵事件（key events），並修改歸因模式的設定；資源層的完整設定權由編輯者持有。另外有兩項可以加在角色上的資料限制：不含成本指標（No Cost Metrics）與不含營收指標（No Revenue Metrics），各自讓成員看不到那一類指標，用在外部人員只該看流量與成效、不該看到營收或廣告花費的情況。GA4 的使用者管理說明頁沒有提到權限到期的設定，和 Meta 的暫時存取權限不同：只需要短期看報表的外部人員，要由管理員在期限到時自己移除。&lt;/p>
&lt;h2 id="追蹤資產換擁有者的條件meta-資料集gtm-容器與-ga4-資源">追蹤資產換擁有者的條件：Meta 資料集、GTM 容器與 GA4 資源&lt;/h2>
&lt;p>&lt;strong>Meta 資料集不能轉移，也不能刪除。&lt;/strong> Meta 的說明頁在建立資料集的步驟之前寫明這一點，替代做法是把資料集分享給合作夥伴（partner，另一個組合，例如代理商的組合）或加入成員。分享只能由擁有資產的組合發起，取得分享的合作夥伴即使有完整權限，也不能再分享給第三方。同樣固定的還有廣告帳號：在某個組合裡建立的廣告帳號永久屬於那個組合，不能刪除也不能轉出；不屬於任何組合的廣告帳號可以被認領進一個組合，而廣告帳號屬於別人時，要由那個擁有者批准認領的要求。粉絲專頁可以換組合，流程是原擁有者先把它從自己的組合移除，另一個組合再加入它。&lt;/p>
&lt;p>所以 Pixel 建在某個組合底下之後，要讓另一方使用它只有兩條路：原組合把 Meta 資料集分享給對方，資料集仍屬於原組合；或對方在自己的組合另建一個資料集、把網站上的 Pixel ID 換掉，代價是原資料集累積的事件資料留在原組合裡。&lt;/p>
&lt;p>&lt;strong>GTM 容器沒有官方的搬移功能。&lt;/strong> Google 的說明頁沒有寫容器能不能從一個帳戶移到另一個帳戶，有文件記載的路徑是把容器匯出成 JSON 檔，再匯入到另一個帳戶的容器裡（匯入時可選擇覆寫或合併）。匯入的目標是另一個容器，它有自己的容器 ID，所以走這條路之後網站上的容器片段要換成新的 ID。不換 ID 的做法是不搬容器、換人移交：在原帳戶加入新擁有者的管理員，再移除原本的管理員。先加後移的順序是因為後台不允許把所有管理員移除，反過來做時移除最後一位會失敗。這是由權限設計推出的做法，前提是整個帳戶裡只有要移交的容器。&lt;/p>
&lt;p>&lt;strong>GA4 資源可以搬到另一個帳戶。&lt;/strong> 執行搬移的人要在來源帳戶與目的帳戶都同時有管理員與編輯者兩個角色。Google Marketing Platform 機構（organization）是位在帳戶之上、由公司建立用來集中管理 Google 行銷產品的單位，搬移受它限制：兩個帳戶分屬不同機構時不能搬；只有目的帳戶屬於機構時，由目的帳戶所屬機構的管理員把來源帳戶連結進同一個機構。說明頁沒有列出兩個帳戶都不屬於任何機構時的限制。搬移時報表資料是移過去而不是複製一份，資料串流與它的設定、資源設定、與其他產品的連結都一起移動，所以網站上的評估 ID 不必改；搬移之前的變更記錄留在來源帳戶。連結本身跟著資源移動；被連結的 Google Ads 帳戶與 BigQuery 專案的歸屬，不在本篇範圍。不能搬的情形包括資源連結了 Google Ad Manager、還有未抽樣報表（GA4 360 付費版才有的報表）正在處理、目的帳戶的資源數已達上限（預設 50 個），以及 GA4 360 資源的機構連結還沒有驗證。&lt;/p>
&lt;p>三家可以換擁有者的單位不同：Meta 資料集與組合內建立的廣告帳號不能換，GTM 容器只能複製設定、不能保留 ID，GA4 資源能整個搬。三家共通的是最高的那一層（組合、帳戶）可以換人移交：把新擁有者的人加成完整存取權限或管理員，再移除原本的人，底下的資產與 ID 全部不動。換人移交是由三家的權限設計推出的做法，說明頁沒有寫成移交流程。它的前提是那一層裡只放著要移交的資產，而且三件事要先處理：Meta 組合裡由別的組合以合作夥伴身分分享進來的資產，換人之後會連同存取一起交出去；廣告帳號綁的付款方式仍是原擁有者的，換人不會改掉它；GA4 與 GTM 帳戶若連結在原擁有者的 Google Marketing Platform 機構底下，換人不會讓帳戶離開那個機構（機構管理員之後還保有哪些權限，本篇沒有查證）。&lt;/p>
&lt;h2 id="追蹤資產的歸屬安排公司自營與委託代理商">追蹤資產的歸屬安排：公司自營與委託代理商&lt;/h2>
&lt;p>Meta 資料集不能轉、GTM 容器不能保留 ID、GA4 資源跨機構不能搬，這三項限制推出一個安排原則：&lt;strong>追蹤資產建在公司自己的組合與帳戶底下，代理商以合作夥伴或成員的身分取得權限。&lt;/strong> 反過來由代理商在自己的組合或帳戶裡建立 Pixel、容器與資源再給公司用，日常運作沒有差別，差別出現在合作結束的時候：Meta 資料集留在代理商的組合裡，公司要嘛繼續靠代理商分享才看得到，要嘛另建一個並改網站上的 Pixel ID；GTM 容器要匯出匯入並換 ID；GA4 資源要搬到公司的帳戶，而兩邊分屬不同機構時搬不了。GTM 的說明頁直接寫了這個建議：帳戶要由組織內部的人管理，不要交給外部代理商。&lt;/p>
&lt;p>各家對應的做法，以及這樣分的理由：&lt;/p></description><content:encoded><![CDATA[<p>這篇整理 Meta、Google Tag Manager（GTM）與 Google Analytics 4（GA4）三家服務商（vendor）的帳號結構：網站上的追蹤資產屬於哪一層的擁有者、誰能對它做什麼、它能不能換到另一個擁有者底下。範圍是擁有關係與權限的結構，各家後台的操作步驟以官方說明為準。內容依據 2026 年 10 月讀取的各家說明頁，帳號規則由服務商決定、會改，引用時要回原頁確認；文中由權限設計推出、說明頁沒有直接寫的做法，會在句子裡標明。</p>
<h2 id="網站程式碼引用的-id-與它所屬的擁有者">網站程式碼引用的 ID 與它所屬的擁有者</h2>
<p>三家的追蹤碼放進網站時，程式碼裡都寫著一個 ID，而這個 ID 指向一個由某一層擁有者持有的資產：</p>
<ul>
<li><strong>Meta</strong>：網站程式碼裡寫的是 Pixel ID（一串數字）。Pixel 收到的事件在 Meta 那一端整理成一個資料集（dataset），本文稱它為 <strong>Meta 資料集</strong>，事件管理工具（Events Manager）的資料集頁面顯示的 ID 就是網站要填的 Pixel ID。Meta 資料集的擁有者是商家資產管理組合（business portfolio，舊名 Business Manager，下文簡稱<strong>組合</strong>）；廣告帳號與粉絲專頁也是組合底下的資產。Facebook 的個人帳號不擁有組合底下的資產，它是組合裡的一位成員，透過組合取得權限。另一條路徑是還沒有建組合、直接在某個人的廣告帳號底下建立 Pixel，這時 Pixel 跟著那個廣告帳號，而那個廣告帳號由建立它的人以自己的 Facebook 帳號管理。</li>
<li><strong>GTM</strong>：網站程式碼裡寫的是 <code>GTM-</code> 開頭的容器 ID。<strong>GTM 容器</strong>（container）是存放代碼（tag）、觸發條件（trigger）與變數的設定單位，網站上的一段片段載入它；容器屬於一個 GTM 帳戶（account），一個帳戶底下可以有多個容器。Google 的說明寫一家公司通常只需要一個帳戶。</li>
<li><strong>GA4</strong>：直接安裝 GA4 時，網站程式碼裡寫的是 <code>G-</code> 開頭的評估 ID（Measurement ID）。評估 ID 屬於網站資料串流（data stream），資料串流屬於一個 <strong>GA4 資源</strong>（property，一個網站或 App 的報表與設定單位），資源屬於一個 GA4 帳戶。Google 對帳戶的定義是「一個法律實體擁有其資料的一組資源」。</li>
</ul>
<p>判斷一個資產的歸屬時要確認兩件事：它屬於哪一層的擁有者，以及它能不能換到另一個擁有者底下。資產換得了擁有者時，網站程式碼不必動；換不了時，要讓新擁有者持有它就只能建一個新資產，網站上的 ID 跟著改，舊資產累積的資料留在原擁有者那裡。三家的資產各屬於哪一種，在〈追蹤資產換擁有者的條件〉一節逐家說明。</p>
<p>Meta 的服務條款規定每個人只建立一個自己的個人帳號，所以「另開一個個人帳號給公司用」這條路在 Meta 不成立。組合是公司資產與員工個人帳號之間的分隔：資產屬於組合，個人帳號只是進出組合的身分。</p>
<h2 id="metagtm-與-ga4-的權限分級">Meta、GTM 與 GA4 的權限分級</h2>
<p>三家都把權限分成兩層：管理擁有者本身（組合或帳戶）的權限，與操作底下個別資產的權限。</p>
<p><strong>Meta 的組合</strong>分成完整存取權限（full access，舊名完整控制權）與部分存取權限（partial access）。部分存取權限再分成基本（Basic，舊名員工）與應用程式和整合（Apps and integrations，舊名開發人員）；財務相關的權限是另外加上的附加權限。持有完整存取權限的成員也可以給其他人 3 到 75 天的暫時存取權限，暫時存取只有基本權限、到期自動失效，適合只需要短期協助的外部人員。持有完整存取權限的成員可以新增與移除成員、變更組合的設定，Meta 建議至少兩位成員擁有完整存取權限。成員對個別資產（某個 Meta 資料集、某個廣告帳號）的權限另外指派。</p>
<p><strong>GTM</strong> 在帳戶層有管理員（Admin）與使用者（User）兩種角色，在容器層有五級權限：無權限、讀取（Read）、編輯（Edit）、核准（Approve）、發布（Publish）。持有編輯權限的成員可以修改容器裡的代碼、觸發條件與變數，但不能建立版本也不能發布；持有核准權限的成員另外可以建立版本與工作區（workspace，容器尚未發布的草稿），但不能發布。<a href="/blog/devops/monitoring/vendors/google-tag-manager/" data-link-title="Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理" data-link-desc="GTM 容器在頁面上承擔的部署責任、資料層作為前端與行銷之間的事件約定、預覽模式與版本發布的驗證方式、自訂 HTML 代碼帶來的安全與效能成本，以及伺服器端代碼管理改變的資料路徑">Google Tag Manager</a> 的自訂 HTML 一節說明過發布權限等同於在網站上執行任意程式碼的權限，容器層的分級讓這個權限可以只交給少數人。GTM 沒有「擁有者」這個角色，帳戶只有管理員，後台不允許把所有管理員移除；Google 建議至少有兩位管理員。</p>
<p><strong>GA4</strong> 的角色是管理員（Administrator）、編輯者（Editor）、行銷人員（Marketer）、分析師（Analyst）、檢視者（Viewer），可以指派在帳戶層或資源層。分析師在檢視者的權限之上，可以把自己建立的探索分析分享給資源的其他成員；行銷人員在分析師的權限之上，可以建立與編輯目標對象、事件與關鍵事件（key events），並修改歸因模式的設定；資源層的完整設定權由編輯者持有。另外有兩項可以加在角色上的資料限制：不含成本指標（No Cost Metrics）與不含營收指標（No Revenue Metrics），各自讓成員看不到那一類指標，用在外部人員只該看流量與成效、不該看到營收或廣告花費的情況。GA4 的使用者管理說明頁沒有提到權限到期的設定，和 Meta 的暫時存取權限不同：只需要短期看報表的外部人員，要由管理員在期限到時自己移除。</p>
<h2 id="追蹤資產換擁有者的條件meta-資料集gtm-容器與-ga4-資源">追蹤資產換擁有者的條件：Meta 資料集、GTM 容器與 GA4 資源</h2>
<p><strong>Meta 資料集不能轉移，也不能刪除。</strong> Meta 的說明頁在建立資料集的步驟之前寫明這一點，替代做法是把資料集分享給合作夥伴（partner，另一個組合，例如代理商的組合）或加入成員。分享只能由擁有資產的組合發起，取得分享的合作夥伴即使有完整權限，也不能再分享給第三方。同樣固定的還有廣告帳號：在某個組合裡建立的廣告帳號永久屬於那個組合，不能刪除也不能轉出；不屬於任何組合的廣告帳號可以被認領進一個組合，而廣告帳號屬於別人時，要由那個擁有者批准認領的要求。粉絲專頁可以換組合，流程是原擁有者先把它從自己的組合移除，另一個組合再加入它。</p>
<p>所以 Pixel 建在某個組合底下之後，要讓另一方使用它只有兩條路：原組合把 Meta 資料集分享給對方，資料集仍屬於原組合；或對方在自己的組合另建一個資料集、把網站上的 Pixel ID 換掉，代價是原資料集累積的事件資料留在原組合裡。</p>
<p><strong>GTM 容器沒有官方的搬移功能。</strong> Google 的說明頁沒有寫容器能不能從一個帳戶移到另一個帳戶，有文件記載的路徑是把容器匯出成 JSON 檔，再匯入到另一個帳戶的容器裡（匯入時可選擇覆寫或合併）。匯入的目標是另一個容器，它有自己的容器 ID，所以走這條路之後網站上的容器片段要換成新的 ID。不換 ID 的做法是不搬容器、換人移交：在原帳戶加入新擁有者的管理員，再移除原本的管理員。先加後移的順序是因為後台不允許把所有管理員移除，反過來做時移除最後一位會失敗。這是由權限設計推出的做法，前提是整個帳戶裡只有要移交的容器。</p>
<p><strong>GA4 資源可以搬到另一個帳戶。</strong> 執行搬移的人要在來源帳戶與目的帳戶都同時有管理員與編輯者兩個角色。Google Marketing Platform 機構（organization）是位在帳戶之上、由公司建立用來集中管理 Google 行銷產品的單位，搬移受它限制：兩個帳戶分屬不同機構時不能搬；只有目的帳戶屬於機構時，由目的帳戶所屬機構的管理員把來源帳戶連結進同一個機構。說明頁沒有列出兩個帳戶都不屬於任何機構時的限制。搬移時報表資料是移過去而不是複製一份，資料串流與它的設定、資源設定、與其他產品的連結都一起移動，所以網站上的評估 ID 不必改；搬移之前的變更記錄留在來源帳戶。連結本身跟著資源移動；被連結的 Google Ads 帳戶與 BigQuery 專案的歸屬，不在本篇範圍。不能搬的情形包括資源連結了 Google Ad Manager、還有未抽樣報表（GA4 360 付費版才有的報表）正在處理、目的帳戶的資源數已達上限（預設 50 個），以及 GA4 360 資源的機構連結還沒有驗證。</p>
<p>三家可以換擁有者的單位不同：Meta 資料集與組合內建立的廣告帳號不能換，GTM 容器只能複製設定、不能保留 ID，GA4 資源能整個搬。三家共通的是最高的那一層（組合、帳戶）可以換人移交：把新擁有者的人加成完整存取權限或管理員，再移除原本的人，底下的資產與 ID 全部不動。換人移交是由三家的權限設計推出的做法，說明頁沒有寫成移交流程。它的前提是那一層裡只放著要移交的資產，而且三件事要先處理：Meta 組合裡由別的組合以合作夥伴身分分享進來的資產，換人之後會連同存取一起交出去；廣告帳號綁的付款方式仍是原擁有者的，換人不會改掉它；GA4 與 GTM 帳戶若連結在原擁有者的 Google Marketing Platform 機構底下，換人不會讓帳戶離開那個機構（機構管理員之後還保有哪些權限，本篇沒有查證）。</p>
<h2 id="追蹤資產的歸屬安排公司自營與委託代理商">追蹤資產的歸屬安排：公司自營與委託代理商</h2>
<p>Meta 資料集不能轉、GTM 容器不能保留 ID、GA4 資源跨機構不能搬，這三項限制推出一個安排原則：<strong>追蹤資產建在公司自己的組合與帳戶底下，代理商以合作夥伴或成員的身分取得權限。</strong> 反過來由代理商在自己的組合或帳戶裡建立 Pixel、容器與資源再給公司用，日常運作沒有差別，差別出現在合作結束的時候：Meta 資料集留在代理商的組合裡，公司要嘛繼續靠代理商分享才看得到，要嘛另建一個並改網站上的 Pixel ID；GTM 容器要匯出匯入並換 ID；GA4 資源要搬到公司的帳戶，而兩邊分屬不同機構時搬不了。GTM 的說明頁直接寫了這個建議：帳戶要由組織內部的人管理，不要交給外部代理商。</p>
<p>各家對應的做法，以及這樣分的理由：</p>
<ul>
<li><strong>Meta</strong>：公司建立自己的組合，Meta 資料集與廣告帳號建在組合底下，再以合作夥伴的方式分享給代理商的組合。代理商自己的員工由代理商在它的組合裡增減，公司不必追蹤代理商的人事異動；合作結束時，公司撤銷對代理商組合的分享，就一次收回所有代理商員工的存取。</li>
<li><strong>GTM</strong>：帳戶由公司建立，公司的管理員把代理商的人員加成帳戶的使用者，再給容器層的權限。發布權限等同在網站上執行任意程式碼，所以權限給到哪一級取決於公司是否在發布前審查代理商的變更：有人審查時，公司把容器層權限給到核准（Approve）這一級，發布由公司的人做；不審查而把發布權限交給代理商時，代理商發布的自訂 HTML 會直接在每一位讀者的瀏覽器上執行，出事的後果由公司承擔。</li>
<li><strong>GA4</strong>：帳戶與資源由公司建立，公司的管理員把代理商的人員加在資源層，角色依代理商要做的事、給到剛好夠用的那一級（<a href="/blog/backend/knowledge-cards/least-privilege/" data-link-title="Least Privilege" data-link-desc="說明身份、服務與人員只應取得完成工作所需的最小權限">最小權限</a>）：看報表與做分析給分析師角色，需要調整目標對象、關鍵事件與歸因設定給行銷人員角色，需要改資源的其他設定才給編輯者角色；代理商不該看到營收或廣告花費時，再加上對應的資料限制。GA4 的說明頁沒有針對代理商的建議，這一條是由角色設計推出的安排。</li>
</ul>
<p><strong>擁有最高管理權的成員（Meta 的完整存取權限、GTM 與 GA4 的管理員）至少兩位，而且至少一位是公司裡長期在職的人。</strong> Meta 與 GTM 的說明頁都建議至少兩位，GTM 還禁止移除所有管理員；GA4 的說明頁沒有這項建議，而它的帳戶同樣只能由管理員增減成員。只有一位時的失效不會立刻出現：組合由一個人就建得起來，建立的人自動取得完整存取權限；其他成員各自握有廣告帳號與資料集的權限，已經指派給其他成員的投放與報表權限不需要完整存取權限，所以唯一的管理員離職或帳號遺失之後，日常的投放與看報表照常運作（由權限設計推出），要等到需要加入新代理商或移除離職者的那一天，才發現沒有人做得到。另一種情形來得比較快：那一位的個人檔案被 Meta 限制刊登廣告時，Meta 的說明寫明，只連結他一個人的廣告帳號也可能一起被停用。</p>
<p>唯一的管理員已經不在、才發現沒有人管理得了之後，三家能不能救回各不相同。</p>
<p>Meta 有一條申訴路徑（管理員爭議），說明頁列出的適用情形有：持有完整存取權限的是沒有所有權的前員工或承包商；公司換了擁有者而失去組合的存取權；組合裡沒有任何人持有完整存取權限；持有完整存取權限的人已過世（附死亡證明）；有法院命令證明申請人是組合所屬公司的合法擁有者。申請時要附上申請人的身分證明、證明擁有這家公司的文件與簽名的聲明書，並且要登入與這個組合有關聯的 Facebook 個人檔案，從 Meta 的商家支援中心送出；核准之後申請人取得完整存取權限，原有成員不會被移出組合。前承包商握著組合不交還，是說明頁明列的情形；唯一一位管理員自己的 Facebook 帳號遺失則不在清單裡，而且那時公司手上可能已經沒有任何與組合有關聯的個人檔案可以登入送件，所以要先走 Facebook 個人帳號的復原，帳號找不回來時，這條申訴路徑很可能走不通（依送件條件推出，本站沒有實際送件驗證）。</p>
<p>GA4 的復原方式是在帳戶裡每一個送資料的網站上放一個寫有驗證資訊的 <code>analytics.txt</code> 檔（放在根網域或送資料的子網域，不能上傳檔案時改在頁面加 meta tag），證明網站屬於自己，帳戶連結了 Google Ads 或 AdSense 時也要證明那些帳戶屬於自己，再填 Google 的聯絡表單。</p>
<p>GTM 的情形寫在同一份 GA4 復原說明頁裡：GTM 目前沒有復原途徑，失去所有管理員的 GTM 帳戶救不回來，只能另建容器、換掉網站上的容器 ID。事先多設一位管理員，省下的就是這些申訴與重建。</p>
<p>只有一個人的公司也適用同一個理由：第二位成員承擔的責任是在唯一的帳號遺失時，還有另一個身分能繼續管理，所以第二位不必是員工，可以是合夥人、家人或委託的會計事務所，給他完整存取權限或管理員。這個人同樣有權限移除原本的管理員，所以人選的條件是可以信任，而不只是方便聯絡。Meta 的條款不允許同一個人另開帳號充當第二位。找不到第二位時，風險集中在那唯一一個帳號上。Meta 的說明頁要求從與組合有關聯的個人檔案送出管理員爭議，那個帳號本身遺失時，這條申訴路徑可能走不通（依送件條件推出，本站沒有實際送件驗證），所以能做的是降低那個帳號遺失的機率、並保住它的復原途徑：替自己的 Facebook 與 Google 帳號開啟兩步驟驗證，把帳號綁定的復原電子郵件與手機號碼設成自己長期持有、不隨公司網域或門號變動的那一組。</p>
<p>帳號是被盜而不是遺失時，情形不同：盜用者以那個帳號的完整存取權限持有組合，網站上的 Pixel 仍把訪客的事件送進他控制的資料集，廣告帳號綁定的付款方式也還能被使用。這時有兩件事不必等帳號復原、也不需要那個帳號的權限，可以同時做：從網站拿掉那個 Pixel 的程式碼，切斷網站送進去的資料；向發卡機構停用廣告帳號綁的那張卡，擋住持續中的扣款（由權限設計推出）。之後走 Facebook 帳號的復原，帳號拿不回來時，在新的組合另建資料集，放回網站的是新的 Pixel ID。</p>
<p><strong>成員的身分屬於個人，資產屬於擁有者。</strong> 三家的成員都是個人身分：Meta 是個人的 Facebook 帳號，GTM 與 GA4 是 Google 帳號。成員用的是公司 Google Workspace 的帳號時，公司在 Workspace 停權或刪除那個帳號，他就登不進 GTM 與 GA4（由帳號機制推出：GTM 與 GA4 以 Google 帳號登入）；用的是個人 Gmail 或個人 Facebook 帳號時，公司停用內部帳號碰不到它們，要到每一個組合與帳戶裡逐一移除；整份離職撤權清單見 <a href="/blog/devops/infra/08-governance-habits/handover-design/" data-link-title="職務交接與存取撤銷設計" data-link-desc="人員異動時的存取撤銷順序、credential rotation、最小交接清單，以及讓交接成本結構性降低的 infra 設計原則">職務交接與存取撤銷設計</a>。資產建在公司的組合與帳戶底下時，員工離職只要由公司的管理員把他從成員中移除，公司就收回了他的權限，資產本身不受影響；資產建在員工自己的組合或帳戶底下時，那位員工就是擁有者，公司沒有可以移除他的權限。</p>
<h2 id="資產與權限的移交方式換代理商與事業轉手">資產與權限的移交方式：換代理商與事業轉手</h2>
<p>移交的每一條路，除了看資產屬於哪一層，還要看兩件事：這一步要哪一方動手，以及那一方是否還聯絡得上、願意配合。下文的「對方」指公司以外要動手的那一方：換代理商時是舊代理商或廣告帳號的擁有者，事業轉手時是賣方；標「由權限設計推出」的做法，是本篇依三家的權限規則推導、Meta 與 Google 的說明頁沒有寫成流程的做法。認領廣告帳號要擁有者批准，分享舊的 Meta 資料集、匯出 GTM 容器、把帳戶連結進 Google Marketing Platform 機構都要對方的人操作，GA4 資源的搬移還要求執行的人在兩個帳戶都同時是管理員與編輯者。對方的狀態分成三種，出口各不相同。對方聯絡得上而且配合時，那段期間是處理這些事的窗口。對方失聯或拒絕配合、但手上沒有公司網站還在使用的資產權限時，剩下的只有不需要對方的路：另建資產、換掉網站上的 ID，以及 Meta 的管理員爭議與 GA4 的 <code>analytics.txt</code> 驗證這兩條由公司自己提出的復原途徑；管理員爭議要從與組合有關聯的個人檔案送件，公司的人從來不是那個組合的成員時這條路可能走不通，<code>analytics.txt</code> 要放在帳戶裡每一個送資料的網站上，所以資源放在代理商、帳戶裡還有其他客戶網站時，<code>analytics.txt</code> 這條路用不上（兩者皆依送件條件推出）。對方不只不配合、還握著資產的權限時（盜用帳號的人、與公司起爭議的代理商），對方能繼續收到網站送出的資料，透過 GTM 容器的發布權限還能在網站上執行程式碼，所以先切斷網站對那個資產的引用：拿掉頁面上屬於對方帳戶的 Pixel 與容器片段，再談移交或另建（由權限設計推出）。</p>
<p><strong>換代理商時</strong>，資產若一開始就建在公司的組合與帳戶底下，要處理的只有權限：公司撤銷舊代理商的合作夥伴分享與成員身分，再把 Meta 資料集與廣告帳號以合作夥伴的方式分享給新代理商的組合，把 GTM 與 GA4 的成員權限給新代理商的人員。撤銷之前先列出舊代理商在公司資產上建立的整合（由權限設計推出）：GTM 容器裡指向代理商自己帳戶的代碼、GA4 連結的代理商 Google Ads 帳戶、Meta 上由代理商系統發出的 Conversions API 存取權杖。這些整合不會因為撤銷權限而消失：指向代理商帳戶的代碼與產品連結會繼續把資料送給代理商；依賴代理商發出的存取權杖的伺服器端事件，可能在撤銷後停止運作。</p>
<p>資產建在舊代理商的組合或帳戶底下時，網站上載入的 ID 是處理的起點。網站上可能同時留著舊代理商建立的 ID，以及更早之前的代理商或員工留下的 ID，而追蹤碼依 ID 送事件：留著的舊 Pixel ID 會讓事件繼續送進舊代理商的資料集，新的資料集也照常收事件，網站畫面與新報表都不會顯示異常；留著的舊 GTM 容器片段更嚴重，舊代理商仍持有那個容器的發布權限，等於仍能在公司網站上執行程式碼。所以要先查清每個網站實際載入的 Pixel ID、容器 ID 與評估 ID（查法見 <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>，或在瀏覽器開發者工具的 Network 面板篩選 Meta 與 Google 的網域），再依各家處理：</p>
<ul>
<li><strong>Meta</strong>：Meta 資料集無法轉給另一個組合，公司在自己的組合建新資料集、把網站上的 Pixel ID 換掉。查看舊資料的管道只能由擁有舊資料集的組合開放，合作結束後能不能開放只取決於舊代理商是否配合，所以要在換掉 Pixel ID 之前請舊代理商以合作夥伴的方式把舊資料集分享給公司。Pixel 建在舊代理商某一位人員自己的廣告帳號底下時，那個廣告帳號若不屬於任何組合，公司可以提出認領，由那位人員批准之後進入公司的組合；Meta 的說明頁沒有寫 Pixel 會不會隨廣告帳號一起進來，認領之後要在公司組合的資料集清單裡確認。那位人員不批准、舊代理商與那位人員都聯絡不上，或認領之後 Pixel 不在清單裡時，公司另建新資料集、換掉 Pixel ID，舊的歷史事件拿不回來。</li>
<li><strong>GTM</strong>：公司請舊代理商把容器匯出成 JSON 檔，由公司的人匯入公司帳戶裡的新容器，再換掉網站上的容器片段。舊代理商的帳戶專為這家公司建立、帳戶裡沒有其他客戶的容器時，也可以換人移交（由權限設計推出）：舊代理商把公司的人加成那個帳戶的管理員，再移除代理商的人，帳戶整個移交、容器 ID 不變。舊代理商的帳戶放著多個客戶的容器時，換人移交會把其他客戶的容器一起交出去，所以只有匯出匯入一途。舊代理商聯絡不上或不配合時，公司依網站上實際執行的追蹤碼（Network 面板裡看得到的 Pixel ID、評估 ID 與各家請求）在自己的帳戶建一個新容器，重新設定代碼，再換掉容器片段；舊容器的設定拿不回來。</li>
<li><strong>GA4</strong>：GA4 資源（property）的搬移受 Google Marketing Platform 機構（帳戶之上的管理單位）限制，說明頁寫明擋下的情形是兩個帳戶分屬不同機構，以及只有目的帳戶屬於機構而來源帳戶不屬於；執行的人在兩個帳戶都要同時是管理員與編輯者。依兩個帳戶目前與機構的關係：
<ul>
<li>兩個帳戶屬於同一個機構：資源可以直接搬到公司的帳戶，評估 ID（Measurement ID）不變。</li>
<li>只有公司的帳戶屬於機構：公司是目的帳戶，這正是說明頁擋下的狀態，所以要先由公司所屬機構的管理員把舊代理商的帳戶連結進公司的機構，兩個帳戶屬於同一個機構之後再搬；連結這一步要舊代理商配合。</li>
<li>只有舊代理商的帳戶屬於機構：Google 的說明頁沒有寫這一種情形，本篇沒有驗證能不能直接搬。說明頁寫明可以搬的是兩個帳戶屬於同一個機構的情形，所以符合說明頁條件的做法是請舊代理商機構的管理員把公司的帳戶連結進同一個機構再搬，這要舊代理商配合（依說明頁的條件推出）。搬完之後公司的帳戶仍在舊代理商的機構底下，能不能移出、機構管理員還保有哪些權限，本篇沒有查證（見〈追蹤資產換擁有者的條件〉末段）。</li>
<li>兩個帳戶分屬不同機構：Google 的說明頁寫明不能搬。</li>
<li>兩個帳戶都不屬於任何機構：說明頁沒有寫是否能直接搬。符合說明頁條件的做法是公司建立機構、把兩個帳戶連結進同一個機構再搬；舊代理商的帳戶專為這家公司建立時，也可以把整個帳戶換人移交（由權限設計推出）。</li>
<li>以上各種情形都搬不了，或需要配合的舊代理商聯絡不上、不配合時（任一成立）：公司另建資源、換掉網站上的評估 ID，歷史報表留在舊帳戶。</li>
</ul>
</li>
</ul>
<p><strong>事業轉手時</strong>，買方要的是整套資產連同歷史資料。Meta、GTM 與 GA4 三家都能用換人的方式移交最上層的擁有者（Meta 的組合、GTM 帳戶、GA4 帳戶；由權限設計推出）：賣方把買方指定的人加成組合的完整存取權限，以及 GTM 帳戶與 GA4 帳戶的管理員，再移除賣方的人。在移除賣方的人之前，要先由買方確認自己登得進去、管理得了，因為移除之後賣方已經沒有權限補救；買方這時才發現自己管理不了的話，Meta 只剩管理員爭議、GA4 只剩 <code>analytics.txt</code> 驗證、GTM 沒有復原途徑。這條路徑底下的 Pixel ID、容器 ID、評估 ID 與歷史資料都不動，前提是三件事先處理好（理由見〈追蹤資產換擁有者的條件〉一節末段）：組合裡沒有別的組合分享進來的資產、廣告帳號的付款方式改成買方的、GA4 與 GTM 帳戶不在賣方的機構底下。</p>
<p>賣方的組合或帳戶裡如果還放著與這個事業無關的資產，整個組合或帳戶不能直接交出去。GA4 與 GTM 還有一條路：賣方先把無關的 GA4 資源搬到賣方另一個帳戶、把無關的 GTM 容器匯出匯入到賣方的新帳戶（那些無關容器原本所在的賣方網站要換成新容器 ID，買方的網站不受影響），帳戶裡只剩這個事業的資產之後再整個移交，買方手上的評估 ID 與容器 ID 都不動（由權限設計推出）。Meta 的組合做不到這一步，因為資料集與組合內建立的廣告帳號搬不出去，只能逐項處理；GA4 與 GTM 不走上面那條路時也一樣逐項處理：粉絲專頁移到買方的組合；Meta 資料集只能由賣方以合作夥伴的方式分享給買方，或由買方另建，組合內建立的廣告帳號也只能由賣方分享給買方或由買方另建；GA4 資源依換代理商那一段 GA4 條列的機構條件搬到買方的帳戶，買賣雙方分屬不同機構時搬不了，只能由買方另建；GTM 容器匯出匯入。</p>
<p>所以一個組合或帳戶只放一個事業的資產，轉手時才有整包移交這個選項。Google 說一家公司通常只需要一個 GTM 帳戶，本篇認為這適用於只經營一個事業的公司；同一家公司經營日後可能分拆出售的事業時，按事業各開一個組合與帳戶。</p>
<h2 id="本站實測資產的擁有者">本站實測資產的擁有者</h2>
<p>本站為 <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> 建立的測試資產，套用的是同一個原則：一個擁有者只放一個用途的資產。Meta 另開一個組合、GTM 與 GA4 各另開一個帳戶，裡面只放實測用的 Meta 資料集、GTM 容器與 GA4 資源，不放進登入帳號原本就有的組合或帳戶；實測結束時，GTM 與 GA4 的帳戶可以整個刪除（刪除後分別有 30 天與 35 天可以從垃圾桶救回），Meta 資料集依規則刪不掉，會一直留在這個只放實測資產的組合裡，不會混進其他用途的組合。建立過程中三家各自的檢查與預設值（Meta 的組合名稱審查、資料集表單預設勾選的 Conversions API 選項、GA4 預設勾選的資料共用選項），以及這些資產上做的擷取，記錄在 <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> 的測試帳號一節與各次結果。</p>
<h2 id="延伸閱讀資產建立之後的資料路徑與部署權限">延伸閱讀：資產建立之後的資料路徑與部署權限</h2>
<p>這篇處理的是資產屬於誰；資產建立之後，事件怎麼從網站送進那個 Meta 資料集或 GA4 資源，由各家的機制文說明。Pixel 的瀏覽器端與伺服器端兩條路徑，以及兩條路徑送出同一個事件時靠 <code>event_id</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>；代理商取得 GTM 容器的發布權限之後能在網站上做什麼、發布前怎麼驗證，見 <a href="/blog/devops/monitoring/vendors/google-tag-manager/" data-link-title="Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理" data-link-desc="GTM 容器在頁面上承擔的部署責任、資料層作為前端與行銷之間的事件約定、預覽模式與版本發布的驗證方式、自訂 HTML 代碼帶來的安全與效能成本，以及伺服器端代碼管理改變的資料路徑">Google Tag Manager</a> 的預覽模式與自訂 HTML 兩節。</p>
]]></content:encoded></item></channel></rss>