<?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>Frontend on Tarragon</title><link>https://tarrragon.github.io/blog/tags/frontend/</link><description>Recent content in Frontend on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/frontend/index.xml" rel="self" type="application/rss+xml"/><item><title>前端 artifact 與 preview deployment 流程</title><link>https://tarrragon.github.io/blog/ci/frontend-deploy/static-artifact-preview-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/frontend-deploy/static-artifact-preview-flow/</guid><description>&lt;p>前端 artifact 流程的核心責任是讓測試、預覽與正式發布使用同一份靜態產物。前端部署常見輸出是 HTML、CSS、JavaScript、圖片、sourcemap 與搜尋索引；這些產物一旦被重新 build，就可能受到環境變數、依賴版本、base URL 或 framework 設定影響，因此 CI/CD 需要把「產生一次、驗證一次、推進同一份」當成主線。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>前端部署的風險集中在 build time。後端服務可以在 runtime 讀取設定、檢查資料庫與逐步接流量；前端靜態產物多半在 build 階段就把 route、asset path、環境變數與 feature flag 預先寫入 bundle。CI/CD 的判讀重點因此是「被部署的 artifact 是否就是已驗證的那一份」。&lt;/p>
&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>Build&lt;/td>
 &lt;td>產生 production-like static artifact&lt;/td>
 &lt;td>lockfile、Node 版本、base URL 是否固定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Browser test&lt;/td>
 &lt;td>驗證使用者可見行為&lt;/td>
 &lt;td>測試是否跑在 build 後 artifact&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview environment&lt;/a>&lt;/td>
 &lt;td>讓 PR 變更可被 reviewer 實際操作&lt;/td>
 &lt;td>preview URL 是否對應 commit / PR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy&lt;/td>
 &lt;td>推進到 hosting、Pages 或 CDN&lt;/td>
 &lt;td>HTML cache、asset cache、SPA fallback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy&lt;/a>&lt;/td>
 &lt;td>重新服務上一份已知可用 artifact&lt;/td>
 &lt;td>舊 artifact、cache purge 與 API 相容性&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責建立可驗證產物。真實服務裡，&lt;code>npm run dev&lt;/code> 成功不代表 production build 能成功；CI 應固定 Node 版本、package manager、lockfile、build command 與必要環境變數，讓 artifact 可以從乾淨環境重建。&lt;/p>
