<?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>Tracking on Tarragon</title><link>https://tarrragon.github.io/blog/tags/tracking/</link><description>Recent content in Tracking 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/tracking/index.xml" rel="self" type="application/rss+xml"/><item><title>Meta Pixel 與 Conversions API：廣告平台轉換追蹤的瀏覽器端與伺服器端</title><link>https://tarrragon.github.io/blog/devops/monitoring/vendors/meta-pixel-conversions-api/</link><pubDate>Thu, 22 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/monitoring/vendors/meta-pixel-conversions-api/</guid><description>&lt;p>這篇整理 Meta（Facebook / Instagram 廣告）的網站轉換追蹤：瀏覽器端的 Meta Pixel、伺服器端的 Conversions API（常簡稱 CAPI），以及兩條路徑並用時的去重與驗證。範圍是網站事件；App 端的事件走 Meta 的 App SDK，不在這篇。欄位規格以 Meta 開發者文件為準，本篇引用的規則（必填欄位、去重的時間窗、&lt;code>event_time&lt;/code> 的上限）都會隨 Meta 改版調整，接入時回官方文件核對一次。&lt;/p>
&lt;h2 id="廣告平台轉換追蹤的資料消費者">廣告平台轉換追蹤的資料消費者&lt;/h2>
&lt;p>Meta Pixel 送出的事件，主要的消費者是 Meta 的廣告投放系統：投放演算法依轉換事件學習該把廣告給誰看，廣告後台依它計算每一檔廣告帶來幾筆轉換。這一點讓它跟 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/06-commercial-comparison/mixpanel-amplitude/" data-link-title="Mixpanel / Amplitude" data-link-desc="行為分析專用方案 vs 通用監控的差異 — Mixpanel 和 Amplitude 的 funnel / cohort / retention 分析能力">Mixpanel / Amplitude&lt;/a> 這類行為分析工具分開——行為分析的消費者是自己的產品團隊，事件怎麼命名、帶什麼屬性由自己決定；廣告平台的事件要讓平台的演算法讀得懂，所以命名照平台的規定。&lt;/p>
&lt;p>Meta 定義了一組標準事件（standard events）：&lt;code>PageView&lt;/code>、&lt;code>ViewContent&lt;/code>、&lt;code>AddToCart&lt;/code>、&lt;code>InitiateCheckout&lt;/code>、&lt;code>Purchase&lt;/code>、&lt;code>Lead&lt;/code>、&lt;code>CompleteRegistration&lt;/code> 等。廣告活動的最佳化目標綁在這些事件名稱上，例如以「購買」為目標的廣告活動讀的是 &lt;code>Purchase&lt;/code>；&lt;code>Purchase&lt;/code> 另外要帶 &lt;code>value&lt;/code> 與 &lt;code>currency&lt;/code>，平台才算得出廣告投資報酬。自己的事件命名（&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;code>namespace.action&lt;/code>）因此需要一層對映：&lt;code>checkout.complete&lt;/code> 送到自己的 collector，同一筆在送往 Meta 時改名成 &lt;code>Purchase&lt;/code>。對映表寫在一個地方、由工程維護，比散在各個埋點呼叫裡改名好查。&lt;/p>
&lt;p>廣告平台各自用自己看得到的事件做歸因，同一筆訂單可能同時被 Meta 與 Google Ads 算成自己帶來的轉換，兩個後台的轉換數加總會大於實際訂單數。跨渠道的歸因模型見 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/08-business-analytics/attribution/" data-link-title="Attribution" data-link-desc="使用者從哪來、哪個渠道帶來轉換 — last-touch / first-touch / multi-touch 歸因模型的差異和選擇">Attribution&lt;/a>。&lt;/p>
&lt;h2 id="meta-pixel-在瀏覽器裡做的事">Meta Pixel 在瀏覽器裡做的事&lt;/h2>
&lt;p>Pixel 的基本碼從 &lt;code>connect.facebook.net&lt;/code> 載入 &lt;code>fbevents.js&lt;/code>，再用 &lt;code>fbq()&lt;/code> 這個全域函式初始化並送事件。&lt;code>fbevents.js&lt;/code> 載入後會再向 Meta 取得這個 Pixel 的設定檔，設定檔載入的模組（plugin，&lt;code>fbevents.js&lt;/code> 依 Pixel 設定檔載入的程式片段）會在網站沒有呼叫 &lt;code>fbq()&lt;/code> 的情況下&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">自己送事件&lt;/a>，例如依按鈕點擊送出的 &lt;code>SubscribedButtonClick&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> 的 Meta Pixel 有視窗那一次。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&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">// Meta 提供的載入片段（略）：定義 fbq 並從 connect.facebook.net 載入 fbevents.js
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">fbq&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;init&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;PIXEL_ID&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// PIXEL_ID 換成 Events Manager 裡那個 Pixel 的 ID
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">fbq&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;track&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;PageView&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 每一頁載入時送一次
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>fbq('init', ...)&lt;/code> 的第二個引數是 Pixel ID，每個 Pixel 在 Meta Events Manager 裡有自己的一串數字，一個網站可以初始化多個 Pixel。&lt;code>PIXEL_ID&lt;/code> 在這裡是佔位值，要換成自己 Events Manager 裡顯示的那一串。&lt;/p>
&lt;p>送轉換事件時，&lt;code>fbq('track', ...)&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">// 第二個引數是標準事件名稱，第三個是事件參數，第四個帶這一筆事件的唯一 ID
&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="nx">fbq&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;track&amp;#39;&lt;/span>&lt;span class="p">,&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">3&lt;/span>&lt;span class="cl"> &lt;span class="p">{&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 class="nx">currency&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;TWD&amp;#39;&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="p">{&lt;/span> &lt;span class="nx">eventID&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>value&lt;/code> 與 &lt;code>currency&lt;/code> 是這一筆購買的金額與幣別，從訂單資料取值。&lt;code>eventID&lt;/code> 是這一筆事件的識別碼，只送 Pixel 的時候可以省略；同一筆事件還要從伺服器再送一次時就必須帶，原因在〈兩路並送時的去重規則〉一節。&lt;/p>
&lt;p>Pixel 也在網站自己的網域上寫 cookie：&lt;code>_fbp&lt;/code> 記錄這個瀏覽器的識別碼；使用者從廣告點進來時，落地網址會帶 &lt;code>fbclid&lt;/code> 參數，Pixel 把它存成 &lt;code>_fbc&lt;/code>。這兩個 cookie 是&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>（寫在網站自己的網域上），之後伺服器端送事件時會用到它們的值。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理 Meta（Facebook / Instagram 廣告）的網站轉換追蹤：瀏覽器端的 Meta Pixel、伺服器端的 Conversions API（常簡稱 CAPI），以及兩條路徑並用時的去重與驗證。範圍是網站事件；App 端的事件走 Meta 的 App SDK，不在這篇。欄位規格以 Meta 開發者文件為準，本篇引用的規則（必填欄位、去重的時間窗、<code>event_time</code> 的上限）都會隨 Meta 改版調整，接入時回官方文件核對一次。</p>
<h2 id="廣告平台轉換追蹤的資料消費者">廣告平台轉換追蹤的資料消費者</h2>
<p>Meta Pixel 送出的事件，主要的消費者是 Meta 的廣告投放系統：投放演算法依轉換事件學習該把廣告給誰看，廣告後台依它計算每一檔廣告帶來幾筆轉換。這一點讓它跟 <a href="/blog/devops/monitoring/06-commercial-comparison/mixpanel-amplitude/" data-link-title="Mixpanel / Amplitude" data-link-desc="行為分析專用方案 vs 通用監控的差異 — Mixpanel 和 Amplitude 的 funnel / cohort / retention 分析能力">Mixpanel / Amplitude</a> 這類行為分析工具分開——行為分析的消費者是自己的產品團隊，事件怎麼命名、帶什麼屬性由自己決定；廣告平台的事件要讓平台的演算法讀得懂，所以命名照平台的規定。</p>
<p>Meta 定義了一組標準事件（standard events）：<code>PageView</code>、<code>ViewContent</code>、<code>AddToCart</code>、<code>InitiateCheckout</code>、<code>Purchase</code>、<code>Lead</code>、<code>CompleteRegistration</code> 等。廣告活動的最佳化目標綁在這些事件名稱上，例如以「購買」為目標的廣告活動讀的是 <code>Purchase</code>；<code>Purchase</code> 另外要帶 <code>value</code> 與 <code>currency</code>，平台才算得出廣告投資報酬。自己的事件命名（<a href="/blog/devops/monitoring/01-mental-model/event-naming-convention/" data-link-title="事件命名規範" data-link-desc="namespace.action 格式的事件命名、命名一致性的工程價值、和商業方案命名慣例的對應">事件命名規範</a> 的 <code>namespace.action</code>）因此需要一層對映：<code>checkout.complete</code> 送到自己的 collector，同一筆在送往 Meta 時改名成 <code>Purchase</code>。對映表寫在一個地方、由工程維護，比散在各個埋點呼叫裡改名好查。</p>
<p>廣告平台各自用自己看得到的事件做歸因，同一筆訂單可能同時被 Meta 與 Google Ads 算成自己帶來的轉換，兩個後台的轉換數加總會大於實際訂單數。跨渠道的歸因模型見 <a href="/blog/devops/monitoring/08-business-analytics/attribution/" data-link-title="Attribution" data-link-desc="使用者從哪來、哪個渠道帶來轉換 — last-touch / first-touch / multi-touch 歸因模型的差異和選擇">Attribution</a>。</p>
<h2 id="meta-pixel-在瀏覽器裡做的事">Meta Pixel 在瀏覽器裡做的事</h2>
<p>Pixel 的基本碼從 <code>connect.facebook.net</code> 載入 <code>fbevents.js</code>，再用 <code>fbq()</code> 這個全域函式初始化並送事件。<code>fbevents.js</code> 載入後會再向 Meta 取得這個 Pixel 的設定檔，設定檔載入的模組（plugin，<code>fbevents.js</code> 依 Pixel 設定檔載入的程式片段）會在網站沒有呼叫 <code>fbq()</code> 的情況下<a href="/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">自己送事件</a>，例如依按鈕點擊送出的 <code>SubscribedButtonClick</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> 的 Meta Pixel 有視窗那一次。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// Meta 提供的載入片段（略）：定義 fbq 並從 connect.facebook.net 載入 fbevents.js
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">fbq</span><span class="p">(</span><span class="s1">&#39;init&#39;</span><span class="p">,</span> <span class="s1">&#39;PIXEL_ID&#39;</span><span class="p">);</span>   <span class="c1">// PIXEL_ID 換成 Events Manager 裡那個 Pixel 的 ID
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">fbq</span><span class="p">(</span><span class="s1">&#39;track&#39;</span><span class="p">,</span> <span class="s1">&#39;PageView&#39;</span><span class="p">);</span>  <span class="c1">// 每一頁載入時送一次
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span></span></span></code></pre></div><p><code>fbq('init', ...)</code> 的第二個引數是 Pixel ID，每個 Pixel 在 Meta Events Manager 裡有自己的一串數字，一個網站可以初始化多個 Pixel。<code>PIXEL_ID</code> 在這裡是佔位值，要換成自己 Events Manager 裡顯示的那一串。</p>
<p>送轉換事件時，<code>fbq('track', ...)</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">// 第二個引數是標準事件名稱，第三個是事件參數，第四個帶這一筆事件的唯一 ID
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">fbq</span><span class="p">(</span><span class="s1">&#39;track&#39;</span><span class="p">,</span> <span class="s1">&#39;Purchase&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">{</span> <span class="nx">value</span><span class="o">:</span> <span class="mi">1280</span><span class="p">,</span> <span class="nx">currency</span><span class="o">:</span> <span class="s1">&#39;TWD&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="p">{</span> <span class="nx">eventID</span><span class="o">:</span> <span class="s1">&#39;order-20261022-0042&#39;</span> <span class="p">});</span></span></span></code></pre></div><p><code>value</code> 與 <code>currency</code> 是這一筆購買的金額與幣別，從訂單資料取值。<code>eventID</code> 是這一筆事件的識別碼，只送 Pixel 的時候可以省略；同一筆事件還要從伺服器再送一次時就必須帶，原因在〈兩路並送時的去重規則〉一節。</p>
<p>Pixel 也在網站自己的網域上寫 cookie：<code>_fbp</code> 記錄這個瀏覽器的識別碼；使用者從廣告點進來時，落地網址會帶 <code>fbclid</code> 參數，Pixel 把它存成 <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>（寫在網站自己的網域上），之後伺服器端送事件時會用到它們的值。</p>
<h2 id="瀏覽器端事件的損失路徑">瀏覽器端事件的損失路徑</h2>
<p>Pixel 的事件從讀者的瀏覽器發出，所以它會在讀者端被擋下，而網站這一端收不到任何錯誤：</p>
<ul>
<li><strong>廣告攔截器擋下 script 本身</strong>：攔截清單收錄 <code>connect.facebook.net</code> 這類已知追蹤網域，<code>fbevents.js</code> 載不進來，<code>fbq()</code> 之後的所有事件都不會送出。</li>
<li><strong>瀏覽器的追蹤防護縮短 cookie 壽命</strong>：Safari 的 Intelligent Tracking Prevention（ITP）會限制由 JavaScript 寫入的第一方 cookie 的有效期，<code>_fbp</code> 因此比預期更早消失：一般情形是使用者 7 天沒有互動之後刪除；從 Meta 廣告帶著 <code>fbclid</code> 進站那一次在落地頁寫下的 cookie，上限只有 24 小時，所以同一個人點了廣告、過了一天以上才回來購買時，就可能被當成另一個瀏覽器（期限規則見 <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>）；部分瀏覽器與 App 內開啟的連結也會移除網址裡的 <code>fbclid</code>。</li>
<li><strong>同意聲明還沒取得</strong>：在要求事先同意的法域（例如 GDPR 下的歐盟讀者），網站在使用者同意之前不能載入 Pixel，這段期間的事件依規定就不送。</li>
<li><strong>自動化與機器人流量過濾</strong>：實測的這個 Pixel，設定檔裡的模組依 User-Agent 判斷機器人流量（模組由 Meta 依各 Pixel 的設定檔下發），<a href="/blog/devops/monitoring/knowledge-cards/headless-browser/" data-link-title="Headless Browser（無頭瀏覽器）" data-link-desc="用 Playwright 或 Puppeteer 測追蹤碼、分析報表裡的機器流量，或自建統計與商業追蹤碼的訪問數對不上時">無頭瀏覽器</a>（User-Agent 帶 <code>HeadlessChrome</code>）送出的事件在瀏覽器裡就被攔下。<code>fbevents.js</code> 照常載入、cookie 照常寫入，網站這一端看不出事件沒送；用自動化工具驗證追蹤設定時會撞上這一條，實測見 <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>跳頁中斷</strong>：購買按鈕按下後立刻導向感謝頁，送事件的請求可能還沒送完頁面就被卸載。這是所有前端事件追蹤共通的狀況，機制與 <code>sendBeacon</code> 的解法見 <a href="/blog/devops/monitoring/05-platform-adaptation/js-ts-platform/" data-link-title="JS/TS 平台適配" data-link-desc="CORS 限制、Service Worker 攔截、SPA 路由變換偵測 — 瀏覽器環境中 SDK 需要處理的平台特殊問題">JS/TS 平台適配</a>。</li>
</ul>
<p>每個網站被擋下的比例取決於讀者組成（裝攔截器的比例、iOS 的比例、歐盟流量的比例），網路上流傳的百分比是別人網站的數字。量自己網站的方式是對帳：同一段期間內，訂單資料庫裡的訂單數與 Events Manager 收到的 <code>Purchase</code> 數相比，差距就是這個網站的瀏覽器端損失。</p>
<h2 id="conversions-api從伺服器送同一批事件">Conversions API：從伺服器送同一批事件</h2>
<p>Conversions API 讓網站的伺服器直接把事件 POST 到 Meta 的 Graph API（<code>/&lt;PIXEL_ID&gt;/events</code>），請求不經過讀者的瀏覽器，所以廣告攔截器與 cookie 期限都影響不到它。代價是伺服器要自己準備瀏覽器原本會自動附上的資訊：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">[{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nt">&#34;event_name&#34;</span><span class="p">:</span> <span class="s2">&#34;Purchase&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nt">&#34;event_time&#34;</span><span class="p">:</span> <span class="mi">1792656000</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nt">&#34;event_id&#34;</span><span class="p">:</span> <span class="s2">&#34;order-20261022-0042&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nt">&#34;action_source&#34;</span><span class="p">:</span> <span class="s2">&#34;website&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nt">&#34;event_source_url&#34;</span><span class="p">:</span> <span class="s2">&#34;https://shop.example.com/checkout/thanks&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nt">&#34;user_data&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="nt">&#34;client_user_agent&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;下單時瀏覽器的 User-Agent&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="nt">&#34;client_ip_address&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;下單時的來源 IP&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="nt">&#34;fbp&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;讀自 _fbp cookie&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">      <span class="nt">&#34;fbc&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;讀自 _fbc cookie&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">      <span class="nt">&#34;em&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;&lt;正規化後的 email 的 SHA-256&gt;&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nt">&#34;custom_data&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;value&#34;</span><span class="p">:</span> <span class="mi">1280</span><span class="p">,</span> <span class="nt">&#34;currency&#34;</span><span class="p">:</span> <span class="s2">&#34;TWD&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="p">}]</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>欄位的來源與限制：</p>
<ul>
<li><code>event_name</code>、<code>event_id</code>、<code>custom_data</code> 與 Pixel 那一筆相同，名稱的對映方式見〈廣告平台轉換追蹤的資料消費者〉一節。</li>
<li><code>event_time</code> 是事件發生的 Unix 時間（秒）。Meta 接受的範圍是送出當下往前七天內；一批資料裡只要有一筆超過七天，整個請求會被拒絕、一筆都不處理，所以用排程補送積壓的事件時要先過濾掉過期的那些。</li>
<li><code>action_source</code> 標明事件發生在哪裡，網站事件填 <code>website</code>。Meta 對網站事件另外要求 <code>event_source_url</code>（事件發生的頁面網址，網域要與 Meta 後台驗證過的網域一致）與 <code>client_user_agent</code>。</li>
<li><code>user_data</code> 是 Meta 用來把這筆事件對回某個 Meta 使用者的資料。<code>fbp</code>、<code>fbc</code> 從請求帶上來的 cookie 讀，<code>client_ip_address</code> 與 <code>client_user_agent</code> 從下單那一次請求的 header 讀，這四個欄位原樣送出；email、電話這類個人資料要先依 Meta 規定的方式正規化（去空白、轉小寫等），再做 SHA-256 雜湊才送。給的欄位越多，Events Manager 顯示的事件比對品質（Event Match Quality）越高。</li>
</ul>
<p>請求要帶 Meta 發給這個 Pixel 的 access token，這個 token 只能放在伺服器上：放進前端程式碼等於公開，任何人都能用它往這個 Pixel 灌假事件。前端程式碼裡的存取憑證會被提取的機制見 <a href="/blog/devops/monitoring/07-security-privacy/client-sdk-authentication/" data-link-title="Client-side SDK 認證的根本限制" data-link-desc="嵌在 client 端的 credential 必然可被提取 — 認清 architecture 天花板後的多層緩解策略，從 origin 驗證到 device attestation">Client-side SDK 認證的根本限制</a>。<code>user_data</code> 裡雜湊過的 email 仍然是可以對回個人的資料，送出前的同意與用途說明照 <a href="/blog/devops/monitoring/07-security-privacy/gdpr-minimization/" data-link-title="GDPR 最小化原則的工程落地" data-link-desc="資料最小化、目的限制、儲存限制 — GDPR 三個核心原則在監控系統的工程實作方式">GDPR 最小化原則的工程落地</a> 的原則處理。</p>
<h2 id="兩路並送時的去重規則">兩路並送時的去重規則</h2>
<p>Meta 建議 Pixel 與 Conversions API 並用：Pixel 送得到的時候帶著完整的瀏覽器資訊，送不到的那些由伺服器補上。兩條路徑都送成功的事件會被 Meta 收到兩次，所以 Meta 用兩個欄位判斷是不是同一筆：Pixel 的 <code>eventID</code> 與伺服器的 <code>event_id</code> 相同、且兩邊的事件名稱相同。依 Meta 文件，去重只在同一個 <code>event_id</code> 第一次送達後的 48 小時內生效；兩筆幾乎同時抵達時 Meta 保留瀏覽器那一筆；48 小時內沒有收到瀏覽器那一筆時，伺服器那一筆照常計入。</p>
<p>這條規則決定了 <code>event_id</code> 要在哪裡產生：在事件的源頭產生一次，再同時交給前端與後端。訂單編號符合這個條件，前後端都拿得到同一個值：感謝頁從訂單資料讀出編號放進 <code>fbq</code> 的第四個引數，伺服器在建立訂單時用同一個編號送 Conversions API。兩邊各自產生一個隨機 UUID 是常見的錯法，兩個值永遠對不上，每一筆轉換都會被算兩次，廣告後台的轉換數膨脹而沒有任何錯誤訊息。</p>
<p>Meta 另有一種用 <code>event_name</code> 加 <code>fbp</code> 或 <code>external_id</code> 比對的去重方式，文件註明它通常只在瀏覽器那一筆先到、伺服器那一筆後到的順序下有效，所以只當作 <code>event_id</code> 的備援。</p>
<p>去重的道理與自架 collector 處理 SDK 重送是同一件事：重送的事件要靠一個在源頭就決定的 ID 才認得出來，見 <a href="/blog/devops/monitoring/04-collector/data-integrity/" data-link-title="端到端資料完整性" data-link-desc="從 SDK 到 storage 的資料損失地圖 — 每個環節的損失類型、控制策略、完整性指標、被自己 SDK DDoS 的防護">端到端資料完整性</a>。</p>
<h2 id="用-test-events-驗證送出的事件">用 Test Events 驗證送出的事件</h2>
<p>Events Manager 在每個 Pixel 底下有 Test Events 頁面，它即時列出這個 Pixel 收到的事件、來源是瀏覽器還是伺服器、以及去重的結果。兩條路徑的驗證方式不同：</p>
<ul>
<li><strong>瀏覽器端</strong>：在 Test Events 頁面輸入網站網址開啟，或在瀏覽器裝 Meta Pixel Helper 擴充功能，操作一次購買流程，確認 <code>Purchase</code> 出現且帶著 <code>value</code>、<code>currency</code> 與 <code>eventID</code>。</li>
<li><strong>伺服器端</strong>：Test Events 頁面會給一組測試代碼，伺服器送 Conversions API 時在請求裡加上 <code>test_event_code</code> 欄位，那批事件就會出現在測試視窗裡。正式上線前要把這個欄位拿掉；帶測試代碼的事件會不會計入正式報表，以 Events Manager 當下的說明為準。</li>
</ul>
<p>驗證去重時兩條路徑一起跑：同一筆測試訂單在 Test Events 裡應該看到瀏覽器與伺服器各一筆，並標示其中一筆被去重。只看到一筆時，先確認另一條路徑確實沒送，還是 <code>event_id</code> 對不上而被當成兩筆不同的事件。</p>
<h2 id="和自架事件追蹤並存的資料流">和自架事件追蹤並存的資料流</h2>
<p>接了 Pixel 與 Conversions API 之後，同一個業務事件（一筆購買）會往三個地方送：自己的 collector、Meta 的瀏覽器端、Meta 的伺服器端。業務事件的定義只寫一次，送往各家時各自做名稱與欄位的對映，比在各個埋點位置分別呼叫三家的 API 好維護，改一個欄位不必找遍三種呼叫。這個分流結構與它的保留期、PII 取捨，在 <a href="/blog/devops/monitoring/telemetry-data-dual-use/" data-link-title="監控資料的雙重用途：行為分析與訊號治理" data-link-desc="同一份 event data 如何同時服務行為分析（funnel / cohort / attribution）和訊號治理（cardinality / cost / signal governance）— 格式交叉、治理衝突與分流架構">監控資料的雙重用途</a> 有完整的推導。</p>
<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> 按按鈕觸發、對照它實際送出的請求與 cookie。追蹤碼要由誰、用什麼流程放上網站，是另一層的決定。行銷團隊常用 tag manager 自行加 Pixel，那條路徑的約定與治理見 <a href="/blog/devops/monitoring/vendors/google-tag-manager/" data-link-title="Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理" data-link-desc="GTM 容器在頁面上承擔的部署責任、資料層作為前端與行銷之間的事件約定、預覽模式與版本發布的驗證方式、自訂 HTML 代碼帶來的安全與效能成本，以及伺服器端代碼管理改變的資料路徑">Google Tag Manager</a>。Pixel 的資料集屬於哪一個商家資產管理組合、建立之後能不能轉給另一個組合，以及委託代理商時資料集該建在誰的組合裡，見 <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/</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>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>First-party Cookie（第一方 cookie）</title><link>https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/first-party-cookie/</link><pubDate>Fri, 23 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/first-party-cookie/</guid><description>&lt;p>第一方 cookie（first-party cookie）是寫在使用者正在瀏覽的那個網站網域上的 cookie；寫在別的網域上、隨對那個網域的請求送出的，是第三方 cookie（third-party cookie）。歸屬只看 cookie 的網域，不看寫入它的 script 是誰的：Meta Pixel 與 GA4 的 script 從 Meta 與 Google 的網域載入，卻用 JavaScript 把識別碼寫在網站自己的網域上，所以 &lt;code>_fbp&lt;/code>、&lt;code>_fbc&lt;/code>、&lt;code>_ga&lt;/code> 都是第一方 cookie。可先對照 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/headless-browser/" data-link-title="Headless Browser（無頭瀏覽器）" data-link-desc="用 Playwright 或 Puppeteer 測追蹤碼、分析報表裡的機器流量，或自建統計與商業追蹤碼的訪問數對不上時">Headless Browser（無頭瀏覽器）&lt;/a>（同一批追蹤碼在無頭模式下照樣寫 cookie、事件卻可能不送）與 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">Autocapture（自動事件收集）&lt;/a>（這些識別碼會隨自動事件一起送出）；識別碼與瀏覽器指紋的差別見 &lt;a href="https://tarrragon.github.io/blog/automation/knowledge-cards/browser-fingerprint/" data-link-title="Browser Fingerprint（瀏覽器指紋）" data-link-desc="由多個單獨無害的瀏覽器環境屬性組合而成的裝置特徵集合，決定自建統計蒐集判別訊號時的能力上限">Browser Fingerprint（瀏覽器指紋）&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>第一方 cookie 是追蹤碼在第三方 cookie 被瀏覽器限制之後保存識別碼的位置。第三方 cookie 由 vendor 的網域直接讀寫，第一方 cookie 寫在網站網域上，瀏覽器送往 vendor 網域的請求不會在 Cookie 標頭裡帶上網站網域的 cookie，所以追蹤碼在瀏覽器裡讀出它的值，放進事件請求的參數送出：在預設設定下（使用者沒有透過同意機制拒絕、追蹤碼的第一方 cookie 設定沒有關掉），Meta Pixel 的每一筆事件都帶 &lt;code>fbp&lt;/code> 參數，值就是 &lt;code>_fbp&lt;/code> 的內容；GA4 的 collect 請求帶 &lt;code>cid&lt;/code>，與 &lt;code>_ga&lt;/code> 值以點分隔的最後兩段數字相同。網站自己沒有呼叫的&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">自動事件&lt;/a>也帶著這些參數送出。&lt;/p>
&lt;h2 id="cookie-期限的設定者與瀏覽器上限">cookie 期限的設定者與瀏覽器上限&lt;/h2>
&lt;p>cookie 的到期時間由寫入它的 script 設定，再受瀏覽器的規則截斷。Chrome 從 104 版起把任何 cookie 的到期時間上限訂為 400 天，要求更久的 cookie 會被改成 400 天。Safari 的 Intelligent Tracking Prevention（ITP）對追蹤碼常用的寫法另有三條限制，依 WebKit 的追蹤防護說明頁：由 JavaScript 寫入的 cookie 與其他 script 寫得到的儲存資料（例如 localStorage），在使用者 7 天沒有與這個網站互動之後刪除（WebKit 先前的說明以使用 Safari 的天數計，現行說明頁沒有寫單位），重新起算的條件是互動、不是單純的造訪；ITP 偵測到網址帶著點擊 ID 這類參數（link decoration）時，落地頁上由 JavaScript 寫入的 cookie 期限上限是 24 小時（WebKit 先前的說明寫明的條件是：把使用者導過來的網域被分類為追蹤者，而且網址帶查詢字串或片段）；回應來自偽裝成第一方、實際指向第三方的伺服器（CNAME 或第三方 IP 偽裝）時，那個 HTTP 回應寫入的 cookie 期限上限是 7 天。所以同一個追蹤碼在不同瀏覽器上的 cookie 壽命不同：GA4 代碼替 &lt;code>_ga&lt;/code> 與 &lt;code>_ga_&amp;lt;評估 ID&amp;gt;&lt;/code> 設的預設期限是兩年，在 Chromium 上實測看到的 400 天是瀏覽器的上限，不是 GA4 的設定；從 Meta 廣告帶著 &lt;code>fbclid&lt;/code> 點進來的那一次，&lt;code>_fbc&lt;/code> 與 &lt;code>_fbp&lt;/code> 正好在落地頁上由 JavaScript 寫入，在 Safari 上可能只留一天。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>在開發者工具的 Application 面板（Chrome）或儲存空間面板（Firefox）查看網站網域底下的 cookie，看名稱、網域、到期時間與 &lt;code>HttpOnly&lt;/code> 欄位；由 JavaScript 寫入的 cookie 不能設 &lt;code>HttpOnly&lt;/code>，所以頁面上任何 script 都讀得到它。一次在 Chromium 上的實測裡，&lt;code>_fbp&lt;/code> 與 &lt;code>_fbc&lt;/code> 的到期時間是寫入起算 90 天，&lt;code>_ga&lt;/code> 與 &lt;code>_ga_&amp;lt;評估 ID&amp;gt;&lt;/code> 是 400 天，四個都不是 &lt;code>HttpOnly&lt;/code>、&lt;code>SameSite&lt;/code> 都是 &lt;code>Lax&lt;/code>；&lt;code>_fbc&lt;/code> 只有網址帶了廣告的&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/08-business-analytics/attribution/" data-link-title="Attribution" data-link-desc="使用者從哪來、哪個渠道帶來轉換 — last-touch / first-touch / multi-touch 歸因模型的差異和選擇">點擊 ID&lt;/a>（&lt;code>fbclid&lt;/code>）時才寫入。各 cookie 的值怎麼對應到事件參數，以 &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> 的結果檔為準。&lt;/p></description><content:encoded><![CDATA[<p>第一方 cookie（first-party cookie）是寫在使用者正在瀏覽的那個網站網域上的 cookie；寫在別的網域上、隨對那個網域的請求送出的，是第三方 cookie（third-party cookie）。歸屬只看 cookie 的網域，不看寫入它的 script 是誰的：Meta Pixel 與 GA4 的 script 從 Meta 與 Google 的網域載入，卻用 JavaScript 把識別碼寫在網站自己的網域上，所以 <code>_fbp</code>、<code>_fbc</code>、<code>_ga</code> 都是第一方 cookie。可先對照 <a href="/blog/devops/monitoring/knowledge-cards/headless-browser/" data-link-title="Headless Browser（無頭瀏覽器）" data-link-desc="用 Playwright 或 Puppeteer 測追蹤碼、分析報表裡的機器流量，或自建統計與商業追蹤碼的訪問數對不上時">Headless Browser（無頭瀏覽器）</a>（同一批追蹤碼在無頭模式下照樣寫 cookie、事件卻可能不送）與 <a href="/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">Autocapture（自動事件收集）</a>（這些識別碼會隨自動事件一起送出）；識別碼與瀏覽器指紋的差別見 <a href="/blog/automation/knowledge-cards/browser-fingerprint/" data-link-title="Browser Fingerprint（瀏覽器指紋）" data-link-desc="由多個單獨無害的瀏覽器環境屬性組合而成的裝置特徵集合，決定自建統計蒐集判別訊號時的能力上限">Browser Fingerprint（瀏覽器指紋）</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>第一方 cookie 是追蹤碼在第三方 cookie 被瀏覽器限制之後保存識別碼的位置。第三方 cookie 由 vendor 的網域直接讀寫，第一方 cookie 寫在網站網域上，瀏覽器送往 vendor 網域的請求不會在 Cookie 標頭裡帶上網站網域的 cookie，所以追蹤碼在瀏覽器裡讀出它的值，放進事件請求的參數送出：在預設設定下（使用者沒有透過同意機制拒絕、追蹤碼的第一方 cookie 設定沒有關掉），Meta Pixel 的每一筆事件都帶 <code>fbp</code> 參數，值就是 <code>_fbp</code> 的內容；GA4 的 collect 請求帶 <code>cid</code>，與 <code>_ga</code> 值以點分隔的最後兩段數字相同。網站自己沒有呼叫的<a href="/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">自動事件</a>也帶著這些參數送出。</p>
<h2 id="cookie-期限的設定者與瀏覽器上限">cookie 期限的設定者與瀏覽器上限</h2>
<p>cookie 的到期時間由寫入它的 script 設定，再受瀏覽器的規則截斷。Chrome 從 104 版起把任何 cookie 的到期時間上限訂為 400 天，要求更久的 cookie 會被改成 400 天。Safari 的 Intelligent Tracking Prevention（ITP）對追蹤碼常用的寫法另有三條限制，依 WebKit 的追蹤防護說明頁：由 JavaScript 寫入的 cookie 與其他 script 寫得到的儲存資料（例如 localStorage），在使用者 7 天沒有與這個網站互動之後刪除（WebKit 先前的說明以使用 Safari 的天數計，現行說明頁沒有寫單位），重新起算的條件是互動、不是單純的造訪；ITP 偵測到網址帶著點擊 ID 這類參數（link decoration）時，落地頁上由 JavaScript 寫入的 cookie 期限上限是 24 小時（WebKit 先前的說明寫明的條件是：把使用者導過來的網域被分類為追蹤者，而且網址帶查詢字串或片段）；回應來自偽裝成第一方、實際指向第三方的伺服器（CNAME 或第三方 IP 偽裝）時，那個 HTTP 回應寫入的 cookie 期限上限是 7 天。所以同一個追蹤碼在不同瀏覽器上的 cookie 壽命不同：GA4 代碼替 <code>_ga</code> 與 <code>_ga_&lt;評估 ID&gt;</code> 設的預設期限是兩年，在 Chromium 上實測看到的 400 天是瀏覽器的上限，不是 GA4 的設定；從 Meta 廣告帶著 <code>fbclid</code> 點進來的那一次，<code>_fbc</code> 與 <code>_fbp</code> 正好在落地頁上由 JavaScript 寫入，在 Safari 上可能只留一天。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>在開發者工具的 Application 面板（Chrome）或儲存空間面板（Firefox）查看網站網域底下的 cookie，看名稱、網域、到期時間與 <code>HttpOnly</code> 欄位；由 JavaScript 寫入的 cookie 不能設 <code>HttpOnly</code>，所以頁面上任何 script 都讀得到它。一次在 Chromium 上的實測裡，<code>_fbp</code> 與 <code>_fbc</code> 的到期時間是寫入起算 90 天，<code>_ga</code> 與 <code>_ga_&lt;評估 ID&gt;</code> 是 400 天，四個都不是 <code>HttpOnly</code>、<code>SameSite</code> 都是 <code>Lax</code>；<code>_fbc</code> 只有網址帶了廣告的<a href="/blog/devops/monitoring/08-business-analytics/attribution/" data-link-title="Attribution" data-link-desc="使用者從哪來、哪個渠道帶來轉換 — last-touch / first-touch / multi-touch 歸因模型的差異和選擇">點擊 ID</a>（<code>fbclid</code>）時才寫入。各 cookie 的值怎麼對應到事件參數，以 <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>一個 cookie 受哪些限制，取決於兩個屬性：它寫在哪個網域，以及它由 JavaScript 寫入還是由伺服器回應的 <code>Set-Cookie</code> 標頭寫入。寫入方式可以在 Network 面板分辨：某個回應的 <code>Set-Cookie</code> 標頭帶著這個名稱，就是伺服器寫的；所有回應都沒有、而它在 script 執行之後出現，就是 JavaScript 寫的。網域決定瀏覽器的第三方 cookie 封鎖管不管得到它，封鎖第三方 cookie 不影響第一方 cookie；寫入方式（連同回應來自誰的伺服器）決定 ITP 的哪一條限制管得到它，JavaScript 寫入的第一方 cookie 仍然受 ITP 限制，所以同一個人隔一週以上沒有互動、或從廣告點進來隔天才回來時，可能被追蹤碼當成另一個瀏覽器。用 Network 面板分辨寫入方式時，要用無痕視窗或先清掉這個網站的 cookie、在載入頁面之前就打開面板，否則前一次造訪留下的 cookie 會被誤判成 JavaScript 寫的。在歐盟 ePrivacy 指令下（第 5(3) 條，本站的解讀見<a href="/blog/automation/05-deploy-quota-security/quota-abuse-privacy/" data-link-title="配額、濫用防護、隱私與遷移訊號" data-link-desc="免費配額實際碰撞時會怎樣、擋髒資料與過濾自己瀏覽的做法、不記 PII 的隱私立場、以及量大到該離開 Sheets 的訊號">配額、濫用防護、隱私與遷移訊號</a>的〈隱私與 PII 邊界〉），判斷是否要取得使用者同意時，看的是「在使用者的裝置上儲存或讀取資訊」這個動作，第一方或第三方都在範圍內；把識別碼寫成第一方 cookie 改變的是瀏覽器的限制，改變不了這條同意的要求。</p>
<h2 id="完整章節">完整章節</h2>
<p>追蹤碼寫下哪些 cookie、值怎麼對應到事件參數 → <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>。<code>_fbp</code>、<code>_fbc</code> 在伺服器端事件裡的用途與 ITP 造成的損失 → <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>。cookie 期限在儲存限制裡的位置 → <a href="/blog/devops/monitoring/07-security-privacy/gdpr-minimization/" data-link-title="GDPR 最小化原則的工程落地" data-link-desc="資料最小化、目的限制、儲存限制 — GDPR 三個核心原則在監控系統的工程實作方式">GDPR 最小化原則的工程落地</a>。</p>
]]></content:encoded></item><item><title>Vendor：各家追蹤碼的接入與驗證</title><link>https://tarrragon.github.io/blog/devops/monitoring/vendors/</link><pubDate>Thu, 22 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/monitoring/vendors/</guid><description>&lt;p>本資料夾的目的是實測各家追蹤系統的設計：每一家有一篇機制文，寫它在頁面或 App 裡做什麼、事件送到哪裡給誰用、資料在哪些環節會掉；有一頁實測頁，讓讀者在瀏覽器裡按按鈕觸發它、對照它實際送出的內容；Playwright 擷取紀錄把同樣的操作自動化並記下擷取時間，留給 vendor 改版之後比對。另有一篇跨三家的帳號治理文章，處理這些追蹤資產建在誰的帳號底下、權限怎麼分、委託代理商與轉手時怎麼移交。本站不是要用這些追蹤碼收集讀者資料，所以實測頁是獨立的頁面、只在讀者按下按鈕時載入。&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/06-commercial-comparison/" data-link-title="模組六：商業方案對照" data-link-desc="Sentry / Crashlytics / Datadog RUM / Mixpanel — 自架 vs 商業的功能和成本取捨">模組六：商業方案對照&lt;/a> 處理的是要不要買、什麼時候從自架換過去，跟本資料夾的分工是「選型」與「設計實測」，同一家 vendor 可以兩邊各有一篇。&lt;/p>
&lt;h2 id="篇目">篇目&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>類型&lt;/th>
 &lt;th>交付&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&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>&lt;/td>
 &lt;td>機制文&lt;/td>
 &lt;td>標準事件、被攔截的路徑、伺服器端補送、&lt;code>event_id&lt;/code> 去重、Test Events&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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;/td>
 &lt;td>實測頁&lt;/td>
 &lt;td>按鈕觸發 PageView / ViewContent / Purchase、&lt;code>_fbp&lt;/code> 與 &lt;code>_fbc&lt;/code> 的產生、載入失敗態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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;/td>
 &lt;td>機制文&lt;/td>
 &lt;td>資料層約定、預覽模式驗證、自訂 HTML 的治理、伺服器端代碼管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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>&lt;/td>
 &lt;td>實測頁&lt;/td>
 &lt;td>容器載入、資料層內容、容器載入前推入的事件、代碼送出的請求&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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>&lt;/td>
 &lt;td>擷取紀錄&lt;/td>
 &lt;td>擷取方式、結果檔欄位、各次結果（標擷取時間與版本）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/vendors/tracking-account-ownership/" data-link-title="追蹤資產的歸屬：Meta、GTM 與 GA4 的擁有層級、權限分級與移交方式" data-link-desc="Meta Pixel 的資料集、GTM 容器與 GA4 資源（property）各自屬於哪一層的擁有者，三家的權限怎麼分級，哪些資產能換到另一個擁有者底下，以及公司自營、委託代理商、換代理商與事業轉手時的歸屬安排">追蹤資產的歸屬&lt;/a>&lt;/td>
 &lt;td>帳號治理&lt;/td>
 &lt;td>Meta 組合、GTM 帳戶與容器、GA4 帳戶與資源的擁有層級、權限分級、能否換擁有者、代理商與轉手時的歸屬&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>擷取紀錄在第一次擷取完成之前維持 &lt;code>draft: true&lt;/code>；2026-10-23 GTM 與 Meta Pixel 各完成一次擷取之後發布。&lt;/p>