&lt;p>Browser test 階段負責驗證使用者實際會看到的頁面。Playwright、visual diff、a11y check 或 smoke test 應盡量對 build 後的靜態站執行，避免 dev server 的 fallback、熱更新或寬鬆路由遮蔽 production 問題。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview environment&lt;/a> 階段負責把 PR 變成可操作畫面。Preview URL 要能追到 PR、commit 與 workflow run，reviewer 才能把畫面問題回報到正確版本；preview 也要隔離 production 資料與 credential，避免預覽環境變成未受控入口。&lt;/p>
&lt;p>Deploy 階段負責把 artifact 放到 hosting 或 CDN。前端部署失敗常出現在 cache policy、SPA fallback、base URL、static route 與 sourcemap 權限；deploy 成功只代表檔案上傳完成，仍需要檢查入口頁、核心路由與 asset 是否能從公開網址載入。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy&lt;/a> 階段負責恢復上一份可用靜態產物。前端 rollback 表面上只是切回舊檔案，但若 API schema、build time config 或 CDN cache 已經變動，舊頁面仍可能呼叫不相容的後端，因此 rollback 要搭配 smoke test 與 cache purge。&lt;/p>
&lt;h2 id="常見失敗路由">常見失敗路由&lt;/h2>
&lt;p>前端 CI 紅燈要先判斷失敗在 build、browser test、preview 還是 production deploy。不同層的修復入口不同；把所有紅燈都當成「重跑 workflow」會掩蓋 artifact 漂移與 cache 問題。&lt;/p>
&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>本機 dev 正常、CI build 失敗&lt;/td>
 &lt;td>production build 條件與本機不同&lt;/td>
 &lt;td>回本機跑 CI 同一條 build command&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>測試通過、上線後空白頁&lt;/td>
 &lt;td>測試沒有覆蓋 production artifact / URL&lt;/td>
 &lt;td>對已部署 artifact 跑 smoke test&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Preview URL 顯示舊畫面&lt;/td>
 &lt;td>preview cache 或 commit 對應錯位&lt;/td>
 &lt;td>檢查 preview artifact 與 workflow run&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>只有深層路由 404&lt;/td>
 &lt;td>SPA fallback 或 static route 設定錯誤&lt;/td>
 &lt;td>檢查 hosting rewrite / base URL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rollback 後仍看到新版&lt;/td>
 &lt;td>CDN / browser cache 尚未失效&lt;/td>
 &lt;td>檢查 cache invalidation 與 HTML cache policy&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的用途是縮短定位時間。前端部署問題常被誤判成「CDN 壞掉」或「瀏覽器快取」，但更常見的根因是 build artifact、route 與 cache policy 的契約沒有明確寫進 pipeline。&lt;/p></description><content:encoded><![CDATA[<p>前端 artifact 流程的核心責任是讓測試、預覽與正式發布使用同一份靜態產物。前端部署常見輸出是 HTML、CSS、JavaScript、圖片、sourcemap 與搜尋索引；這些產物一旦被重新 build，就可能受到環境變數、依賴版本、base URL 或 framework 設定影響，因此 CI/CD 需要把「產生一次、驗證一次、推進同一份」當成主線。</p>
<h2 id="流程定位">流程定位</h2>
<p>前端部署的風險集中在 build time。後端服務可以在 runtime 讀取設定、檢查資料庫與逐步接流量；前端靜態產物多半在 build 階段就把 route、asset path、環境變數與 feature flag 預先寫入 bundle。CI/CD 的判讀重點因此是「被部署的 artifact 是否就是已驗證的那一份」。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>產生 production-like static artifact</td>
          <td>lockfile、Node 版本、base URL 是否固定</td>
      </tr>
      <tr>
          <td>Browser test</td>
          <td>驗證使用者可見行為</td>
          <td>測試是否跑在 build 後 artifact</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview environment</a></td>
          <td>讓 PR 變更可被 reviewer 實際操作</td>
          <td>preview URL 是否對應 commit / PR</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>推進到 hosting、Pages 或 CDN</td>
          <td>HTML cache、asset cache、SPA fallback</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy</a></td>
          <td>重新服務上一份已知可用 artifact</td>
          <td>舊 artifact、cache purge 與 API 相容性</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責建立可驗證產物。真實服務裡，<code>npm run dev</code> 成功不代表 production build 能成功；CI 應固定 Node 版本、package manager、lockfile、build command 與必要環境變數，讓 artifact 可以從乾淨環境重建。</p>
<p>Browser test 階段負責驗證使用者實際會看到的頁面。Playwright、visual diff、a11y check 或 smoke test 應盡量對 build 後的靜態站執行，避免 dev server 的 fallback、熱更新或寬鬆路由遮蔽 production 問題。</p>
<p><a href="/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">Preview environment</a> 階段負責把 PR 變成可操作畫面。Preview URL 要能追到 PR、commit 與 workflow run，reviewer 才能把畫面問題回報到正確版本；preview 也要隔離 production 資料與 credential，避免預覽環境變成未受控入口。</p>
<p>Deploy 階段負責把 artifact 放到 hosting 或 CDN。前端部署失敗常出現在 cache policy、SPA fallback、base URL、static route 與 sourcemap 權限；deploy 成功只代表檔案上傳完成，仍需要檢查入口頁、核心路由與 asset 是否能從公開網址載入。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback strategy</a> 階段負責恢復上一份可用靜態產物。前端 rollback 表面上只是切回舊檔案，但若 API schema、build time config 或 CDN cache 已經變動，舊頁面仍可能呼叫不相容的後端，因此 rollback 要搭配 smoke test 與 cache purge。</p>
<h2 id="常見失敗路由">常見失敗路由</h2>
<p>前端 CI 紅燈要先判斷失敗在 build、browser test、preview 還是 production deploy。不同層的修復入口不同；把所有紅燈都當成「重跑 workflow」會掩蓋 artifact 漂移與 cache 問題。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本機 dev 正常、CI build 失敗</td>
          <td>production build 條件與本機不同</td>
          <td>回本機跑 CI 同一條 build command</td>
      </tr>
      <tr>
          <td>測試通過、上線後空白頁</td>
          <td>測試沒有覆蓋 production artifact / URL</td>
          <td>對已部署 artifact 跑 smoke test</td>
      </tr>
      <tr>
          <td>Preview URL 顯示舊畫面</td>
          <td>preview cache 或 commit 對應錯位</td>
          <td>檢查 preview artifact 與 workflow run</td>
      </tr>
      <tr>
          <td>只有深層路由 404</td>
          <td>SPA fallback 或 static route 設定錯誤</td>
          <td>檢查 hosting rewrite / base URL</td>
      </tr>
      <tr>
          <td>rollback 後仍看到新版</td>
          <td>CDN / browser cache 尚未失效</td>
          <td>檢查 cache invalidation 與 HTML cache policy</td>
      </tr>
  </tbody>
</table>
<p>這張表的用途是縮短定位時間。前端部署問題常被誤判成「CDN 壞掉」或「瀏覽器快取」，但更常見的根因是 build artifact、route 與 cache policy 的契約沒有明確寫進 pipeline。</p>
<h2 id="最小-workflow-骨架">最小 workflow 骨架</h2>
<p>前端 workflow 應把 build、test、preview 與 deploy 的資料流顯性化。下面是概念骨架，重點在 artifact handoff 的方向，特定平台語法是次要的。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm ci</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm run build</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/upload-artifact</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-dist</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">          </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">dist</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">build</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/download-artifact</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-dist</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm run test:e2e:static</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">  </span><span class="nt">preview</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">github.event_name == &#39;pull_request&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/download-artifact</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-dist</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm run deploy:preview</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">github.ref == &#39;refs/heads/main&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/download-artifact</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-dist</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm run deploy:production</span></span></span></code></pre></div><p>這個骨架讓 deploy 依賴 test，也讓 test 與 deploy 使用 build job 產生的同一份產物。若專案需要在不同環境注入設定，要明確區分 build time config 與 runtime config，避免同一份 artifact 被重新 build 成另一份內容。</p>
<h2 id="tripwire">Tripwire</h2>
<p>Tripwire 的責任是提醒前端 workflow 需要重切。當同一類問題反覆出現，局部補命令通常只能暫時遮住資料流錯位。</p>
<ul>
<li>Preview 常和 production 不一致：把 preview 改成部署 build artifact，讓 preview job 沿用同一份產物。</li>
<li>E2E 測試通過但 production 壞：把 E2E 改到 static artifact 或 production-like server 上執行。</li>
<li>rollback 依賴人工找舊 commit：保留 release artifact 與版本索引，讓回退指向明確產物。</li>
<li>CDN cache 問題反覆出現：把 HTML cache、asset cache 與 purge 策略寫進 deploy checklist。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>前端部署總覽：回 <a href="../">前端部署 CI/CD</a>。</li>
<li>Gate 原理：讀 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>本 blog 靜態站案例：讀 <a href="../../blog-project-deploy/">本 blog 專案部署</a>。</li>
</ul>
]]></content:encoded></item><item><title>前端感測器設計</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/frontend-sensor-design/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/frontend-sensor-design/</guid><description>&lt;p>感測器是 SDK 主動偵測使用者行為的元件。和 &lt;a href="https://tarrragon.github.io/blog/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 攔截的是系統級事件（uncaught exception、unhandled rejection），感測器偵測的是業務級行為（使用者點了什麼、看了哪個畫面、操作花了多久）。兩者互補：auto-intercept 提供 error 和 lifecycle 的基礎層，感測器提供 event 和 metric 的業務層。&lt;/p>
&lt;h2 id="點擊觸碰感測器">點擊/觸碰感測器&lt;/h2>
&lt;p>點擊感測器偵測使用者和 UI 元素的互動 — 按鈕點擊、連結觸碰、選單選擇。每次互動產生一個 event 類型的事件。&lt;/p>
&lt;h3 id="哪些元素值得追蹤">哪些元素值得追蹤&lt;/h3>
&lt;p>追蹤粒度的判斷依據是「這個互動是否對應一個有意義的使用者意圖」。&lt;/p>
&lt;p>有意義的互動（值得追蹤）：提交表單、點擊導航按鈕、觸發功能操作（連線、配對、匯出）。這些互動對應使用者的明確意圖，是 &lt;a href="https://tarrragon.github.io/blog/monitoring/08-business-analytics/funnel-analysis/" data-link-title="Funnel Analysis" data-link-desc="使用者在哪一步流失 — 從事件序列計算每步轉換率、找出流失最嚴重的步驟、區分設計問題和技術問題">funnel 分析&lt;/a> 的步驟候選。&lt;/p>
&lt;p>低價值的互動（通常不追蹤）：滾動、hover、重複的相同操作（每秒多次的按鈕連按）。這些互動要麼太頻繁（滾動每秒觸發數十次），要麼不代表新的使用者意圖。&lt;/p>
&lt;h3 id="實作方式">實作方式&lt;/h3>
&lt;p>&lt;strong>Web（JS/TS）&lt;/strong>：在 document 層級用 event delegation 攔截 click 事件，過濾出帶 &lt;code>data-track&lt;/code> attribute 的元素。開發者在需要追蹤的元素上加 &lt;code>data-track=&amp;quot;connect-button&amp;quot;&lt;/code>，感測器自動收集。不追蹤所有 click — 只追蹤被標記的。&lt;/p>
&lt;p>&lt;strong>Flutter&lt;/strong>：用 NavigatorObserver 或 custom GestureDetector wrapper。GestureDetector 包裝在需要追蹤的 widget 外層，onTap 觸發時送出事件。&lt;/p>
&lt;h3 id="效能影響">效能影響&lt;/h3>
&lt;p>Event delegation 在 document 層級只有一個 listener，效能影響接近零。瓶頸在事件產生頻率 — 如果追蹤了高頻操作（每秒多次的滑動），事件進入 buffer 的速度可能超過 flush 的速度。用取樣控制（見本章末段）。&lt;/p>
&lt;h2 id="導航路由感測器">導航/路由感測器&lt;/h2>
&lt;p>導航感測器偵測使用者在不同畫面之間的切換 — page view、screen view、route change。每次切換產生一個 lifecycle 類型的事件。&lt;/p>
&lt;h3 id="平台差異">平台差異&lt;/h3>
&lt;p>&lt;strong>Web SPA&lt;/strong>：SPA 的 route 變換不觸發頁面載入，需要主動偵測 URL 變化。兩種偵測方式：&lt;/p>
&lt;ul>
&lt;li>History API 攔截：覆寫 &lt;code>pushState&lt;/code> / &lt;code>replaceState&lt;/code>，攔截 &lt;code>popstate&lt;/code> 事件&lt;/li>
&lt;li>框架層級 Hook：React Router 的 &lt;code>useLocation&lt;/code>、Vue Router 的 &lt;code>afterEach&lt;/code> guard&lt;/li>
&lt;/ul>
&lt;p>History API 攔截是 SDK 層的通用做法（不依賴框架）；框架 Hook 更精確但需要使用者整合（見 &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/js-ts-platform/" data-link-title="JS/TS 平台適配" data-link-desc="CORS 限制、Service Worker 攔截、SPA 路由變換偵測 — 瀏覽器環境中 SDK 需要處理的平台特殊問題">JS/TS 平台&lt;/a> 的 SPA 路由段）。&lt;/p>
&lt;p>&lt;strong>Flutter&lt;/strong>：用 &lt;code>NavigatorObserver&lt;/code> 的 &lt;code>didPush&lt;/code> / &lt;code>didPop&lt;/code> / &lt;code>didReplace&lt;/code> 回呼。每次路由變化自動觸發，不需要使用者在每個頁面手動埋點。&lt;/p>
&lt;p>&lt;strong>Python CLI/Hook&lt;/strong>：沒有「畫面切換」的概念。對應的 lifecycle 事件是 &lt;code>hook.start&lt;/code> / &lt;code>hook.complete&lt;/code> — 每個 Hook 執行視為一個「畫面」。&lt;/p>
&lt;h3 id="事件-schema">事件 schema&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;lifecycle&amp;#34;&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="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;screen.view&amp;#34;&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="nt">&amp;#34;data&amp;#34;&lt;/span>&lt;span class="p">:&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="nt">&amp;#34;screen_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;TerminalScreen&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;previous_screen&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;HomeScreen&amp;#34;&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="nt">&amp;#34;navigation_method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;push&amp;#34;&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;span class="line">&lt;span class="ln">9&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>navigation_method&lt;/code>（push / pop / replace / go）記錄導航方式，和 &lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/go-push-semantics/" data-link-title="go vs push vs pushReplacement 的 UX 語意表" data-link-desc="三種導航方法對堆疊、back 行為、使用者心理模型的影響 — 選擇依據是使用者的意圖而非技術方便">go vs push 的 UX 語意&lt;/a> 對應。&lt;/p>
&lt;h2 id="錯誤邊界感測器">錯誤邊界感測器&lt;/h2>
&lt;p>錯誤邊界感測器攔截元件級的 error — 和 auto-intercept 的全域 error 攔截互補。&lt;/p></description><content:encoded><![CDATA[<p>感測器是 SDK 主動偵測使用者行為的元件。和 <a href="/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">自動攔截機制</a> 的被動攔截不同 — auto-intercept 攔截的是系統級事件（uncaught exception、unhandled rejection），感測器偵測的是業務級行為（使用者點了什麼、看了哪個畫面、操作花了多久）。兩者互補：auto-intercept 提供 error 和 lifecycle 的基礎層，感測器提供 event 和 metric 的業務層。</p>
<h2 id="點擊觸碰感測器">點擊/觸碰感測器</h2>
<p>點擊感測器偵測使用者和 UI 元素的互動 — 按鈕點擊、連結觸碰、選單選擇。每次互動產生一個 event 類型的事件。</p>
<h3 id="哪些元素值得追蹤">哪些元素值得追蹤</h3>
<p>追蹤粒度的判斷依據是「這個互動是否對應一個有意義的使用者意圖」。</p>
<p>有意義的互動（值得追蹤）：提交表單、點擊導航按鈕、觸發功能操作（連線、配對、匯出）。這些互動對應使用者的明確意圖，是 <a href="/blog/monitoring/08-business-analytics/funnel-analysis/" data-link-title="Funnel Analysis" data-link-desc="使用者在哪一步流失 — 從事件序列計算每步轉換率、找出流失最嚴重的步驟、區分設計問題和技術問題">funnel 分析</a> 的步驟候選。</p>
<p>低價值的互動（通常不追蹤）：滾動、hover、重複的相同操作（每秒多次的按鈕連按）。這些互動要麼太頻繁（滾動每秒觸發數十次），要麼不代表新的使用者意圖。</p>
<h3 id="實作方式">實作方式</h3>
<p><strong>Web（JS/TS）</strong>：在 document 層級用 event delegation 攔截 click 事件，過濾出帶 <code>data-track</code> attribute 的元素。開發者在需要追蹤的元素上加 <code>data-track=&quot;connect-button&quot;</code>，感測器自動收集。不追蹤所有 click — 只追蹤被標記的。</p>
<p><strong>Flutter</strong>：用 NavigatorObserver 或 custom GestureDetector wrapper。GestureDetector 包裝在需要追蹤的 widget 外層，onTap 觸發時送出事件。</p>
<h3 id="效能影響">效能影響</h3>
<p>Event delegation 在 document 層級只有一個 listener，效能影響接近零。瓶頸在事件產生頻率 — 如果追蹤了高頻操作（每秒多次的滑動），事件進入 buffer 的速度可能超過 flush 的速度。用取樣控制（見本章末段）。</p>
<h2 id="導航路由感測器">導航/路由感測器</h2>
<p>導航感測器偵測使用者在不同畫面之間的切換 — page view、screen view、route change。每次切換產生一個 lifecycle 類型的事件。</p>
<h3 id="平台差異">平台差異</h3>
<p><strong>Web SPA</strong>：SPA 的 route 變換不觸發頁面載入，需要主動偵測 URL 變化。兩種偵測方式：</p>
<ul>
<li>History API 攔截：覆寫 <code>pushState</code> / <code>replaceState</code>，攔截 <code>popstate</code> 事件</li>
<li>框架層級 Hook：React Router 的 <code>useLocation</code>、Vue Router 的 <code>afterEach</code> guard</li>
</ul>
<p>History API 攔截是 SDK 層的通用做法（不依賴框架）；框架 Hook 更精確但需要使用者整合（見 <a href="/blog/monitoring/05-platform-adaptation/js-ts-platform/" data-link-title="JS/TS 平台適配" data-link-desc="CORS 限制、Service Worker 攔截、SPA 路由變換偵測 — 瀏覽器環境中 SDK 需要處理的平台特殊問題">JS/TS 平台</a> 的 SPA 路由段）。</p>
<p><strong>Flutter</strong>：用 <code>NavigatorObserver</code> 的 <code>didPush</code> / <code>didPop</code> / <code>didReplace</code> 回呼。每次路由變化自動觸發，不需要使用者在每個頁面手動埋點。</p>
<p><strong>Python CLI/Hook</strong>：沒有「畫面切換」的概念。對應的 lifecycle 事件是 <code>hook.start</code> / <code>hook.complete</code> — 每個 Hook 執行視為一個「畫面」。</p>
<h3 id="事件-schema">事件 schema</h3>





<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;type&#34;</span><span class="p">:</span> <span class="s2">&#34;lifecycle&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;screen.view&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</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">5</span><span class="cl">    <span class="nt">&#34;screen_name&#34;</span><span class="p">:</span> <span class="s2">&#34;TerminalScreen&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nt">&#34;previous_screen&#34;</span><span class="p">:</span> <span class="s2">&#34;HomeScreen&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nt">&#34;navigation_method&#34;</span><span class="p">:</span> <span class="s2">&#34;push&#34;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>navigation_method</code>（push / pop / replace / go）記錄導航方式，和 <a href="/blog/ux-design/05-navigation-patterns/go-push-semantics/" data-link-title="go vs push vs pushReplacement 的 UX 語意表" data-link-desc="三種導航方法對堆疊、back 行為、使用者心理模型的影響 — 選擇依據是使用者的意圖而非技術方便">go vs push 的 UX 語意</a> 對應。</p>
<h2 id="錯誤邊界感測器">錯誤邊界感測器</h2>
<p>錯誤邊界感測器攔截元件級的 error — 和 auto-intercept 的全域 error 攔截互補。</p>
<h3 id="和-auto-intercept-的職責分工">和 auto-intercept 的職責分工</h3>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>機制</th>
          <th>攔截什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全域</td>
          <td>auto-intercept（<code>window.onerror</code> / <code>FlutterError.onError</code>）</td>
          <td>uncaught exception、未處理的 Promise rejection</td>
      </tr>
      <tr>
          <td>元件</td>
          <td>錯誤邊界感測器（React ErrorBoundary / Flutter Widget error handler）</td>
          <td>元件渲染失敗、子樹 error</td>
      </tr>
  </tbody>
</table>
<p>全域攔截捕獲「逃逸到頂層的 error」，錯誤邊界捕獲「在元件層級就被攔住的 error」。如果一個 error 被元件的 ErrorBoundary 捕獲，它不會觸發 <code>window.onerror</code> — auto-intercept 看不到它。錯誤邊界感測器填補這個缺口。</p>
<h3 id="實作方式-1">實作方式</h3>
<p><strong>React</strong>：ErrorBoundary 元件的 <code>componentDidCatch</code> 回呼中呼叫 <code>monitor.error()</code>。</p>
<p><strong>Flutter</strong>：在 Widget 層用 <code>ErrorWidget.builder</code> 或自訂的 error handling widget。</p>
<h3 id="額外-context">額外 context</h3>
<p>錯誤邊界感測器比全域攔截多一個 context — 知道 error 發生在哪個元件（component name / widget name）。這個資訊在 error 的 data schema 中記錄為 <code>component</code> 欄位。</p>
<h2 id="效能標記感測器">效能標記感測器</h2>
<p>效能標記感測器量測操作的延遲和系統的渲染表現。產生 metric 類型的事件。</p>
<h3 id="web-core-vitals">Web Core Vitals</h3>
<p>Web 平台用 <code>PerformanceObserver</code> API 自動收集三個核心指標：</p>
<ul>
<li><strong>LCP</strong>（Largest Contentful Paint）：最大內容元素的載入時間</li>
<li><strong>FID</strong>（First Input Delay）：首次互動的延遲</li>
<li><strong>CLS</strong>（Cumulative Layout Shift）：累計佈局位移分數</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">new</span> <span class="nx">PerformanceObserver</span><span class="p">((</span><span class="nx">list</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">entry</span> <span class="k">of</span> <span class="nx">list</span><span class="p">.</span><span class="nx">getEntries</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">monitor</span><span class="p">.</span><span class="nx">metric</span><span class="p">(</span><span class="sb">`web.vitals.</span><span class="si">${</span><span class="nx">entry</span><span class="p">.</span><span class="nx">entryType</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">      <span class="nx">value</span><span class="o">:</span> <span class="nx">entry</span><span class="p">.</span><span class="nx">startTime</span> <span class="o">||</span> <span class="nx">entry</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">      <span class="nx">url</span><span class="o">:</span> <span class="nx">location</span><span class="p">.</span><span class="nx">pathname</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}).</span><span class="nx">observe</span><span class="p">({</span> <span class="nx">type</span><span class="o">:</span> <span class="s1">&#39;largest-contentful-paint&#39;</span><span class="p">,</span> <span class="nx">buffered</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>實務上依 entryType 分別取值（LCP 用 <code>startTime</code>、CLS 用 <code>value</code>、FID 用 <code>processingStart - startTime</code>），上述範例簡化示意。</p>
<h3 id="flutter-frame-timing">Flutter frame timing</h3>
<p>Flutter 用 <code>SchedulerBinding.addTimingsCallback</code> 偵測掉幀：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">SchedulerBinding</span><span class="p">.</span><span class="n">instance</span><span class="p">.</span><span class="n">addTimingsCallback</span><span class="p">((</span><span class="n">timings</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="k">for</span> <span class="p">(</span><span class="kd">final</span> <span class="n">t</span> <span class="k">in</span> <span class="n">timings</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">t</span><span class="p">.</span><span class="n">totalSpan</span> <span class="o">&gt;</span> <span class="kd">const</span> <span class="n">Duration</span><span class="p">(</span><span class="nl">milliseconds:</span> <span class="m">16</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">      <span class="n">monitor</span><span class="p">.</span><span class="n">metric</span><span class="p">(</span><span class="s1">&#39;render.frame_drop&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="s1">&#39;build_ms&#39;</span><span class="o">:</span> <span class="n">t</span><span class="p">.</span><span class="n">buildDuration</span><span class="p">.</span><span class="n">inMilliseconds</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="s1">&#39;raster_ms&#39;</span><span class="o">:</span> <span class="n">t</span><span class="p">.</span><span class="n">rasterDuration</span><span class="p">.</span><span class="n">inMilliseconds</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>16ms 是 60fps 的單幀預算。超過代表掉幀。</p>
<h3 id="自訂-duration-量測">自訂 duration 量測</h3>
<p>業務操作的延遲用手動標記量測：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">final</span> <span class="n">stopwatch</span> <span class="o">=</span> <span class="n">Stopwatch</span><span class="p">()..</span><span class="n">start</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">await</span> <span class="n">connectToTerminal</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">stopwatch</span><span class="p">.</span><span class="n">stop</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">monitor</span><span class="p">.</span><span class="n">metric</span><span class="p">(</span><span class="s1">&#39;terminal.connect.duration&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="s1">&#39;duration_ms&#39;</span><span class="o">:</span> <span class="n">stopwatch</span><span class="p">.</span><span class="n">elapsedMilliseconds</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><h2 id="輸入敏感度感測器">輸入敏感度感測器</h2>
<p>輸入敏感度感測器偵測使用者正在輸入敏感資料 — 密碼欄位、API key 輸入、信用卡號碼。這個感測器的責任是<strong>觸發 redaction，而非記錄輸入內容</strong>。</p>
<h3 id="偵測邏輯">偵測邏輯</h3>
<p><strong>Web</strong>：偵測 <code>&lt;input type=&quot;password&quot;&gt;</code>、帶有 <code>autocomplete=&quot;cc-number&quot;</code> 或 <code>data-sensitive</code> attribute 的欄位。當使用者 focus 這些欄位時，標記當前 session 進入「敏感輸入模式」— 後續的事件自動加嚴 <a href="/blog/monitoring/knowledge-cards/redaction/" data-link-title="Redaction" data-link-desc="說明在事件資料離開 client 之前把敏感欄位的值替換成遮罩或移除的機制">redaction</a> 規則（例如暫停記錄按鍵事件）。</p>
<p><strong>Flutter</strong>：偵測 <code>TextField</code> 的 <code>obscureText: true</code> 或 <code>enableIMEPersonalizedLearning: false</code>（見 <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">安全敏感輸入框的 IME 控制</a>）。</p>
<h3 id="不記錄的原則">不記錄的原則</h3>
<p>輸入敏感度感測器偵測「使用者正在輸入敏感內容」這個事實，但不記錄輸入的內容本身。送出的事件只包含：</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;type&#34;</span><span class="p">:</span> <span class="s2">&#34;lifecycle&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;input.sensitive_mode.entered&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;field_type&#34;</span><span class="p">:</span> <span class="s2">&#34;password&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="取樣策略設計">取樣策略設計</h2>
<p>感測器產生的事件量可能很大（效能標記每 30 秒一筆 × 活躍使用者數）。取樣控制事件量、避免 SDK 和 collector 的資源壓力。</p>
<h3 id="三種取樣模式">三種取樣模式</h3>
<p><strong>全收</strong>：每筆事件都送出。適合事件量低且每筆都有價值的類型 — error（每筆都可能是新 bug）、lifecycle 狀態轉換（量低）、認證失敗（安全敏感）。</p>
<p><strong>百分比取樣</strong>：隨機丟棄一定比例的事件。適合高頻的效能和行為事件。取樣率由 SDK config 控制：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">sensors</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">metric</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">render.frame_drop</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">sampling</span><span class="p">:</span><span class="w"> </span><span class="m">0.1</span><span class="w"> </span>}<span class="w">    </span><span class="c"># 只收 10%</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">resource.memory</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">sampling</span><span class="p">:</span><span class="w"> </span><span class="m">0.5</span><span class="w"> </span>}<span class="w">       </span><span class="c"># 收 50%</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">event</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="nt">feature.*.used</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">sampling</span><span class="p">:</span><span class="w"> </span><span class="m">1.0</span><span class="w"> </span>}<span class="w">        </span><span class="c"># 全收</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="nt">click.*</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">sampling</span><span class="p">:</span><span class="w"> </span><span class="m">0.1</span><span class="w"> </span>}<span class="w">               </span><span class="c"># 只收 10%</span></span></span></code></pre></div><p>百分比取樣的代價是低機率事件可能被漏掉（取樣 10% 時、發生 5 次的事件可能一次都沒收到）。</p>
<p><strong>條件取樣</strong>：正常情況下取樣、特定條件下全收。適合「平時不需要全量但問題發生時需要完整資料」的場景。例：正常 session 取樣 10%、但 session 內發生 error 後、該 session 剩餘事件全收（error session 的完整 context 比正常 session 更有價值）。</p>
<h3 id="取樣率的管理">取樣率的管理</h3>
<p>取樣率可以從三個層級設定：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>設定方式</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SDK 本地 config</td>
          <td>隨 app 版本部署</td>
          <td>固定的基線取樣率</td>
      </tr>
      <tr>
          <td>Collector 下發</td>
          <td>SDK 啟動時從 collector 取得 config</td>
          <td>動態調整、不需要重新部署 app</td>
      </tr>
      <tr>
          <td>Feature flag 服務</td>
          <td>整合 LaunchDarkly / Unleash</td>
          <td>實驗期間對特定群組調整取樣</td>
      </tr>
  </tbody>
</table>
<p>三個層級由上到下優先順序遞增 — feature flag 覆蓋 collector config、collector config 覆蓋本地 config。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>動機驅動的事件設計（哪些動機需要哪些感測器） → <a href="/blog/monitoring/01-mental-model/motivation-to-event-mapping/" data-link-title="動機驅動的事件設計" data-link-desc="Debug / 商業 / 資安 / 效能四個動機各自需要什麼事件 — 從「為什麼收」反推「收什麼」和「什麼階段啟用」">動機驅動的事件設計</a></li>
<li>感測器的啟停控制和生命週期 → <a href="/blog/monitoring/03-sdk-design/sensor-lifecycle-management/" data-link-title="感測器生命週期管理" data-link-desc="產品生命週期的五個階段各啟用什麼感測器 — feature flag 整合、取樣率動態調整、感測器開關的可觀察性">感測器生命週期管理</a></li>
<li>被動攔截機制（和感測器互補） → <a href="/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">自動攔截機制</a></li>
<li>安全敏感輸入的完整 checklist → <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">安全敏感輸入框的 IME 控制</a></li>
</ul>
]]></content:encoded></item><item><title>前端部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/frontend-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/frontend-deploy/</guid><description>&lt;p>前端部署 CI/CD 的核心責任是把瀏覽器可執行的靜態產物安全交付到 hosting、CDN 或 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">preview environment&lt;/a>。前端部署常見輸出是 HTML、CSS、JavaScript、圖片與搜尋索引；它的風險集中在 build &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact&lt;/a>、路由、cache、環境變數與使用者可見回歸。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>前端部署和後端部署的差異在於 runtime 責任位置。前端產物通常在 build time 完成大部分工作，發布後由 browser、CDN 或 static hosting 提供服務；後端服務則要在 runtime 處理連線、資料庫、migration、狀態與 rollback。&lt;/p>
&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>Build&lt;/td>
 &lt;td>bundle、static site、asset hashing&lt;/td>
 &lt;td>build 是否可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Test&lt;/td>
 &lt;td>browser regression、a11y、layout&lt;/td>
 &lt;td>Playwright / visual diff 是否通過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact&lt;/a>&lt;/td>
 &lt;td>static files、search index、sourcemap&lt;/td>
 &lt;td>測試與發布是否同一份產物&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Deploy&lt;/td>
 &lt;td>hosting、CDN、Pages、preview URL&lt;/td>
 &lt;td>cache invalidation 與路由是否正確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a>&lt;/td>
 &lt;td>回退前一版 static artifact&lt;/td>
 &lt;td>是否保留可回復版本&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責產生 browser 實際會執行的內容。真實服務常見訊號是 bundle size、asset hash、base URL、環境變數與 static route 是否穩定；若 build 只能在開發機成功，CI 就要把 Node 版本、package lock、build command 與環境變數收斂成固定入口。&lt;/p>
&lt;p>Test 階段負責驗證使用者可見行為。前端常見測試包含 component test、browser regression、accessibility check 與 layout check；測試應盡量靠近 production artifact，讓 dev server 的寬鬆行為不會蓋掉實際部署問題。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact&lt;/a> 階段負責保存可發布產物。靜態檔、搜尋索引與 sourcemap 都可能影響使用者體驗與除錯能力；測試與發布共用同一份 artifact，可以避免「測試通過的是 A，發布出去的是 B」的漂移。&lt;/p>
&lt;p>Deploy 階段負責把 artifact 放到 hosting 或 CDN。真實風險通常集中在 HTML cache、asset cache、SPA fallback、preview URL 與 production domain 是否對齊。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy&lt;/a> 階段負責讓上一個可用 artifact 能重新服務使用者。前端 rollback 通常比後端快，但若 build time 環境變數、資料 schema 或 CDN cache 已變更，回退仍需要驗證頁面路由與 API 相容性。&lt;/p>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>CDN cache 要和 asset hash、HTML cache policy 分開看。&lt;/li>
&lt;li>Preview environment 要能對應 PR，讓 reviewer 看到真實 build。&lt;/li>
&lt;li>前端測試要跑在 production-like artifact 上，避免 dev server 行為遮蔽問題。&lt;/li>
&lt;li>環境變數若在 build time 注入，重新發布才會生效。&lt;/li>
&lt;li>SPA route 需要 fallback 設定，靜態站 route 需要檔案路徑與 base URL 對齊。&lt;/li>
&lt;/ul>
&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="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程&lt;/a>&lt;/td>
 &lt;td>Static artifact and preview&lt;/td>
 &lt;td>串起 build、browser test、preview 與 rollback&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>前端 artifact 流程：讀 &lt;a href="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程&lt;/a>。&lt;/li>
&lt;li>本 blog 的靜態站案例：讀 &lt;a href="../blog-project-deploy/">本 blog 專案部署&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>失敗處理：讀 &lt;a href="../github-actions-failure-flow/">CI 失敗到修復發布流程&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>前端部署 CI/CD 的核心責任是把瀏覽器可執行的靜態產物安全交付到 hosting、CDN 或 <a href="/blog/ci/knowledge-cards/preview-environment/" data-link-title="Preview Environment" data-link-desc="說明 pull request 變更如何在隔離部署環境中被驗證">preview environment</a>。前端部署常見輸出是 HTML、CSS、JavaScript、圖片與搜尋索引；它的風險集中在 build <a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">artifact</a>、路由、cache、環境變數與使用者可見回歸。</p>
<h2 id="場域定位">場域定位</h2>
<p>前端部署和後端部署的差異在於 runtime 責任位置。前端產物通常在 build time 完成大部分工作，發布後由 browser、CDN 或 static hosting 提供服務；後端服務則要在 runtime 處理連線、資料庫、migration、狀態與 rollback。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>前端部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>bundle、static site、asset hashing</td>
          <td>build 是否可重現</td>
      </tr>
      <tr>
          <td>Test</td>
          <td>browser regression、a11y、layout</td>
          <td>Playwright / visual diff 是否通過</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact</a></td>
          <td>static files、search index、sourcemap</td>
          <td>測試與發布是否同一份產物</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>hosting、CDN、Pages、preview URL</td>
          <td>cache invalidation 與路由是否正確</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a></td>
          <td>回退前一版 static artifact</td>
          <td>是否保留可回復版本</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責產生 browser 實際會執行的內容。真實服務常見訊號是 bundle size、asset hash、base URL、環境變數與 static route 是否穩定；若 build 只能在開發機成功，CI 就要把 Node 版本、package lock、build command 與環境變數收斂成固定入口。</p>
<p>Test 階段負責驗證使用者可見行為。前端常見測試包含 component test、browser regression、accessibility check 與 layout check；測試應盡量靠近 production artifact，讓 dev server 的寬鬆行為不會蓋掉實際部署問題。</p>
<p><a href="/blog/ci/knowledge-cards/artifact/" data-link-title="Artifact" data-link-desc="說明 CI/CD 中可被驗證、保存與發布的交付產物">Artifact</a> 階段負責保存可發布產物。靜態檔、搜尋索引與 sourcemap 都可能影響使用者體驗與除錯能力；測試與發布共用同一份 artifact，可以避免「測試通過的是 A，發布出去的是 B」的漂移。</p>
<p>Deploy 階段負責把 artifact 放到 hosting 或 CDN。真實風險通常集中在 HTML cache、asset cache、SPA fallback、preview URL 與 production domain 是否對齊。</p>
<p><a href="/blog/ci/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明發布異常時如何快速回到已知可用狀態">Rollback Strategy</a> 階段負責讓上一個可用 artifact 能重新服務使用者。前端 rollback 通常比後端快，但若 build time 環境變數、資料 schema 或 CDN cache 已變更，回退仍需要驗證頁面路由與 API 相容性。</p>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>CDN cache 要和 asset hash、HTML cache policy 分開看。</li>
<li>Preview environment 要能對應 PR，讓 reviewer 看到真實 build。</li>
<li>前端測試要跑在 production-like artifact 上，避免 dev server 行為遮蔽問題。</li>
<li>環境變數若在 build time 注入，重新發布才會生效。</li>
<li>SPA route 需要 fallback 設定，靜態站 route 需要檔案路徑與 base URL 對齊。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程</a></td>
          <td>Static artifact and preview</td>
          <td>串起 build、browser test、preview 與 rollback</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>前端 artifact 流程：讀 <a href="static-artifact-preview-flow/">前端 artifact 與 preview deployment 流程</a>。</li>
<li>本 blog 的靜態站案例：讀 <a href="../blog-project-deploy/">本 blog 專案部署</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>失敗處理：讀 <a href="../github-actions-failure-flow/">CI 失敗到修復發布流程</a>。</li>
</ul>
]]></content:encoded></item></channel></rss>