&lt;h2 id="篇目的收錄條件">篇目的收錄條件&lt;/h2>
&lt;p>收進來的 vendor 要有一段自架方案沒有的機制可以寫：廣告平台的資料消費者是投放演算法、tag manager 把部署權交給行銷人員，這些在模組一到八的自架脈絡裡沒有對應物。只是功能清單不同的 vendor（另一家 error tracking）先在模組六的對照文補一列，累積到有獨立機制可講再開篇。&lt;/p>
&lt;h2 id="實測方式的決策紀錄">實測方式的決策紀錄&lt;/h2>
&lt;p>本站已經有一套自製的流量 beacon（&lt;code>layouts/partials/custom_body.html&lt;/code>，說明在 &lt;a href="https://tarrragon.github.io/blog/automation/02-analytics-beacon/" data-link-title="模組二：流量 beacon 實作" data-link-desc="把「頁面被看了就送一則事件、接收端寫進 Sheet」從零做到收到第一筆真實瀏覽紀錄時的完整實作">Automation 模組二：流量 beacon 實作&lt;/a>），設計上刻意不送完整 User-Agent、識別碼是隨機值、有 opt-out。全站加上第三方追蹤碼會推翻這組設計：讀者的瀏覽紀錄送到 Meta 與 Google、頁面多出 &lt;code>_fbp&lt;/code>、&lt;code>_ga&lt;/code> 這類 cookie、每一頁多載入第三方 script，歐盟讀者的部分還需要同意聲明。所以不全站嵌入，採用兩種實測方式並行（2026-10-22 決定）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>獨立實測頁&lt;/strong>：實作是 &lt;code>layouts/shortcodes/vendor-lab.html&lt;/code>。頁面預設不注入任何第三方 script，讀者按下載入按鈕才注入；ID 由讀者填寫，預設值取 &lt;code>hugo.toml&lt;/code> 的 &lt;code>[params.vendorLab]&lt;/code>。&lt;code>tests/vendor-lab.spec.ts&lt;/code> 把 vendor 網域攔下來回 stub，守住「按下之前零請求」與 ID 格式檢查。&lt;/li>
&lt;li>&lt;strong>本機 Playwright 擷取&lt;/strong>：&lt;code>scripts/vendor-capture/capture.mjs&lt;/code> 操作實測頁並把請求、cookie、資料層寫成 JSON（&lt;code>scripts/vendor-capture/results/&lt;/code>），每份結果帶擷取時間、瀏覽器版本、vendor script 的回應標頭與本體雜湊，因為 vendor 改版之後結果就不再適用。Meta Pixel 在無頭瀏覽器裡不送事件，擷取 Meta 要加 &lt;code>--headed&lt;/code>（2026-10-23 實測）；腳本不改寫 User-Agent。&lt;/li>
&lt;li>&lt;strong>文章不寫測試 ID、實測頁不設預設值&lt;/strong>（2026-10-23 決定）：Pixel ID、容器 ID 與評估 ID 本來就寫在網頁原始碼裡，公開它們不會讓別人登入帳號；風險在反方向，任何人都能拿 ID 往帳號送事件，而 Meta 的資料集不能刪除，被灌入假事件之後只能另建一個。這組測試帳號只放實測資產、沒有投放廣告，被灌入假事件的影響只在測試報表上，所以接受這個風險：擷取結果的 JSON 保留原始 ID（repo 是公開的，ID 因此查得到），供日後比對。文章正文不寫實際值，理由是讀者照抄別人的 ID 去測試，事件會送進別人的帳號、也學不到用自己的 ID；文章只描述 ID 的格式與查詢位置。&lt;code>hugo.toml&lt;/code> 的 &lt;code>[params.vendorLab]&lt;/code> 維持空白，讀者在實測頁填自己的 ID。&lt;/li>
&lt;li>&lt;strong>測試帳號&lt;/strong>：Meta 商家資產管理組合「Tarragon Tech Notes」裡的資料集、GTM 帳戶 &lt;code>vendor-lab-test&lt;/code>、GA4 帳戶 &lt;code>vendor-lab-test&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> 的〈擷取用的測試帳號〉一節，為什麼另開擁有者記在 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/vendors/tracking-account-ownership/" data-link-title="追蹤資產的歸屬：Meta、GTM 與 GA4 的擁有層級、權限分級與移交方式" data-link-desc="Meta Pixel 的資料集、GTM 容器與 GA4 資源（property）各自屬於哪一層的擁有者，三家的權限怎麼分級，哪些資產能換到另一個擁有者底下，以及公司自營、委託代理商、換代理商與事業轉手時的歸屬安排">追蹤資產的歸屬&lt;/a> 的〈本站實測資產的擁有者〉一節。&lt;/li>
&lt;/ul>
&lt;p>Conversions API 這類伺服器端路徑在本站有一個限制：本站的接收端是 Apps Script，而 Apps Script 的 &lt;code>doPost&lt;/code> 讀不到請求的 HTTP header（拿不到 IP 與 User-Agent），Meta 對網站事件要求的 &lt;code>client_user_agent&lt;/code> 只能由前端放進 payload，這又回到送完整 User-Agent 的隱私取捨上，所以伺服器端目前只寫機制、不做實測。&lt;/p></description><content:encoded><![CDATA[<p>本資料夾的目的是實測各家追蹤系統的設計：每一家有一篇機制文，寫它在頁面或 App 裡做什麼、事件送到哪裡給誰用、資料在哪些環節會掉；有一頁實測頁，讓讀者在瀏覽器裡按按鈕觸發它、對照它實際送出的內容；Playwright 擷取紀錄把同樣的操作自動化並記下擷取時間，留給 vendor 改版之後比對。另有一篇跨三家的帳號治理文章，處理這些追蹤資產建在誰的帳號底下、權限怎麼分、委託代理商與轉手時怎麼移交。本站不是要用這些追蹤碼收集讀者資料，所以實測頁是獨立的頁面、只在讀者按下按鈕時載入。<a href="/blog/devops/monitoring/06-commercial-comparison/" data-link-title="模組六：商業方案對照" data-link-desc="Sentry / Crashlytics / Datadog RUM / Mixpanel — 自架 vs 商業的功能和成本取捨">模組六：商業方案對照</a> 處理的是要不要買、什麼時候從自架換過去，跟本資料夾的分工是「選型」與「設計實測」，同一家 vendor 可以兩邊各有一篇。</p>
<h2 id="篇目">篇目</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>類型</th>
          <th>交付</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><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></td>
          <td>機制文</td>
          <td>標準事件、被攔截的路徑、伺服器端補送、<code>event_id</code> 去重、Test Events</td>
      </tr>
      <tr>
          <td><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></td>
          <td>實測頁</td>
          <td>按鈕觸發 PageView / ViewContent / Purchase、<code>_fbp</code> 與 <code>_fbc</code> 的產生、載入失敗態</td>
      </tr>
      <tr>
          <td><a href="/blog/devops/monitoring/vendors/google-tag-manager/" data-link-title="Google Tag Manager：追蹤碼的部署層、資料層約定與發布治理" data-link-desc="GTM 容器在頁面上承擔的部署責任、資料層作為前端與行銷之間的事件約定、預覽模式與版本發布的驗證方式、自訂 HTML 代碼帶來的安全與效能成本，以及伺服器端代碼管理改變的資料路徑">Google Tag Manager</a></td>
          <td>機制文</td>
          <td>資料層約定、預覽模式驗證、自訂 HTML 的治理、伺服器端代碼管理</td>
      </tr>
      <tr>
          <td><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></td>
          <td>實測頁</td>
          <td>容器載入、資料層內容、容器載入前推入的事件、代碼送出的請求</td>
      </tr>
      <tr>
          <td><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></td>
          <td>擷取紀錄</td>
          <td>擷取方式、結果檔欄位、各次結果（標擷取時間與版本）</td>
      </tr>
      <tr>
          <td><a href="/blog/devops/monitoring/vendors/tracking-account-ownership/" data-link-title="追蹤資產的歸屬：Meta、GTM 與 GA4 的擁有層級、權限分級與移交方式" data-link-desc="Meta Pixel 的資料集、GTM 容器與 GA4 資源（property）各自屬於哪一層的擁有者，三家的權限怎麼分級，哪些資產能換到另一個擁有者底下，以及公司自營、委託代理商、換代理商與事業轉手時的歸屬安排">追蹤資產的歸屬</a></td>
          <td>帳號治理</td>
          <td>Meta 組合、GTM 帳戶與容器、GA4 帳戶與資源的擁有層級、權限分級、能否換擁有者、代理商與轉手時的歸屬</td>
      </tr>
  </tbody>
</table>
<p>擷取紀錄在第一次擷取完成之前維持 <code>draft: true</code>；2026-10-23 GTM 與 Meta Pixel 各完成一次擷取之後發布。</p>
<h2 id="篇目的收錄條件">篇目的收錄條件</h2>
<p>收進來的 vendor 要有一段自架方案沒有的機制可以寫：廣告平台的資料消費者是投放演算法、tag manager 把部署權交給行銷人員，這些在模組一到八的自架脈絡裡沒有對應物。只是功能清單不同的 vendor（另一家 error tracking）先在模組六的對照文補一列，累積到有獨立機制可講再開篇。</p>
<h2 id="實測方式的決策紀錄">實測方式的決策紀錄</h2>
<p>本站已經有一套自製的流量 beacon（<code>layouts/partials/custom_body.html</code>，說明在 <a href="/blog/automation/02-analytics-beacon/" data-link-title="模組二：流量 beacon 實作" data-link-desc="把「頁面被看了就送一則事件、接收端寫進 Sheet」從零做到收到第一筆真實瀏覽紀錄時的完整實作">Automation 模組二：流量 beacon 實作</a>），設計上刻意不送完整 User-Agent、識別碼是隨機值、有 opt-out。全站加上第三方追蹤碼會推翻這組設計：讀者的瀏覽紀錄送到 Meta 與 Google、頁面多出 <code>_fbp</code>、<code>_ga</code> 這類 cookie、每一頁多載入第三方 script，歐盟讀者的部分還需要同意聲明。所以不全站嵌入，採用兩種實測方式並行（2026-10-22 決定）：</p>
<ul>
<li><strong>獨立實測頁</strong>：實作是 <code>layouts/shortcodes/vendor-lab.html</code>。頁面預設不注入任何第三方 script，讀者按下載入按鈕才注入；ID 由讀者填寫，預設值取 <code>hugo.toml</code> 的 <code>[params.vendorLab]</code>。<code>tests/vendor-lab.spec.ts</code> 把 vendor 網域攔下來回 stub，守住「按下之前零請求」與 ID 格式檢查。</li>
<li><strong>本機 Playwright 擷取</strong>：<code>scripts/vendor-capture/capture.mjs</code> 操作實測頁並把請求、cookie、資料層寫成 JSON（<code>scripts/vendor-capture/results/</code>），每份結果帶擷取時間、瀏覽器版本、vendor script 的回應標頭與本體雜湊，因為 vendor 改版之後結果就不再適用。Meta Pixel 在無頭瀏覽器裡不送事件，擷取 Meta 要加 <code>--headed</code>（2026-10-23 實測）；腳本不改寫 User-Agent。</li>
<li><strong>文章不寫測試 ID、實測頁不設預設值</strong>（2026-10-23 決定）：Pixel ID、容器 ID 與評估 ID 本來就寫在網頁原始碼裡，公開它們不會讓別人登入帳號；風險在反方向，任何人都能拿 ID 往帳號送事件，而 Meta 的資料集不能刪除，被灌入假事件之後只能另建一個。這組測試帳號只放實測資產、沒有投放廣告，被灌入假事件的影響只在測試報表上，所以接受這個風險：擷取結果的 JSON 保留原始 ID（repo 是公開的，ID 因此查得到），供日後比對。文章正文不寫實際值，理由是讀者照抄別人的 ID 去測試，事件會送進別人的帳號、也學不到用自己的 ID；文章只描述 ID 的格式與查詢位置。<code>hugo.toml</code> 的 <code>[params.vendorLab]</code> 維持空白，讀者在實測頁填自己的 ID。</li>
<li><strong>測試帳號</strong>：Meta 商家資產管理組合「Tarragon Tech Notes」裡的資料集、GTM 帳戶 <code>vendor-lab-test</code>、GA4 帳戶 <code>vendor-lab-test</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> 的〈擷取用的測試帳號〉一節，為什麼另開擁有者記在 <a href="/blog/devops/monitoring/vendors/tracking-account-ownership/" data-link-title="追蹤資產的歸屬：Meta、GTM 與 GA4 的擁有層級、權限分級與移交方式" data-link-desc="Meta Pixel 的資料集、GTM 容器與 GA4 資源（property）各自屬於哪一層的擁有者，三家的權限怎麼分級，哪些資產能換到另一個擁有者底下，以及公司自營、委託代理商、換代理商與事業轉手時的歸屬安排">追蹤資產的歸屬</a> 的〈本站實測資產的擁有者〉一節。</li>
</ul>
<p>Conversions API 這類伺服器端路徑在本站有一個限制：本站的接收端是 Apps Script，而 Apps Script 的 <code>doPost</code> 讀不到請求的 HTTP header（拿不到 IP 與 User-Agent），Meta 對網站事件要求的 <code>client_user_agent</code> 只能由前端放進 payload，這又回到送完整 User-Agent 的隱私取捨上，所以伺服器端目前只寫機制、不做實測。</p>
<h2 id="backlog">Backlog</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>類型</th>
          <th>前置條件</th>
          <th>規模</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GA4 網頁版的事件模型與 Measurement Protocol（機制文 + 實測頁）</td>
          <td>主章</td>
          <td>無</td>
          <td>2</td>
      </tr>
      <tr>
          <td>Mixpanel 的接入面（identify / alias、Ingestion API）</td>
          <td>主章</td>
          <td>無</td>
          <td>1</td>
      </tr>
      <tr>
          <td>實際送件驗證 Meta 管理員爭議對「唯一帳號已遺失」與「前承包商持有組合」兩種情形的處理結果；說明頁層級已查證（前承包商明列為可申請，唯一帳號遺失不在清單、送件要登入與組合有關聯的個人檔案），結果寫回追蹤資產的歸屬</td>
          <td>案例</td>
          <td>需有可申訴的實際組合</td>
          <td>小</td>
      </tr>
      <tr>
          <td>異源複核：另一個執行者（另一個工作階段或真人）用同一組 frame 獨立掃追蹤資產的歸屬與擷取紀錄，兩邊各自報數再並排</td>
          <td>案例</td>
          <td>有另一個執行者可用</td>
          <td>小</td>
      </tr>
      <tr>
          <td>查證三項本輪沒讀到原文的宣稱：Meta 合作夥伴存取裡「代理商提出要求、擁有者核准」那條路徑（team-access-management 已標未查證）；GA4 使用者群組的前提（同篇）；visitor-identity 寫 ITP 天數「曾經是三十天」（WebKit 文件裡 30 天屬於另一條規則，查證後修正）</td>
          <td>跨模組</td>
          <td>用瀏覽器讀 Meta 商家說明頁與 GA4 使用者管理頁</td>
          <td>小</td>
      </tr>
      <tr>
          <td>時效複核：三家帳號規則與 fbevents.js、gtm.js 會改版，半年後重讀追蹤資產的歸屬與擷取紀錄兩篇、三張追蹤碼知識卡（first-party-cookie、headless-browser、autocapture），用 <code>rg &quot;botblocking|_fbp|_ga\b|SubscribedButtonClick|管理員爭議|主機名稱篩選|ITP|autoConfig&quot; content/</code> 找出其他引用處一併核對，並重跑一次擷取，比對雜湊與說明頁</td>
          <td>案例</td>
          <td>2027-04 之後</td>
          <td>小</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>Headless Browser（無頭瀏覽器）</title><link>https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/headless-browser/</link><pubDate>Fri, 23 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/headless-browser/</guid><description>&lt;p>無頭瀏覽器（headless browser）是不開啟可見視窗的瀏覽器：頁面照常載入、JavaScript 照常執行，只是畫面不顯示出來。無頭與被程式驅動是兩個獨立的屬性：有視窗的瀏覽器同樣可以被程式驅動，命令列直接執行 &lt;code>chrome --headless&lt;/code> 輸出截圖也不經過任何自動化框架；Playwright、Puppeteer 這類自動化工具則多半同時具備兩者，預設就以無頭模式啟動 Chromium。可先對照 &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 限制時">First-party Cookie（第一方 cookie）&lt;/a>（無頭模式下追蹤碼照樣寫 cookie）與 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">Autocapture（自動事件收集）&lt;/a>（網站沒有呼叫的事件由誰決定送出）；把無頭瀏覽器當成要辨識的訪客時的訊號設計，見 &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>；不靠 User-Agent、改從瀏覽器環境辨識自動化的做法，見 &lt;a href="https://tarrragon.github.io/blog/automation/knowledge-cards/browser-fingerprint/" data-link-title="Browser Fingerprint（瀏覽器指紋）" data-link-desc="由多個單獨無害的瀏覽器環境屬性組合而成的裝置特徵集合，決定自建統計蒐集判別訊號時的能力上限">Browser Fingerprint（瀏覽器指紋）&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>無頭模式改變的是瀏覽器自己對外暴露的特徵，追蹤碼在訪客的瀏覽器裡、收資料的伺服器在 vendor 那一端，都可能依這些特徵決定事件送不送出、收不收下。最直接的特徵是 User-Agent（瀏覽器在每個請求裡附上的身分字串）：一次以 Playwright 預設無頭模式啟動 Chromium 的實測裡，User-Agent 帶 &lt;code>HeadlessChrome&lt;/code> 字樣，有視窗模式下是一般的 &lt;code>Chrome&lt;/code>。被程式驅動則另有自己的特徵，&lt;code>navigator.webdriver&lt;/code> 在 Playwright、Puppeteer、Selenium 驅動的瀏覽器裡是 &lt;code>true&lt;/code>，有沒有視窗都一樣。所以「擋無頭瀏覽器」與「擋自動化」是兩條不同的規則：比對 &lt;code>HeadlessChrome&lt;/code> 字樣的規則換成有視窗模式就不命中，比對 &lt;code>navigator.webdriver&lt;/code> 的規則兩種模式都命中。被攔下的事件裡也包括網站沒有呼叫的&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">自動事件&lt;/a>，它們走的是同一條送出路徑。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>本站用 Playwright 寫的擷取腳本（&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>）在實測頁上載入本站測試用的一個 Meta Pixel。&lt;code>fbevents.js&lt;/code> 載入後會向 Meta 取得這個 Pixel 的設定檔，設定檔指定要載入的模組（plugin）裡有一個 &lt;code>botblocking&lt;/code>，它拿 &lt;code>navigator.userAgent&lt;/code> 比對一份含 &lt;code>HeadlessChrome/&lt;/code> 的機器人規則清單。無頭模式下 &lt;code>fbevents.js&lt;/code> 照樣載入、&lt;code>_fbp&lt;/code> 照樣寫入，而 PageView、ViewContent、Purchase 一筆都沒有送出；腳本改用有視窗模式之後，三筆事件都送出了。這個模組只攔事件請求，script 的載入與&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> 的寫入不受影響，頁面上的狀態訊息在兩種模式下也相同，從頁面上看不出事件沒送。GA4 的 collect 請求在無頭模式下照樣送出並收到回應；GA4 在接收端預設排除已知機器人流量，依據是 Google 的研究與 IAB（Interactive Advertising Bureau，網路廣告業的產業組織）的機器人清單，被排除了多少不會顯示，所以從擷取結果判斷不出無頭模式的事件是否進了報表。&lt;/p>
&lt;h2 id="判讀方式">判讀方式&lt;/h2>
&lt;p>兩次擷取用同一個 Pixel、在相近的時間、做相同的操作、只切換有沒有視窗時，兩種模式的結果差異量的是追蹤碼對無頭特徵的反應：有視窗模式下送得出、無頭模式下送不出的事件，是被比對 User-Agent 這類無頭特徵的規則攔下的。兩次之間隔得久，Pixel 的設定檔可能已經改版，差異就不能只歸給無頭模式。有視窗模式的 Playwright 仍然是被程式驅動的瀏覽器（&lt;code>navigator.webdriver&lt;/code> 為真），所以它的結果不等於真人訪客的結果；追蹤碼改成依 &lt;code>navigator.webdriver&lt;/code> 判斷時，有視窗模式也會被攔。比對自建統計與商業追蹤碼的訪問數時，差額裡可能有一部分是無頭瀏覽器的訪問：自建統計的接收端收下並標成機器流量，Meta Pixel 在瀏覽器裡就丟掉。反過來，GA4 的代碼在無頭模式下照常送出事件，所以拿正式網站的評估 ID 或 Pixel ID 跑自動化測試，事件會送進正式的資源，進不進報表由 Google 的已知機器人排除判定，而排除了多少看不到；擷取與測試用測試專用的帳戶與 ID，做法見&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>的〈擷取用的測試帳號〉。&lt;/p>
&lt;h2 id="完整章節">完整章節&lt;/h2>
&lt;p>無頭與有視窗兩種模式下 Meta Pixel 實際送出的請求、&lt;code>botblocking&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>。自建統計怎麼用 User-Agent、&lt;code>navigator.webdriver&lt;/code> 等訊號標記機器流量 → &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>。GA4 預設的已知機器人排除與主機名稱篩選器 → &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/07-security-privacy/client-sdk-authentication/" data-link-title="Client-side SDK 認證的根本限制" data-link-desc="嵌在 client 端的 credential 必然可被提取 — 認清 architecture 天花板後的多層緩解策略，從 origin 驗證到 device attestation">Client-side SDK 認證的根本限制&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>無頭瀏覽器（headless browser）是不開啟可見視窗的瀏覽器：頁面照常載入、JavaScript 照常執行，只是畫面不顯示出來。無頭與被程式驅動是兩個獨立的屬性：有視窗的瀏覽器同樣可以被程式驅動，命令列直接執行 <code>chrome --headless</code> 輸出截圖也不經過任何自動化框架；Playwright、Puppeteer 這類自動化工具則多半同時具備兩者，預設就以無頭模式啟動 Chromium。可先對照 <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>（無頭模式下追蹤碼照樣寫 cookie）與 <a href="/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">Autocapture（自動事件收集）</a>（網站沒有呼叫的事件由誰決定送出）；把無頭瀏覽器當成要辨識的訪客時的訊號設計，見 <a href="/blog/automation/06-reading-the-data/automated-traffic/" data-link-title="辨識自動化流量" data-link-desc="接收端讀不到 HTTP 標頭時在前端蒐集訊號分辨機器與真人；多數 AI 訓練爬蟲不執行 JavaScript，因此不會出現在這份統計裡">辨識自動化流量</a>；不靠 User-Agent、改從瀏覽器環境辨識自動化的做法，見 <a href="/blog/automation/knowledge-cards/browser-fingerprint/" data-link-title="Browser Fingerprint（瀏覽器指紋）" data-link-desc="由多個單獨無害的瀏覽器環境屬性組合而成的裝置特徵集合，決定自建統計蒐集判別訊號時的能力上限">Browser Fingerprint（瀏覽器指紋）</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>無頭模式改變的是瀏覽器自己對外暴露的特徵，追蹤碼在訪客的瀏覽器裡、收資料的伺服器在 vendor 那一端，都可能依這些特徵決定事件送不送出、收不收下。最直接的特徵是 User-Agent（瀏覽器在每個請求裡附上的身分字串）：一次以 Playwright 預設無頭模式啟動 Chromium 的實測裡，User-Agent 帶 <code>HeadlessChrome</code> 字樣，有視窗模式下是一般的 <code>Chrome</code>。被程式驅動則另有自己的特徵，<code>navigator.webdriver</code> 在 Playwright、Puppeteer、Selenium 驅動的瀏覽器裡是 <code>true</code>，有沒有視窗都一樣。所以「擋無頭瀏覽器」與「擋自動化」是兩條不同的規則：比對 <code>HeadlessChrome</code> 字樣的規則換成有視窗模式就不命中，比對 <code>navigator.webdriver</code> 的規則兩種模式都命中。被攔下的事件裡也包括網站沒有呼叫的<a href="/blog/devops/monitoring/knowledge-cards/autocapture/" data-link-title="Autocapture（自動事件收集）" data-link-desc="盤點網站實際送出哪些事件與內容、報表裡出現網站沒有呼叫過的事件，或在手動埋點與自動收集之間取捨時">自動事件</a>，它們走的是同一條送出路徑。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>本站用 Playwright 寫的擷取腳本（<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>）在實測頁上載入本站測試用的一個 Meta Pixel。<code>fbevents.js</code> 載入後會向 Meta 取得這個 Pixel 的設定檔，設定檔指定要載入的模組（plugin）裡有一個 <code>botblocking</code>，它拿 <code>navigator.userAgent</code> 比對一份含 <code>HeadlessChrome/</code> 的機器人規則清單。無頭模式下 <code>fbevents.js</code> 照樣載入、<code>_fbp</code> 照樣寫入，而 PageView、ViewContent、Purchase 一筆都沒有送出；腳本改用有視窗模式之後，三筆事件都送出了。這個模組只攔事件請求，script 的載入與<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> 的寫入不受影響，頁面上的狀態訊息在兩種模式下也相同，從頁面上看不出事件沒送。GA4 的 collect 請求在無頭模式下照樣送出並收到回應；GA4 在接收端預設排除已知機器人流量，依據是 Google 的研究與 IAB（Interactive Advertising Bureau，網路廣告業的產業組織）的機器人清單，被排除了多少不會顯示，所以從擷取結果判斷不出無頭模式的事件是否進了報表。</p>
<h2 id="判讀方式">判讀方式</h2>
<p>兩次擷取用同一個 Pixel、在相近的時間、做相同的操作、只切換有沒有視窗時，兩種模式的結果差異量的是追蹤碼對無頭特徵的反應：有視窗模式下送得出、無頭模式下送不出的事件，是被比對 User-Agent 這類無頭特徵的規則攔下的。兩次之間隔得久，Pixel 的設定檔可能已經改版，差異就不能只歸給無頭模式。有視窗模式的 Playwright 仍然是被程式驅動的瀏覽器（<code>navigator.webdriver</code> 為真），所以它的結果不等於真人訪客的結果；追蹤碼改成依 <code>navigator.webdriver</code> 判斷時，有視窗模式也會被攔。比對自建統計與商業追蹤碼的訪問數時，差額裡可能有一部分是無頭瀏覽器的訪問：自建統計的接收端收下並標成機器流量，Meta Pixel 在瀏覽器裡就丟掉。反過來，GA4 的代碼在無頭模式下照常送出事件，所以拿正式網站的評估 ID 或 Pixel ID 跑自動化測試，事件會送進正式的資源，進不進報表由 Google 的已知機器人排除判定，而排除了多少看不到；擷取與測試用測試專用的帳戶與 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>的〈擷取用的測試帳號〉。</p>
<h2 id="完整章節">完整章節</h2>
<p>無頭與有視窗兩種模式下 Meta Pixel 實際送出的請求、<code>botblocking</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>。自建統計怎麼用 User-Agent、<code>navigator.webdriver</code> 等訊號標記機器流量 → <a href="/blog/automation/06-reading-the-data/automated-traffic/" data-link-title="辨識自動化流量" data-link-desc="接收端讀不到 HTTP 標頭時在前端蒐集訊號分辨機器與真人；多數 AI 訓練爬蟲不執行 JavaScript，因此不會出現在這份統計裡">辨識自動化流量</a>。GA4 預設的已知機器人排除與主機名稱篩選器 → <a href="/blog/devops/monitoring/07-security-privacy/client-sdk-authentication/" data-link-title="Client-side SDK 認證的根本限制" data-link-desc="嵌在 client 端的 credential 必然可被提取 — 認清 architecture 天花板後的多層緩解策略，從 origin 驗證到 device attestation">Client-side SDK 認證的根本限制</a>。</p>
]]></content:encoded></item><item><title>Autocapture（自動事件收集）</title><link>https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/autocapture/</link><pubDate>Fri, 23 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/autocapture/</guid><description>&lt;p>自動事件收集（autocapture）是由追蹤碼自己決定送出哪些事件、網站不必逐一呼叫的做法：script 在頁面上監聽點擊、捲動、表單這類互動，或在頁面載入、工作階段開始這類時點，依 vendor 的規則產生事件並送出。涵蓋哪些互動由 vendor 的規則決定，不是頁面上的每一個互動都會送。相對的做法是手動埋點：開發者在要追蹤的元素或事件處自己寫追蹤程式（標上屬性或呼叫追蹤函式），只有這些互動才會送出。可先對照 &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 限制時">First-party Cookie（第一方 cookie）&lt;/a>（自動事件同樣帶著識別碼 cookie 的值送出）與 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/headless-browser/" data-link-title="Headless Browser（無頭瀏覽器）" data-link-desc="用 Playwright 或 Puppeteer 測追蹤碼、分析報表裡的機器流量，或自建統計與商業追蹤碼的訪問數對不上時">Headless Browser（無頭瀏覽器）&lt;/a>（瀏覽器裡的機器人過濾對自動事件一樣生效）。自建 SDK 在事件送出前遮蔽欄位的做法見 &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/redaction/" data-link-title="Redaction" data-link-desc="說明在事件資料離開 client 之前把敏感欄位的值替換成遮罩或移除的機制">Redaction&lt;/a>；商業追蹤碼的自動事件由 vendor 的 script 直接送出，不經過網站自建 SDK 的遮蔽步驟，網站能用的是 vendor 自己提供的設定：GA4 在網頁資料串流上有資料遮蔽（data redaction），在瀏覽器裡、資料送出之前遮掉事件參數裡的 email 與指定的網址查詢參數；Meta Pixel 能做的是關閉自動事件（做法在本卡〈概念位置〉的 Meta Pixel 段）。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>自動事件收集決定的是「事件從哪裡來」，位置在事件產生的那一端，比管線上的&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="在事件產生階段按比例丟棄部分事件降低管線負載 — 分靜態取樣（config 固定比例）和動態取樣（背壓觸發自動降低）">取樣&lt;/a>與&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/knowledge-cards/redaction/" data-link-title="Redaction" data-link-desc="說明在事件資料離開 client 之前把敏感欄位的值替換成遮罩或移除的機制">遮蔽&lt;/a>都早：事件的欄位由 vendor 的規則決定，網站要等它送出之後才看得到內容。它與本站自建 SDK 的&lt;a href="https://tarrragon.github.io/blog/devops/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">自動攔截機制&lt;/a>（auto-intercept）同樣不需要開發者為每個事件寫程式，分辨兩者的依據是送出規則由誰持有：自動攔截的規則寫在網站自己的 SDK 程式碼裡，自動事件收集的規則由 vendor 決定，網站的原始碼裡看不到。名稱各家不同：Meta 把這類事件叫自動事件（automatic events），GA4 叫自動收集的事件（automatically collected events），本站把兩者統稱為自動事件。兩家的範圍與開關也不同。&lt;/p>
&lt;p>&lt;strong>Meta Pixel&lt;/strong> 的自動事件由 Pixel 的設定檔指定的模組產生：&lt;code>fbevents.js&lt;/code> 載入後會向 Meta 取得這個 Pixel 的設定檔，設定檔決定要載入哪些模組（plugin，見 &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> 的〈Meta Pixel 在瀏覽器裡做的事〉）。實測時每按一次頁面上的按鈕，&lt;code>fbevents.js&lt;/code> 都在網站呼叫的事件之外多送一筆 &lt;code>SubscribedButtonClick&lt;/code>，內容包含按鈕文字（&lt;code>cd[buttonText]&lt;/code>）、按鈕的 DOM 特徵與頁面特徵；設定檔來自 Meta 的伺服器，所以送出哪些自動事件由 Meta 那一端的設定決定，網站的原始碼裡看不到。要關掉 Pixel 依按鈕點擊與頁面中繼資料自動送出的事件，Meta 的開發者文件給的做法是在 Pixel 基本碼裡、&lt;code>fbq('init', …)&lt;/code> 之前加一行 &lt;code>fbq('set', 'autoConfig', false, '&amp;lt;Pixel ID&amp;gt;')&lt;/code>，&lt;code>&amp;lt;Pixel ID&amp;gt;&lt;/code> 換成自己的 Pixel ID。&lt;/p>
&lt;p>&lt;strong>GA4&lt;/strong> 的自動收集事件分成只要裝了 Google 代碼就會送的基本事件，例如 &lt;code>first_visit&lt;/code>、&lt;code>session_start&lt;/code>、&lt;code>user_engagement&lt;/code>，以及由加強型評估（enhanced measurement）控制的事件。加強型評估是網頁資料串流（GA4 資源裡代表一個網站資料來源的設定單位）上的一組開關，涵蓋捲動、外連點擊、站內搜尋、影片互動、檔案下載與表單互動，其中檔案下載會帶連結文字（&lt;code>link_text&lt;/code>）、表單送出會帶送出按鈕的文字。&lt;code>page_view&lt;/code> 也列在加強型評估底下，而它是其中唯一關不掉的一項。這組開關在 GA4 管理介面的資料串流設定裡，需要編輯者以上的權限。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>自動事件在請求裡的樣子與網站自己呼叫的事件相同，差別只在網站的程式碼裡找不到對應的呼叫。Meta 的事件請求送到 &lt;code>www.facebook.com/tr/&lt;/code>，事件名稱在 &lt;code>ev&lt;/code> 參數；GA4 的事件請求送到 Google Analytics 網域下的 &lt;code>g/collect&lt;/code>（在開發者工具的 Network 面板以 &lt;code>collect&lt;/code> 篩選），事件名稱在 &lt;code>en&lt;/code> 參數。一次 Meta Pixel 的實測裡，網站呼叫了 ViewContent 與 Purchase 各一次，擷取結果裡多出兩筆 &lt;code>SubscribedButtonClick&lt;/code>，各自緊跟在一次按鈕點擊之後。&lt;/p>
&lt;h2 id="判讀方式">判讀方式&lt;/h2>
&lt;p>網站原始碼只列得出手動埋點的那一部分，網站實際送出的資料要從送出的請求裡數；按鈕文字、連結文字這類欄位也在送出範圍內，按鈕或連結上顯示了使用者的個人資料時，那段文字會一起送到 vendor。兩種埋點方式的取捨方向相反：手動埋點會漏掉沒有寫追蹤程式的互動，換到的是送出的欄位全部由開發者決定；自動收集不需要逐一標記，而涵蓋哪些互動、送出哪些欄位由 vendor 的規則決定、會隨 vendor 改版變動。vendor 改版不會通知網站，所以送出資料的盤點要定期重做；Meta Pixel 的 &lt;code>fbevents.js&lt;/code> 版本可以在頁面的開發者工具 Console 輸入 &lt;code>fbq.version&lt;/code> 讀出，但自動事件由每個 Pixel 的設定檔決定，設定檔或事件管理工具裡的設定改了，&lt;code>fbq.version&lt;/code> 不會變，所以版本相同不代表送出的內容沒變，盤點仍要看實際送出的請求。&lt;/p>
&lt;h2 id="完整章節">完整章節&lt;/h2>
&lt;p>Meta Pixel 自動加送的事件與它的欄位 → &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>的〈各次擷取的結果〉。以 &lt;code>data-track&lt;/code> 屬性手動埋點的感測器設計，與它和自動收集的對照 → &lt;a href="https://tarrragon.github.io/blog/devops/monitoring/03-sdk-design/frontend-sensor-design/" data-link-title="前端感測器設計" data-link-desc="什麼行為值得埋感測器、每類感測器的實作方式、取樣策略和效能影響 — 和 auto-intercept 的被動攔截互補">前端感測器設計&lt;/a>。設定檔指定的模組怎麼在網站沒呼叫時自己送事件 → &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> 的〈Meta Pixel 在瀏覽器裡做的事〉；同一類模組依 User-Agent 攔下事件的機制 → &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>的〈擷取方式〉。&lt;/p></description><content:encoded><![CDATA[<p>自動事件收集（autocapture）是由追蹤碼自己決定送出哪些事件、網站不必逐一呼叫的做法：script 在頁面上監聽點擊、捲動、表單這類互動，或在頁面載入、工作階段開始這類時點，依 vendor 的規則產生事件並送出。涵蓋哪些互動由 vendor 的規則決定，不是頁面上的每一個互動都會送。相對的做法是手動埋點：開發者在要追蹤的元素或事件處自己寫追蹤程式（標上屬性或呼叫追蹤函式），只有這些互動才會送出。可先對照 <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>（自動事件同樣帶著識別碼 cookie 的值送出）與 <a href="/blog/devops/monitoring/knowledge-cards/headless-browser/" data-link-title="Headless Browser（無頭瀏覽器）" data-link-desc="用 Playwright 或 Puppeteer 測追蹤碼、分析報表裡的機器流量，或自建統計與商業追蹤碼的訪問數對不上時">Headless Browser（無頭瀏覽器）</a>（瀏覽器裡的機器人過濾對自動事件一樣生效）。自建 SDK 在事件送出前遮蔽欄位的做法見 <a href="/blog/devops/monitoring/knowledge-cards/redaction/" data-link-title="Redaction" data-link-desc="說明在事件資料離開 client 之前把敏感欄位的值替換成遮罩或移除的機制">Redaction</a>；商業追蹤碼的自動事件由 vendor 的 script 直接送出，不經過網站自建 SDK 的遮蔽步驟，網站能用的是 vendor 自己提供的設定：GA4 在網頁資料串流上有資料遮蔽（data redaction），在瀏覽器裡、資料送出之前遮掉事件參數裡的 email 與指定的網址查詢參數；Meta Pixel 能做的是關閉自動事件（做法在本卡〈概念位置〉的 Meta Pixel 段）。</p>
<h2 id="概念位置">概念位置</h2>
<p>自動事件收集決定的是「事件從哪裡來」，位置在事件產生的那一端，比管線上的<a href="/blog/devops/monitoring/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="在事件產生階段按比例丟棄部分事件降低管線負載 — 分靜態取樣（config 固定比例）和動態取樣（背壓觸發自動降低）">取樣</a>與<a href="/blog/devops/monitoring/knowledge-cards/redaction/" data-link-title="Redaction" data-link-desc="說明在事件資料離開 client 之前把敏感欄位的值替換成遮罩或移除的機制">遮蔽</a>都早：事件的欄位由 vendor 的規則決定，網站要等它送出之後才看得到內容。它與本站自建 SDK 的<a href="/blog/devops/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">自動攔截機制</a>（auto-intercept）同樣不需要開發者為每個事件寫程式，分辨兩者的依據是送出規則由誰持有：自動攔截的規則寫在網站自己的 SDK 程式碼裡，自動事件收集的規則由 vendor 決定，網站的原始碼裡看不到。名稱各家不同：Meta 把這類事件叫自動事件（automatic events），GA4 叫自動收集的事件（automatically collected events），本站把兩者統稱為自動事件。兩家的範圍與開關也不同。</p>
<p><strong>Meta Pixel</strong> 的自動事件由 Pixel 的設定檔指定的模組產生：<code>fbevents.js</code> 載入後會向 Meta 取得這個 Pixel 的設定檔，設定檔決定要載入哪些模組（plugin，見 <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> 的〈Meta Pixel 在瀏覽器裡做的事〉）。實測時每按一次頁面上的按鈕，<code>fbevents.js</code> 都在網站呼叫的事件之外多送一筆 <code>SubscribedButtonClick</code>，內容包含按鈕文字（<code>cd[buttonText]</code>）、按鈕的 DOM 特徵與頁面特徵；設定檔來自 Meta 的伺服器，所以送出哪些自動事件由 Meta 那一端的設定決定，網站的原始碼裡看不到。要關掉 Pixel 依按鈕點擊與頁面中繼資料自動送出的事件，Meta 的開發者文件給的做法是在 Pixel 基本碼裡、<code>fbq('init', …)</code> 之前加一行 <code>fbq('set', 'autoConfig', false, '&lt;Pixel ID&gt;')</code>，<code>&lt;Pixel ID&gt;</code> 換成自己的 Pixel ID。</p>
<p><strong>GA4</strong> 的自動收集事件分成只要裝了 Google 代碼就會送的基本事件，例如 <code>first_visit</code>、<code>session_start</code>、<code>user_engagement</code>，以及由加強型評估（enhanced measurement）控制的事件。加強型評估是網頁資料串流（GA4 資源裡代表一個網站資料來源的設定單位）上的一組開關，涵蓋捲動、外連點擊、站內搜尋、影片互動、檔案下載與表單互動，其中檔案下載會帶連結文字（<code>link_text</code>）、表單送出會帶送出按鈕的文字。<code>page_view</code> 也列在加強型評估底下，而它是其中唯一關不掉的一項。這組開關在 GA4 管理介面的資料串流設定裡，需要編輯者以上的權限。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>自動事件在請求裡的樣子與網站自己呼叫的事件相同，差別只在網站的程式碼裡找不到對應的呼叫。Meta 的事件請求送到 <code>www.facebook.com/tr/</code>，事件名稱在 <code>ev</code> 參數；GA4 的事件請求送到 Google Analytics 網域下的 <code>g/collect</code>（在開發者工具的 Network 面板以 <code>collect</code> 篩選），事件名稱在 <code>en</code> 參數。一次 Meta Pixel 的實測裡，網站呼叫了 ViewContent 與 Purchase 各一次，擷取結果裡多出兩筆 <code>SubscribedButtonClick</code>，各自緊跟在一次按鈕點擊之後。</p>
<h2 id="判讀方式">判讀方式</h2>
<p>網站原始碼只列得出手動埋點的那一部分，網站實際送出的資料要從送出的請求裡數；按鈕文字、連結文字這類欄位也在送出範圍內，按鈕或連結上顯示了使用者的個人資料時，那段文字會一起送到 vendor。兩種埋點方式的取捨方向相反：手動埋點會漏掉沒有寫追蹤程式的互動，換到的是送出的欄位全部由開發者決定；自動收集不需要逐一標記，而涵蓋哪些互動、送出哪些欄位由 vendor 的規則決定、會隨 vendor 改版變動。vendor 改版不會通知網站，所以送出資料的盤點要定期重做；Meta Pixel 的 <code>fbevents.js</code> 版本可以在頁面的開發者工具 Console 輸入 <code>fbq.version</code> 讀出，但自動事件由每個 Pixel 的設定檔決定，設定檔或事件管理工具裡的設定改了，<code>fbq.version</code> 不會變，所以版本相同不代表送出的內容沒變，盤點仍要看實際送出的請求。</p>
<h2 id="完整章節">完整章節</h2>
<p>Meta Pixel 自動加送的事件與它的欄位 → <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>的〈各次擷取的結果〉。以 <code>data-track</code> 屬性手動埋點的感測器設計，與它和自動收集的對照 → <a href="/blog/devops/monitoring/03-sdk-design/frontend-sensor-design/" data-link-title="前端感測器設計" data-link-desc="什麼行為值得埋感測器、每類感測器的實作方式、取樣策略和效能影響 — 和 auto-intercept 的被動攔截互補">前端感測器設計</a>。設定檔指定的模組怎麼在網站沒呼叫時自己送事件 → <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> 的〈Meta Pixel 在瀏覽器裡做的事〉；同一類模組依 User-Agent 攔下事件的機制 → <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>
]]></content:encoded></item></channel></rss>