<?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>Sdk on Tarragon</title><link>https://tarrragon.github.io/blog/tags/sdk/</link><description>Recent content in Sdk on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 24 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/sdk/index.xml" rel="self" type="application/rss+xml"/><item><title>SDK Redaction API 設計</title><link>https://tarrragon.github.io/blog/monitoring/07-security-privacy/sdk-redaction-api/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/07-security-privacy/sdk-redaction-api/</guid><description>&lt;p>Redaction 是在事件資料離開 client 之前，把敏感欄位的值替換成遮罩或移除。本章聚焦 redaction 的策略面 — 哪些資訊需要保護、保護的判斷依據和適用範圍。SDK 的 API 實作細節（初始化方式、helper 函式設計、和 flush 管線的整合）見 &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/redaction-helper/" data-link-title="SDK redaction helper" data-link-desc="在事件離開 SDK 前移除敏感資訊 — 預設 redaction rule 處理常見 pattern，自訂 rule 處理業務特定的 secret">SDK redaction helper&lt;/a>。Redaction 在 SDK 端執行的設計原則是「敏感資料不離開 client」— 一旦資料送到 collector，即使 collector 有 access control，資料已經在網路上傳輸過，多了一層洩漏面。&lt;/p>
&lt;h2 id="預設-redaction-rule">預設 Redaction Rule&lt;/h2>
&lt;p>SDK 內建的 redaction rule 覆蓋最常見的敏感欄位模式。開發者不需要設定就能獲得基本保護。&lt;/p>
&lt;h3 id="欄位名稱比對">欄位名稱比對&lt;/h3>
&lt;p>以下欄位名稱（不分大小寫）的值自動替換為 &lt;code>[REDACTED]&lt;/code>：&lt;/p>
&lt;ul>
&lt;li>&lt;code>password&lt;/code>、&lt;code>passwd&lt;/code>、&lt;code>secret&lt;/code>、&lt;code>token&lt;/code>、&lt;code>api_key&lt;/code>、&lt;code>apiKey&lt;/code>&lt;/li>
&lt;li>&lt;code>authorization&lt;/code>、&lt;code>auth&lt;/code>、&lt;code>credential&lt;/code>&lt;/li>
&lt;li>&lt;code>ssn&lt;/code>、&lt;code>social_security&lt;/code>&lt;/li>
&lt;li>&lt;code>credit_card&lt;/code>、&lt;code>card_number&lt;/code>、&lt;code>cvv&lt;/code>、&lt;code>cvc&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>欄位名稱比對用 substring match — &lt;code>user_password&lt;/code> 包含 &lt;code>password&lt;/code> 會被 redact，&lt;code>password_reset_token&lt;/code> 包含 &lt;code>password&lt;/code> 和 &lt;code>token&lt;/code> 也會。&lt;/p>
&lt;h3 id="值格式比對">值格式比對&lt;/h3>
&lt;p>以下格式的值無論欄位名稱為何都自動替換：&lt;/p>
&lt;ul>
&lt;li>Email 地址格式（&lt;code>user@domain.com&lt;/code> → &lt;code>u***@domain.com&lt;/code>）&lt;/li>
&lt;li>信用卡號碼格式（連續 13-19 位數字 → 保留末四碼）&lt;/li>
&lt;li>Bearer token 格式（&lt;code>Bearer xxx&lt;/code> → &lt;code>Bearer [REDACTED]&lt;/code>）&lt;/li>
&lt;/ul>
&lt;p>值格式比對用正則表達式。正則的效能影響在大量事件時需要注意 — 預設 rule 的正則保持簡單，避免 catastrophic backtracking。&lt;/p>
&lt;h2 id="自訂-pattern">自訂 Pattern&lt;/h2>
&lt;p>應用可能有自己的 secret 格式，預設 rule 覆蓋不到。SDK 提供 API 讓開發者註冊自訂 redaction pattern。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">monitor.addRedactionRule(
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> name: &amp;#39;internal-api-key&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> pattern: RegExp(r&amp;#39;sk_live_[a-zA-Z0-9]{24}&amp;#39;),
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> replacement: &amp;#39;[REDACTED:api-key]&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">monitor.addRedactionRule(
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> name: &amp;#39;database-url&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> fieldNames: [&amp;#39;database_url&amp;#39;, &amp;#39;db_url&amp;#39;, &amp;#39;connection_string&amp;#39;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> replacement: &amp;#39;[REDACTED:db-url]&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>自訂 pattern 的設計考量：&lt;/p>
&lt;p>&lt;strong>Pattern 在 init 時註冊&lt;/strong>。Redaction rule 在 SDK 初始化時設定，之後所有事件都通過這些 rule。不支援動態修改 — 避免「中途加 rule 導致之前的事件沒被 redact」的困惑。&lt;/p>
&lt;p>&lt;strong>Pattern 順序無關&lt;/strong>。所有 rule 獨立執行，不依賴順序。一個欄位可以匹配多個 rule，以第一個匹配的 replacement 為準。&lt;/p>
&lt;p>&lt;strong>Replacement 可以保留部分資訊&lt;/strong>。&lt;code>[REDACTED]&lt;/code> 完全遮蔽，&lt;code>[REDACTED:api-key]&lt;/code> 保留類型資訊，&lt;code>u***@domain.com&lt;/code> 保留結構。保留類型資訊對 debug 有幫助 — 看到 &lt;code>[REDACTED:api-key]&lt;/code> 至少知道這裡原本有一個 API key。&lt;/p>
&lt;h2 id="redaction-的適用範圍">Redaction 的適用範圍&lt;/h2>
&lt;p>Redaction 應用在 SDK 送出事件前的最後一步 — 在序列化（JSON encode）之前。適用範圍包括：&lt;/p>
&lt;ul>
&lt;li>Event 的 data 欄位（自由欄位，開發者可能放入任何內容）&lt;/li>
&lt;li>Error 的 stack trace（檔案路徑可能包含使用者名稱或部署路徑）&lt;/li>
&lt;li>Error 的 message（例外訊息可能包含 query string 或參數值）&lt;/li>
&lt;li>Lifecycle 的 metadata（連線 URL 可能包含認證資訊）&lt;/li>
&lt;/ul>
&lt;p>Redaction 不應用在 SDK 的內部欄位（timestamp、event type、session ID）— 這些是 SDK 自己產生的，不包含使用者資料。&lt;/p></description><content:encoded><![CDATA[<p>Redaction 是在事件資料離開 client 之前，把敏感欄位的值替換成遮罩或移除。本章聚焦 redaction 的策略面 — 哪些資訊需要保護、保護的判斷依據和適用範圍。SDK 的 API 實作細節（初始化方式、helper 函式設計、和 flush 管線的整合）見 <a href="/blog/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>。Redaction 在 SDK 端執行的設計原則是「敏感資料不離開 client」— 一旦資料送到 collector，即使 collector 有 access control，資料已經在網路上傳輸過，多了一層洩漏面。</p>
<h2 id="預設-redaction-rule">預設 Redaction Rule</h2>
<p>SDK 內建的 redaction rule 覆蓋最常見的敏感欄位模式。開發者不需要設定就能獲得基本保護。</p>
<h3 id="欄位名稱比對">欄位名稱比對</h3>
<p>以下欄位名稱（不分大小寫）的值自動替換為 <code>[REDACTED]</code>：</p>
<ul>
<li><code>password</code>、<code>passwd</code>、<code>secret</code>、<code>token</code>、<code>api_key</code>、<code>apiKey</code></li>
<li><code>authorization</code>、<code>auth</code>、<code>credential</code></li>
<li><code>ssn</code>、<code>social_security</code></li>
<li><code>credit_card</code>、<code>card_number</code>、<code>cvv</code>、<code>cvc</code></li>
</ul>
<p>欄位名稱比對用 substring match — <code>user_password</code> 包含 <code>password</code> 會被 redact，<code>password_reset_token</code> 包含 <code>password</code> 和 <code>token</code> 也會。</p>
<h3 id="值格式比對">值格式比對</h3>
<p>以下格式的值無論欄位名稱為何都自動替換：</p>
<ul>
<li>Email 地址格式（<code>user@domain.com</code> → <code>u***@domain.com</code>）</li>
<li>信用卡號碼格式（連續 13-19 位數字 → 保留末四碼）</li>
<li>Bearer token 格式（<code>Bearer xxx</code> → <code>Bearer [REDACTED]</code>）</li>
</ul>
<p>值格式比對用正則表達式。正則的效能影響在大量事件時需要注意 — 預設 rule 的正則保持簡單，避免 catastrophic backtracking。</p>
<h2 id="自訂-pattern">自訂 Pattern</h2>
<p>應用可能有自己的 secret 格式，預設 rule 覆蓋不到。SDK 提供 API 讓開發者註冊自訂 redaction pattern。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">monitor.addRedactionRule(
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  name: &#39;internal-api-key&#39;,
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  pattern: RegExp(r&#39;sk_live_[a-zA-Z0-9]{24}&#39;),
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  replacement: &#39;[REDACTED:api-key]&#39;,
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">)
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">monitor.addRedactionRule(
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  name: &#39;database-url&#39;,
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  fieldNames: [&#39;database_url&#39;, &#39;db_url&#39;, &#39;connection_string&#39;],
</span></span><span class="line"><span class="ln">10</span><span class="cl">  replacement: &#39;[REDACTED:db-url]&#39;,
</span></span><span class="line"><span class="ln">11</span><span class="cl">)</span></span></code></pre></div><p>自訂 pattern 的設計考量：</p>
<p><strong>Pattern 在 init 時註冊</strong>。Redaction rule 在 SDK 初始化時設定，之後所有事件都通過這些 rule。不支援動態修改 — 避免「中途加 rule 導致之前的事件沒被 redact」的困惑。</p>
<p><strong>Pattern 順序無關</strong>。所有 rule 獨立執行，不依賴順序。一個欄位可以匹配多個 rule，以第一個匹配的 replacement 為準。</p>
<p><strong>Replacement 可以保留部分資訊</strong>。<code>[REDACTED]</code> 完全遮蔽，<code>[REDACTED:api-key]</code> 保留類型資訊，<code>u***@domain.com</code> 保留結構。保留類型資訊對 debug 有幫助 — 看到 <code>[REDACTED:api-key]</code> 至少知道這裡原本有一個 API key。</p>
<h2 id="redaction-的適用範圍">Redaction 的適用範圍</h2>
<p>Redaction 應用在 SDK 送出事件前的最後一步 — 在序列化（JSON encode）之前。適用範圍包括：</p>
<ul>
<li>Event 的 data 欄位（自由欄位，開發者可能放入任何內容）</li>
<li>Error 的 stack trace（檔案路徑可能包含使用者名稱或部署路徑）</li>
<li>Error 的 message（例外訊息可能包含 query string 或參數值）</li>
<li>Lifecycle 的 metadata（連線 URL 可能包含認證資訊）</li>
</ul>
<p>Redaction 不應用在 SDK 的內部欄位（timestamp、event type、session ID）— 這些是 SDK 自己產生的，不包含使用者資料。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>資料離開 client 後的保護 → <a href="/blog/monitoring/07-security-privacy/transport-security/" data-link-title="Transport 安全" data-link-desc="HTTPS / basic auth / 同區網也要加密的理由 — 監控資料在傳輸途中的保護機制">Transport 安全</a></li>
<li>去識別化策略 → <a href="/blog/monitoring/07-security-privacy/anonymization-strategy/" data-link-title="去識別化策略" data-link-desc="IP 截斷 / user agent 簡化 / stack trace 路徑清理 / session UUID — 四種去識別化技術的適用場景和實作方式">去識別化策略</a></li>
<li>IME 個人化學習的 secret 洩漏風險 → <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">ux-design 模組三 IME 安全 checklist</a></li>
</ul>
]]></content:encoded></item><item><title>SDK 公開 API 設計</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/public-api/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/public-api/</guid><description>&lt;p>SDK 的公開 API 是應用程式和監控系統之間的契約。六個方法涵蓋 SDK 的完整生命週期：初始化、四類事件上報、資料送出控制和資源釋放。跨平台的 SDK（JS / Flutter / Python）共用相同的方法簽名，讓開發者在不同平台上使用一致的 API。&lt;/p>
&lt;h2 id="六個方法">六個方法&lt;/h2>
&lt;h3 id="init">init&lt;/h3>
&lt;p>SDK 初始化。設定 collector endpoint、app 識別資訊、flush 間隔、buffer 大小。在 app 啟動時呼叫一次。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Monitor.init({
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> endpoint: &amp;#39;https://collector.example.com/v1/events&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> app: &amp;#39;my_app&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> version: &amp;#39;1.2.0&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> flushInterval: 30000, // 毫秒
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> bufferSize: 100,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">})&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>init 負責建立 session、記錄 lifecycle.session.start 事件、啟動 flush 計時器。init 之前呼叫其他方法應該拋出明確錯誤（SDK 未初始化），而非靜默忽略。&lt;/p>
&lt;p>&lt;strong>連線驗證策略：lazy&lt;/strong>。init 不驗證 collector 是否可達 — 不發 HTTP 請求、不 ping endpoint。init 的失敗只代表配置錯誤（缺少 endpoint 參數），不代表網路問題。網路問題在第一次 flush 時才浮現，flush 失敗時事件保留在 buffer 等待重試。&lt;/p>
&lt;p>Lazy 策略的理由：SDK 不應阻塞主程式的啟動流程。如果 init 驗證連線，collector 暫時不可用時 app 會啟動失敗 — 監控工具反而變成可用性的瓶頸。短生命週期腳本（&lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/python-platform/" data-link-title="Python 平台適配" data-link-desc="GIL 與 threading、atexit 可靠性、subprocess 監控 — Python SDK 的平台特殊考量">Python 平台適配：短生命週期腳本&lt;/a>）對這一點更敏感 — hook 腳本不能因為 collector 沒啟動就拒絕執行。&lt;/p>
&lt;h3 id="event">event&lt;/h3>
&lt;p>記錄使用者操作事件（&lt;a href="https://tarrragon.github.io/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">四類事件中的 Event 類&lt;/a>）。接受事件名稱和可選的 data 物件。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Monitor.event(&amp;#39;terminal.connect.start&amp;#39;, { url: &amp;#39;wss://...&amp;#39; })
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Monitor.event(&amp;#39;enrollment.qr.scan&amp;#39;)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>event 方法是非阻塞的 — 事件進入內部 buffer 立即返回，不等待網路送出。應用程式的操作流程不應該被監控 SDK 的網路延遲阻塞。&lt;/p>
&lt;h3 id="error">error&lt;/h3>
&lt;p>記錄錯誤事件。接受 Error/Exception 物件或自訂的錯誤描述。自動附加 stack trace、錯誤類型、觸發位置。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Monitor.error(exception, { step: &amp;#39;ws_connect&amp;#39; })
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Monitor.error(&amp;#39;Auth token missing&amp;#39;, { context: &amp;#39;handshake&amp;#39; })&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>error 方法和自動攔截機制（&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>）互補 — 自動攔截處理未捕獲的例外，error 方法處理開發者主動上報的已知錯誤。&lt;/p>
&lt;h3 id="metric">metric&lt;/h3>
&lt;p>記錄數值指標。接受指標名稱和數值。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Monitor.metric(&amp;#39;connect.duration_ms&amp;#39;, 320)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Monitor.metric(&amp;#39;terminal.fps&amp;#39;, 58.5)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>metric 方法記錄的是離散的數值快照。聚合計算（平均、百分位、趨勢）在 collector 端完成，SDK 端只負責記錄原始值。&lt;/p>
&lt;h3 id="flush">flush&lt;/h3>
&lt;p>強制送出 buffer 中所有待發事件。正常情況下 SDK 按 flushInterval 定期自動 flush（&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出&lt;/a>）。flush 方法用於需要確保事件已送出的場景 — 例如 app 即將進入背景或使用者手動觸發 log 上傳。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">await Monitor.flush()&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>flush 是非同步方法 — 需要等待網路請求完成。呼叫端可以 await 確認送出成功，也可以 fire-and-forget。&lt;/p></description><content:encoded><![CDATA[<p>SDK 的公開 API 是應用程式和監控系統之間的契約。六個方法涵蓋 SDK 的完整生命週期：初始化、四類事件上報、資料送出控制和資源釋放。跨平台的 SDK（JS / Flutter / Python）共用相同的方法簽名，讓開發者在不同平台上使用一致的 API。</p>
<h2 id="六個方法">六個方法</h2>
<h3 id="init">init</h3>
<p>SDK 初始化。設定 collector endpoint、app 識別資訊、flush 間隔、buffer 大小。在 app 啟動時呼叫一次。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Monitor.init({
</span></span><span class="line"><span class="ln">2</span><span class="cl">  endpoint: &#39;https://collector.example.com/v1/events&#39;,
</span></span><span class="line"><span class="ln">3</span><span class="cl">  app: &#39;my_app&#39;,
</span></span><span class="line"><span class="ln">4</span><span class="cl">  version: &#39;1.2.0&#39;,
</span></span><span class="line"><span class="ln">5</span><span class="cl">  flushInterval: 30000,   // 毫秒
</span></span><span class="line"><span class="ln">6</span><span class="cl">  bufferSize: 100,
</span></span><span class="line"><span class="ln">7</span><span class="cl">})</span></span></code></pre></div><p>init 負責建立 session、記錄 lifecycle.session.start 事件、啟動 flush 計時器。init 之前呼叫其他方法應該拋出明確錯誤（SDK 未初始化），而非靜默忽略。</p>
<p><strong>連線驗證策略：lazy</strong>。init 不驗證 collector 是否可達 — 不發 HTTP 請求、不 ping endpoint。init 的失敗只代表配置錯誤（缺少 endpoint 參數），不代表網路問題。網路問題在第一次 flush 時才浮現，flush 失敗時事件保留在 buffer 等待重試。</p>
<p>Lazy 策略的理由：SDK 不應阻塞主程式的啟動流程。如果 init 驗證連線，collector 暫時不可用時 app 會啟動失敗 — 監控工具反而變成可用性的瓶頸。短生命週期腳本（<a href="/blog/monitoring/05-platform-adaptation/python-platform/" data-link-title="Python 平台適配" data-link-desc="GIL 與 threading、atexit 可靠性、subprocess 監控 — Python SDK 的平台特殊考量">Python 平台適配：短生命週期腳本</a>）對這一點更敏感 — hook 腳本不能因為 collector 沒啟動就拒絕執行。</p>
<h3 id="event">event</h3>
<p>記錄使用者操作事件（<a href="/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">四類事件中的 Event 類</a>）。接受事件名稱和可選的 data 物件。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Monitor.event(&#39;terminal.connect.start&#39;, { url: &#39;wss://...&#39; })
</span></span><span class="line"><span class="ln">2</span><span class="cl">Monitor.event(&#39;enrollment.qr.scan&#39;)</span></span></code></pre></div><p>event 方法是非阻塞的 — 事件進入內部 buffer 立即返回，不等待網路送出。應用程式的操作流程不應該被監控 SDK 的網路延遲阻塞。</p>
<h3 id="error">error</h3>
<p>記錄錯誤事件。接受 Error/Exception 物件或自訂的錯誤描述。自動附加 stack trace、錯誤類型、觸發位置。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Monitor.error(exception, { step: &#39;ws_connect&#39; })
</span></span><span class="line"><span class="ln">2</span><span class="cl">Monitor.error(&#39;Auth token missing&#39;, { context: &#39;handshake&#39; })</span></span></code></pre></div><p>error 方法和自動攔截機制（<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>）互補 — 自動攔截處理未捕獲的例外，error 方法處理開發者主動上報的已知錯誤。</p>
<h3 id="metric">metric</h3>
<p>記錄數值指標。接受指標名稱和數值。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Monitor.metric(&#39;connect.duration_ms&#39;, 320)
</span></span><span class="line"><span class="ln">2</span><span class="cl">Monitor.metric(&#39;terminal.fps&#39;, 58.5)</span></span></code></pre></div><p>metric 方法記錄的是離散的數值快照。聚合計算（平均、百分位、趨勢）在 collector 端完成，SDK 端只負責記錄原始值。</p>
<h3 id="flush">flush</h3>
<p>強制送出 buffer 中所有待發事件。正常情況下 SDK 按 flushInterval 定期自動 flush（<a href="/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出</a>）。flush 方法用於需要確保事件已送出的場景 — 例如 app 即將進入背景或使用者手動觸發 log 上傳。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">await Monitor.flush()</span></span></code></pre></div><p>flush 是非同步方法 — 需要等待網路請求完成。呼叫端可以 await 確認送出成功，也可以 fire-and-forget。</p>
<h3 id="close">close</h3>
<p>SDK 資源釋放。停止 flush 計時器、送出 buffer 中剩餘事件、關閉網路連線、記錄 lifecycle.session.end 事件。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">await Monitor.close()</span></span></code></pre></div><p>close 在 app 關閉時呼叫。呼叫後 SDK 進入已關閉狀態，後續的 event/error/metric 呼叫應該被靜默忽略（不拋錯，因為 app 正在關閉）。</p>
<h2 id="api-設計原則">API 設計原則</h2>
<p><strong>方法名稱和四類事件對齊</strong>。event / error / metric 三個方法直接對應三類事件，lifecycle 事件由 init 和 close 自動產生。開發者看到方法名稱就知道對應哪類事件。</p>
<p><strong>所有上報方法非阻塞</strong>。event、error、metric 進 buffer 立即返回。監控 SDK 阻塞應用程式的操作流程是反模式。</p>
<p><strong>init 和 close 成對出現</strong>。init 開始 session，close 結束 session。兩者界定 SDK 的活躍期間。</p>
<p>各平台的 SDK 整合範例（Flutter 的 pubspec.yaml + main.dart init、Python 的 pip install + init code、JS 的 script tag + init）見 monitor repo 各 SDK 的 README。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<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>Buffer 和 flush 的策略 → <a href="/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出策略</a></li>
<li>SDK 端的資料脫敏 → <a href="/blog/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></li>
<li>SDK 的 HTTP POST 行為需要 protocol test → <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試</a></li>
</ul>
]]></content:encoded></item><item><title>自動攔截機制</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/auto-intercept/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/auto-intercept/</guid><description>&lt;p>自動攔截機制讓 SDK 在開發者不寫任何 error 上報程式碼的情況下，自動捕獲未處理的例外並記錄為 error 事件。每個平台有各自的全域錯誤處理器，SDK 在 init 時註冊攔截器，捕獲後轉換為統一的 error 事件格式送出。&lt;/p>
&lt;h2 id="各平台的攔截點">各平台的攔截點&lt;/h2>
&lt;h3 id="javascript--typescript">JavaScript / TypeScript&lt;/h3>
&lt;p>JS 環境有兩個全域錯誤攔截點：&lt;/p>
&lt;p>&lt;code>window.onerror&lt;/code> 捕獲同步程式碼中未處理的例外。回呼函式收到 error message、來源 URL、行號、列號和 Error 物件。&lt;/p>
&lt;p>&lt;code>window.onunhandledrejection&lt;/code> 捕獲未處理的 Promise rejection。回呼函式收到 PromiseRejectionEvent，包含 rejection reason。&lt;/p>
&lt;p>SDK 在 init 時註冊這兩個處理器。註冊前先保存原有的處理器（如果有），攔截後先呼叫原有處理器再執行 SDK 的記錄邏輯 — 避免覆蓋應用程式已有的錯誤處理。&lt;/p>
&lt;p>限制：&lt;code>onerror&lt;/code> 對跨域腳本的錯誤只收到 &lt;code>Script error.&lt;/code> 訊息，沒有 stack trace。需要在 &lt;code>&amp;lt;script&amp;gt;&lt;/code> 標籤加 &lt;code>crossorigin&lt;/code> 屬性，server 端的 CORS header 加 &lt;code>Access-Control-Allow-Origin&lt;/code>。&lt;/p>
&lt;h3 id="flutter">Flutter&lt;/h3>
&lt;p>Flutter 有兩個攔截層：&lt;/p>
&lt;p>&lt;code>FlutterError.onError&lt;/code> 捕獲 widget build / layout / paint 過程中的例外。預設行為是在 console 印出錯誤，SDK 替換為記錄 error 事件後再呼叫預設處理器。&lt;/p>
&lt;p>&lt;code>PlatformDispatcher.instance.onError&lt;/code> 捕獲其他非同步區域的未處理例外（Dart 2.15+）。包含 Isolate 內的未捕獲例外。&lt;/p>
&lt;p>&lt;code>runZonedGuarded&lt;/code> 是另一個選項 — 在指定的 Zone 內捕獲所有未處理例外。SDK 可以用 &lt;code>runZonedGuarded&lt;/code> 包住整個 &lt;code>runApp()&lt;/code>，但這和 &lt;code>PlatformDispatcher.onError&lt;/code> 有重疊，需要避免同一個例外被記錄兩次。&lt;/p>
&lt;p>限制：Flutter 的 release mode 會移除 stack trace 的符號資訊（obfuscation）。需要保留 debug symbols 檔案（&lt;code>.dSYM&lt;/code> / &lt;code>mapping.txt&lt;/code>），在 collector 端做 symbolication。&lt;/p>
&lt;h3 id="python">Python&lt;/h3>
&lt;p>&lt;code>sys.excepthook&lt;/code> 處理主執行緒的未捕獲例外。回呼函式收到 exception type、value 和 traceback。&lt;/p>
&lt;p>&lt;code>threading.excepthook&lt;/code>（Python 3.8+）處理子執行緒的未捕獲例外。&lt;/p>
&lt;p>&lt;code>atexit.register&lt;/code> 用於在 Python 程序退出時 flush 剩餘的 buffer。但 &lt;code>atexit&lt;/code> 在 &lt;code>os._exit()&lt;/code> 或 SIGKILL 時不會執行。&lt;/p>
&lt;p>限制：Python 的 GIL 讓 SDK 的網路操作可能阻塞主執行緒。SDK 的 flush 應該在獨立的 daemon thread 中執行，主執行緒只負責把事件放入 buffer。&lt;/p>
&lt;h2 id="攔截後的統一處理">攔截後的統一處理&lt;/h2>
&lt;p>不同平台的錯誤物件格式不同（JS 的 Error、Flutter 的 FlutterErrorDetails、Python 的 sys.exc_info tuple）。SDK 在攔截後把平台特定的錯誤物件轉換為統一的 error 事件格式：&lt;/p>
&lt;ul>
&lt;li>type: &lt;code>&amp;quot;error&amp;quot;&lt;/code>&lt;/li>
&lt;li>name: 從 error class name 推導（&lt;code>TypeError&lt;/code> → &lt;code>error.TypeError&lt;/code>）&lt;/li>
&lt;li>data: 包含 message、stack trace（字串化）、觸發位置&lt;/li>
&lt;/ul>
&lt;p>轉換層是每個平台 SDK 唯一的平台特定程式碼。轉換完成後，事件進入和手動上報相同的 buffer → flush 管線。&lt;/p>
&lt;h2 id="和手動上報的分工">和手動上報的分工&lt;/h2>
&lt;p>自動攔截處理「開發者沒有預期到的錯誤」— 未捕獲的例外、未處理的 rejection。手動上報（&lt;code>Monitor.error()&lt;/code>）處理「開發者知道可能發生但想記錄的錯誤」— 已捕獲的例外、業務邏輯的異常狀態。&lt;/p>
&lt;p>兩者進入同一個 buffer 和 flush 管線，在 collector 端可以用 data 中的 &lt;code>source: &amp;quot;auto&amp;quot;&lt;/code> / &lt;code>source: &amp;quot;manual&amp;quot;&lt;/code> 欄位區分。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>SDK 公開 API → &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/public-api/" data-link-title="SDK 公開 API 設計" data-link-desc="init / event / error / metric / flush / close 六個方法構成 SDK 的完整生命週期 — 跨平台共用相同 API 介面">SDK 公開 API 設計&lt;/a>&lt;/li>
&lt;li>各平台的深入適配問題 → &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/" data-link-title="模組五：平台適配" data-link-desc="JS CORS / Flutter isolate / Python GIL / Go graceful shutdown — 各平台的特殊考量">模組五 平台適配&lt;/a>&lt;/li>
&lt;li>Buffer 和 flush → &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出策略&lt;/a>&lt;/li>
&lt;li>主動感測器設計（和被動攔截互補）→ &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/frontend-sensor-design/" data-link-title="前端感測器設計" data-link-desc="什麼行為值得埋感測器、每類感測器的實作方式、取樣策略和效能影響 — 和 auto-intercept 的被動攔截互補">前端感測器設計&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>自動攔截機制讓 SDK 在開發者不寫任何 error 上報程式碼的情況下，自動捕獲未處理的例外並記錄為 error 事件。每個平台有各自的全域錯誤處理器，SDK 在 init 時註冊攔截器，捕獲後轉換為統一的 error 事件格式送出。</p>
<h2 id="各平台的攔截點">各平台的攔截點</h2>
<h3 id="javascript--typescript">JavaScript / TypeScript</h3>
<p>JS 環境有兩個全域錯誤攔截點：</p>
<p><code>window.onerror</code> 捕獲同步程式碼中未處理的例外。回呼函式收到 error message、來源 URL、行號、列號和 Error 物件。</p>
<p><code>window.onunhandledrejection</code> 捕獲未處理的 Promise rejection。回呼函式收到 PromiseRejectionEvent，包含 rejection reason。</p>
<p>SDK 在 init 時註冊這兩個處理器。註冊前先保存原有的處理器（如果有），攔截後先呼叫原有處理器再執行 SDK 的記錄邏輯 — 避免覆蓋應用程式已有的錯誤處理。</p>
<p>限制：<code>onerror</code> 對跨域腳本的錯誤只收到 <code>Script error.</code> 訊息，沒有 stack trace。需要在 <code>&lt;script&gt;</code> 標籤加 <code>crossorigin</code> 屬性，server 端的 CORS header 加 <code>Access-Control-Allow-Origin</code>。</p>
<h3 id="flutter">Flutter</h3>
<p>Flutter 有兩個攔截層：</p>
<p><code>FlutterError.onError</code> 捕獲 widget build / layout / paint 過程中的例外。預設行為是在 console 印出錯誤，SDK 替換為記錄 error 事件後再呼叫預設處理器。</p>
<p><code>PlatformDispatcher.instance.onError</code> 捕獲其他非同步區域的未處理例外（Dart 2.15+）。包含 Isolate 內的未捕獲例外。</p>
<p><code>runZonedGuarded</code> 是另一個選項 — 在指定的 Zone 內捕獲所有未處理例外。SDK 可以用 <code>runZonedGuarded</code> 包住整個 <code>runApp()</code>，但這和 <code>PlatformDispatcher.onError</code> 有重疊，需要避免同一個例外被記錄兩次。</p>
<p>限制：Flutter 的 release mode 會移除 stack trace 的符號資訊（obfuscation）。需要保留 debug symbols 檔案（<code>.dSYM</code> / <code>mapping.txt</code>），在 collector 端做 symbolication。</p>
<h3 id="python">Python</h3>
<p><code>sys.excepthook</code> 處理主執行緒的未捕獲例外。回呼函式收到 exception type、value 和 traceback。</p>
<p><code>threading.excepthook</code>（Python 3.8+）處理子執行緒的未捕獲例外。</p>
<p><code>atexit.register</code> 用於在 Python 程序退出時 flush 剩餘的 buffer。但 <code>atexit</code> 在 <code>os._exit()</code> 或 SIGKILL 時不會執行。</p>
<p>限制：Python 的 GIL 讓 SDK 的網路操作可能阻塞主執行緒。SDK 的 flush 應該在獨立的 daemon thread 中執行，主執行緒只負責把事件放入 buffer。</p>
<h2 id="攔截後的統一處理">攔截後的統一處理</h2>
<p>不同平台的錯誤物件格式不同（JS 的 Error、Flutter 的 FlutterErrorDetails、Python 的 sys.exc_info tuple）。SDK 在攔截後把平台特定的錯誤物件轉換為統一的 error 事件格式：</p>
<ul>
<li>type: <code>&quot;error&quot;</code></li>
<li>name: 從 error class name 推導（<code>TypeError</code> → <code>error.TypeError</code>）</li>
<li>data: 包含 message、stack trace（字串化）、觸發位置</li>
</ul>
<p>轉換層是每個平台 SDK 唯一的平台特定程式碼。轉換完成後，事件進入和手動上報相同的 buffer → flush 管線。</p>
<h2 id="和手動上報的分工">和手動上報的分工</h2>
<p>自動攔截處理「開發者沒有預期到的錯誤」— 未捕獲的例外、未處理的 rejection。手動上報（<code>Monitor.error()</code>）處理「開發者知道可能發生但想記錄的錯誤」— 已捕獲的例外、業務邏輯的異常狀態。</p>
<p>兩者進入同一個 buffer 和 flush 管線，在 collector 端可以用 data 中的 <code>source: &quot;auto&quot;</code> / <code>source: &quot;manual&quot;</code> 欄位區分。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>SDK 公開 API → <a href="/blog/monitoring/03-sdk-design/public-api/" data-link-title="SDK 公開 API 設計" data-link-desc="init / event / error / metric / flush / close 六個方法構成 SDK 的完整生命週期 — 跨平台共用相同 API 介面">SDK 公開 API 設計</a></li>
<li>各平台的深入適配問題 → <a href="/blog/monitoring/05-platform-adaptation/" data-link-title="模組五：平台適配" data-link-desc="JS CORS / Flutter isolate / Python GIL / Go graceful shutdown — 各平台的特殊考量">模組五 平台適配</a></li>
<li>Buffer 和 flush → <a href="/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出策略</a></li>
<li>主動感測器設計（和被動攔截互補）→ <a href="/blog/monitoring/03-sdk-design/frontend-sensor-design/" data-link-title="前端感測器設計" data-link-desc="什麼行為值得埋感測器、每類感測器的實作方式、取樣策略和效能影響 — 和 auto-intercept 的被動攔截互補">前端感測器設計</a></li>
</ul>
]]></content:encoded></item><item><title>模組三：SDK 設計模式</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/</guid><description>&lt;p>回答「怎麼在各平台埋點」。三個 SDK（JS/Flutter/Python）共用同一套事件格式，公開 API 保持一致。&lt;/p>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> SDK 公開 API 設計（init / event / error / metric / flush / close）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 自動攔截機制（JS window.onerror / Flutter FlutterError / Python sys.excepthook）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 攢批送出策略（flush interval / buffer size / flush on close）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 離線 buffer 與重試（FIFO 丟棄 / 本地 persistence / 恢復後補發的取捨）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> SDK redaction helper（模組七的實作層）&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試&lt;/a>：SDK 的 HTTP POST 行為需要 protocol test&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安&lt;/a>：redaction 在 SDK 端做&lt;/li>
&lt;li>← &lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/" data-link-title="模組一：測試策略分層" data-link-desc="Unit / Protocol Integration / Screen State 三層測試各自的職責、盲區和判斷原則">testing 模組一 測試策略&lt;/a>：mock 遮蔽機制影響 SDK 的 auto-intercept 行為驗證&lt;/li>
&lt;li>實作 repo：tarrragon/monitor 的 sdk-js / sdk-flutter / sdk-python&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「怎麼在各平台埋點」。三個 SDK（JS/Flutter/Python）共用同一套事件格式，公開 API 保持一致。</p>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> SDK 公開 API 設計（init / event / error / metric / flush / close）</li>
<li><input checked="" disabled="" type="checkbox"> 自動攔截機制（JS window.onerror / Flutter FlutterError / Python sys.excepthook）</li>
<li><input checked="" disabled="" type="checkbox"> 攢批送出策略（flush interval / buffer size / flush on close）</li>
<li><input checked="" disabled="" type="checkbox"> 離線 buffer 與重試（FIFO 丟棄 / 本地 persistence / 恢復後補發的取捨）</li>
<li><input checked="" disabled="" type="checkbox"> SDK redaction helper（模組七的實作層）</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試</a>：SDK 的 HTTP POST 行為需要 protocol test</li>
<li>→ <a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安</a>：redaction 在 SDK 端做</li>
<li>← <a href="/blog/testing/01-test-strategy-layers/" data-link-title="模組一：測試策略分層" data-link-desc="Unit / Protocol Integration / Screen State 三層測試各自的職責、盲區和判斷原則">testing 模組一 測試策略</a>：mock 遮蔽機制影響 SDK 的 auto-intercept 行為驗證</li>
<li>實作 repo：tarrragon/monitor 的 sdk-js / sdk-flutter / sdk-python</li>
</ul>
]]></content:encoded></item><item><title>攢批送出策略</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/batch-flush/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/batch-flush/</guid><description>&lt;p>攢批送出策略控制事件從 SDK 內部 buffer 送到 collector 的時機。事件產生後先進入記憶體 buffer，累積到一定數量或間隔一定時間後，一次性透過 HTTP POST 送出整批事件。攢批的目的是減少網路請求次數 — 100 筆事件合併成一個 HTTP 請求，比 100 個獨立請求的網路開銷低。&lt;/p>
&lt;h2 id="三個觸發條件">三個觸發條件&lt;/h2>
&lt;h3 id="時間觸發flush-interval">時間觸發（flush interval）&lt;/h3>
&lt;p>固定間隔自動 flush。SDK 在 init 時啟動計時器，每隔 N 毫秒檢查 buffer 是否有待發事件，有則送出。&lt;/p>
&lt;p>合理的間隔範圍：10-60 秒。間隔太短（1 秒）接近逐筆送出，失去攢批的效益；間隔太長（5 分鐘）可能讓事件延遲到達 collector，影響即時監控和告警的反應速度。&lt;/p>
&lt;p>自用工具場景下 30 秒是合理的預設 — 事件量低，30 秒的延遲對 debug 分析沒有實質影響。商業產品可以降到 10 秒以獲得更接近即時的 error 告警。&lt;/p>
&lt;h3 id="數量觸發buffer-size">數量觸發（buffer size）&lt;/h3>
&lt;p>Buffer 內的事件數量達到上限時立即 flush。Buffer size 設定為一次 HTTP POST 的合理 payload 大小對應的事件數量。&lt;/p>
&lt;p>合理的數量範圍：50-200 筆。數量太少（10 筆）頻繁觸發 flush；數量太多（1000 筆）單次 HTTP POST 的 payload 過大，增加傳輸失敗的風險（超時、記憶體）。&lt;/p>
&lt;p>數量觸發和時間觸發互為備援。高頻事件場景（使用者快速操作）靠數量觸發避免 buffer 溢出；低頻事件場景（使用者長時間閒置）靠時間觸發確保事件在合理時間內送出。&lt;/p>
&lt;h3 id="關閉觸發flush-on-close">關閉觸發（flush on close）&lt;/h3>
&lt;p>SDK close 時強制 flush buffer 中所有剩餘事件。這是最後一道保障 — app 關閉後 buffer 中未送出的事件就永久遺失了。&lt;/p>
&lt;p>close flush 的挑戰是時間限制。iOS app 進入背景後約 5 秒會被系統 suspend，Android 的限制更嚴格。Close flush 必須在這個時間窗口內完成網路請求。如果 buffer 中事件太多導致 flush 超時，需要截斷 — 送出最近的 N 筆，放棄較舊的。&lt;/p>
&lt;h2 id="buffer-管理">Buffer 管理&lt;/h2>
&lt;h3 id="記憶體-buffer">記憶體 buffer&lt;/h3>
&lt;p>Buffer 在記憶體中維護一個事件陣列。新事件 append 到尾端，flush 時取出整個陣列送出並清空。&lt;/p>
&lt;p>記憶體 buffer 的上限應該設定為 buffer size 的 2-3 倍（允許 1-2 次 flush 失敗後累積的事件）。超過上限時丟棄最舊的事件（FIFO），保留最新的 — 最新的事件對 debug 和即時分析的價值更高。&lt;/p>
&lt;h3 id="離線-buffer">離線 buffer&lt;/h3>
&lt;p>網路不可用時，事件累積在記憶體 buffer 中。如果離線時間超過記憶體 buffer 容量，需要離線 persistence — 見 &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer 與重試&lt;/a>。&lt;/p>
&lt;h2 id="flush-失敗處理">Flush 失敗處理&lt;/h2>
&lt;p>HTTP POST 失敗時（網路中斷、server 回 5xx、超時），事件保留在 buffer 中等待下一次 flush 重試。不立即重試 — 連續失敗通常代表網路問題或 server 問題，立即重試只會增加負載。&lt;/p>
&lt;p>重試次數有上限（3 次）。超過重試上限的事件被丟棄，記錄一筆 &lt;code>sdk.flush.dropped&lt;/code> metric 事件（這筆 metric 本身也進 buffer，在下次成功 flush 時送出）。&lt;/p>
&lt;h3 id="sdk-對-collector-回應的處理">SDK 對 collector 回應的處理&lt;/h3>
&lt;p>SDK 只需要判斷 HTTP status code 就知道怎麼處理 buffer，不需要解析 response body 的細節。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Status&lt;/th>
 &lt;th>SDK 行為&lt;/th>
 &lt;th>理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>200&lt;/td>
 &lt;td>清除已送出的 buffer&lt;/td>
 &lt;td>全部成功&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>207&lt;/td>
 &lt;td>清除 buffer + 記錄 warning log&lt;/td>
 &lt;td>合法事件已被接受；失敗事件是 schema 問題，重試也不會過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>400&lt;/td>
 &lt;td>清除 buffer + 記錄 error log&lt;/td>
 &lt;td>Schema 問題重試也不會過，保留在 buffer 只會擋住後續事件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>503&lt;/td>
 &lt;td>保留 buffer + 等待 &lt;code>retry_after&lt;/code> 秒&lt;/td>
 &lt;td>collector 暫時不可用，事件本身沒問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>其他（network error / timeout）&lt;/td>
 &lt;td>保留 buffer + 下次 flush 重試&lt;/td>
 &lt;td>暫時性問題，重試有機會成功&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>207 和 400 都清 buffer 的關鍵判斷：Schema 驗證失敗是 SDK 端產出了不合規的事件，問題在 SDK 的事件建構邏輯（程式碼 bug），不在 collector 或網路 — 重試相同事件永遠不會過。SDK 把失敗事件的 error 訊息記到 warning/error log 供開發者排查，然後放行後續事件。&lt;/p></description><content:encoded><![CDATA[<p>攢批送出策略控制事件從 SDK 內部 buffer 送到 collector 的時機。事件產生後先進入記憶體 buffer，累積到一定數量或間隔一定時間後，一次性透過 HTTP POST 送出整批事件。攢批的目的是減少網路請求次數 — 100 筆事件合併成一個 HTTP 請求，比 100 個獨立請求的網路開銷低。</p>
<h2 id="三個觸發條件">三個觸發條件</h2>
<h3 id="時間觸發flush-interval">時間觸發（flush interval）</h3>
<p>固定間隔自動 flush。SDK 在 init 時啟動計時器，每隔 N 毫秒檢查 buffer 是否有待發事件，有則送出。</p>
<p>合理的間隔範圍：10-60 秒。間隔太短（1 秒）接近逐筆送出，失去攢批的效益；間隔太長（5 分鐘）可能讓事件延遲到達 collector，影響即時監控和告警的反應速度。</p>
<p>自用工具場景下 30 秒是合理的預設 — 事件量低，30 秒的延遲對 debug 分析沒有實質影響。商業產品可以降到 10 秒以獲得更接近即時的 error 告警。</p>
<h3 id="數量觸發buffer-size">數量觸發（buffer size）</h3>
<p>Buffer 內的事件數量達到上限時立即 flush。Buffer size 設定為一次 HTTP POST 的合理 payload 大小對應的事件數量。</p>
<p>合理的數量範圍：50-200 筆。數量太少（10 筆）頻繁觸發 flush；數量太多（1000 筆）單次 HTTP POST 的 payload 過大，增加傳輸失敗的風險（超時、記憶體）。</p>
<p>數量觸發和時間觸發互為備援。高頻事件場景（使用者快速操作）靠數量觸發避免 buffer 溢出；低頻事件場景（使用者長時間閒置）靠時間觸發確保事件在合理時間內送出。</p>
<h3 id="關閉觸發flush-on-close">關閉觸發（flush on close）</h3>
<p>SDK close 時強制 flush buffer 中所有剩餘事件。這是最後一道保障 — app 關閉後 buffer 中未送出的事件就永久遺失了。</p>
<p>close flush 的挑戰是時間限制。iOS app 進入背景後約 5 秒會被系統 suspend，Android 的限制更嚴格。Close flush 必須在這個時間窗口內完成網路請求。如果 buffer 中事件太多導致 flush 超時，需要截斷 — 送出最近的 N 筆，放棄較舊的。</p>
<h2 id="buffer-管理">Buffer 管理</h2>
<h3 id="記憶體-buffer">記憶體 buffer</h3>
<p>Buffer 在記憶體中維護一個事件陣列。新事件 append 到尾端，flush 時取出整個陣列送出並清空。</p>
<p>記憶體 buffer 的上限應該設定為 buffer size 的 2-3 倍（允許 1-2 次 flush 失敗後累積的事件）。超過上限時丟棄最舊的事件（FIFO），保留最新的 — 最新的事件對 debug 和即時分析的價值更高。</p>
<h3 id="離線-buffer">離線 buffer</h3>
<p>網路不可用時，事件累積在記憶體 buffer 中。如果離線時間超過記憶體 buffer 容量，需要離線 persistence — 見 <a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer 與重試</a>。</p>
<h2 id="flush-失敗處理">Flush 失敗處理</h2>
<p>HTTP POST 失敗時（網路中斷、server 回 5xx、超時），事件保留在 buffer 中等待下一次 flush 重試。不立即重試 — 連續失敗通常代表網路問題或 server 問題，立即重試只會增加負載。</p>
<p>重試次數有上限（3 次）。超過重試上限的事件被丟棄，記錄一筆 <code>sdk.flush.dropped</code> metric 事件（這筆 metric 本身也進 buffer，在下次成功 flush 時送出）。</p>
<h3 id="sdk-對-collector-回應的處理">SDK 對 collector 回應的處理</h3>
<p>SDK 只需要判斷 HTTP status code 就知道怎麼處理 buffer，不需要解析 response body 的細節。</p>
<table>
  <thead>
      <tr>
          <th>Status</th>
          <th>SDK 行為</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>200</td>
          <td>清除已送出的 buffer</td>
          <td>全部成功</td>
      </tr>
      <tr>
          <td>207</td>
          <td>清除 buffer + 記錄 warning log</td>
          <td>合法事件已被接受；失敗事件是 schema 問題，重試也不會過</td>
      </tr>
      <tr>
          <td>400</td>
          <td>清除 buffer + 記錄 error log</td>
          <td>Schema 問題重試也不會過，保留在 buffer 只會擋住後續事件</td>
      </tr>
      <tr>
          <td>503</td>
          <td>保留 buffer + 等待 <code>retry_after</code> 秒</td>
          <td>collector 暫時不可用，事件本身沒問題</td>
      </tr>
      <tr>
          <td>其他（network error / timeout）</td>
          <td>保留 buffer + 下次 flush 重試</td>
          <td>暫時性問題，重試有機會成功</td>
      </tr>
  </tbody>
</table>
<p>207 和 400 都清 buffer 的關鍵判斷：Schema 驗證失敗是 SDK 端產出了不合規的事件，問題在 SDK 的事件建構邏輯（程式碼 bug），不在 collector 或網路 — 重試相同事件永遠不會過。SDK 把失敗事件的 error 訊息記到 warning/error log 供開發者排查，然後放行後續事件。</p>
<p>503 保留 buffer 的關鍵判斷：collector 暫時不可用是基礎設施問題（SQLite busy timeout、背壓），事件本身合法，等 collector 恢復後重試會成功。<code>retry_after</code> 由 collector 在回應中指定，SDK 用這個值設定下次 flush 的最小等待時間。</p>
<h2 id="batch-格式">Batch 格式</h2>
<p>SDK 在 flush 時把 buffer 中所有事件包裝成一個 batch，帶上 <code>batch_id</code> 送出。</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;batch_id&#34;</span><span class="p">:</span> <span class="s2">&#34;019537a0-7b2c-7def-8a2b-3c4d5e6f7890&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;events&#34;</span><span class="p">:</span> <span class="p">[</span> <span class="err">...</span> <span class="p">]</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>batch_id</code> 由 SDK 在 flush 時產生。使用 UUID v7（<code>uuid.uuid7()</code>，Python 3.14+ 標準庫）——時間戳前綴保證有序（debug 時按 batch_id 排序即時間順序），隨機後綴保證唯一（高負載下多個 SDK 同時 flush 不碰撞）。用途是追蹤和 debug（collector log 中標記同一批事件的來源）。Collector 不依賴 batch_id 做去重 — 同一批事件被 SDK 重試時會帶不同的 batch_id（每次 flush 重新產生），collector 按事件內容（timestamp + source + name）判斷是否重複。</p>
<p>UUID v7 而非時間戳格式的選型理由：時間戳格式（<code>b-{YYYYMMDD}-{HHMMSSfff}</code>）在同毫秒多次 flush 時會碰撞，雖然 MVP 的 debug 用途碰撞無害，但 batch_id 碰撞在後續版本的離線補發去重場景（見 <a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer 與重試</a>）會造成歧義。UUID v7 兼顧有序和唯一，一次到位。</p>
<h2 id="heartbeat-和-flush-的整合">Heartbeat 和 flush 的整合</h2>
<p>DevOps dashboard 需要 <code>sdk.heartbeat</code> 事件判斷 SDK 是否存活。Heartbeat 不需要獨立的 timer — 整合在 flush timer 中：</p>
<p>flush timer 觸發時，如果 buffer 為空且距上次 heartbeat 超過設定間隔（預設 5 分鐘），自動注入一筆 <code>sdk.heartbeat</code> lifecycle 事件後送出。App idle 時仍有心跳但不多一個 timer；app 活躍時 heartbeat 被正常事件的 flush 取代（buffer 不會為空）。</p>
<p>Heartbeat 間隔由 SDK init config 的 <code>heartbeatInterval</code> 設定。設為 0 停用 heartbeat。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>離線場景的處理 → <a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer 與重試</a></li>
<li>SDK 公開 API → <a href="/blog/monitoring/03-sdk-design/public-api/" data-link-title="SDK 公開 API 設計" data-link-desc="init / event / error / metric / flush / close 六個方法構成 SDK 的完整生命週期 — 跨平台共用相同 API 介面">SDK 公開 API 設計</a></li>
<li>Collector 端如何接收批次事件 → <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">模組四 Collector 架構</a></li>
</ul>
]]></content:encoded></item><item><title>離線 buffer 與重試</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/offline-buffer/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/offline-buffer/</guid><description>&lt;p>離線 buffer 處理的是「事件產生時網路不可用」的場景。記憶體 buffer 有容量上限，離線時間超過 buffer 容量時需要決策：丟棄舊事件、持久化到本地儲存、或兩者混合。每種策略有不同的複雜度和資料保留量的取捨。&lt;/p>
&lt;h2 id="三種策略">三種策略&lt;/h2>
&lt;h3 id="fifo-丟棄最簡單">FIFO 丟棄（最簡單）&lt;/h3>
&lt;p>Buffer 滿時丟棄最舊的事件，保留最新的。整個 buffer 在記憶體中，不做本地 persistence。&lt;/p>
&lt;p>優點：實作最簡單（array + 容量檢查），不需要檔案系統存取，不增加磁碟 I/O。&lt;/p>
&lt;p>代價：離線超過 buffer 容量時，較舊的事件永久遺失。如果離線 30 分鐘、buffer 容量 200 筆、事件產生速率每分鐘 10 筆，前 100 筆（前 10 分鐘）的事件被丟棄。&lt;/p>
&lt;p>適合場景：自用工具（離線場景少、遺失部分事件影響低）、SDK 初期版本（先用最簡單的策略上線）。&lt;/p>
&lt;h3 id="本地-persistence最完整">本地 persistence（最完整）&lt;/h3>
&lt;p>Buffer 滿時把事件寫入本地檔案（SQLite、JSONL 檔案、SharedPreferences / UserDefaults）。網路恢復後從本地檔案讀取並補發。&lt;/p>
&lt;p>優點：離線期間的事件不會遺失（在本地儲存容量內）。&lt;/p>
&lt;p>代價：實作複雜度高 — 需要處理檔案讀寫、並發存取（多執行緒安全）、本地儲存容量管理（磁碟空間上限）、補發時的去重（同一筆事件可能已在記憶體 buffer 中被 flush 過）。&lt;/p>
&lt;p>適合場景：商業產品（使用者在地鐵、電梯、飛航模式下使用）、離線時間長且事件不可遺失的需求。&lt;/p>
&lt;h3 id="混合策略">混合策略&lt;/h3>
&lt;p>記憶體 buffer 處理正常情況和短暫離線。離線超過記憶體 buffer 容量時，溢出的事件寫入本地檔案。網路恢復後先 flush 記憶體 buffer（最新事件），再補發本地檔案中的事件（較舊事件）。&lt;/p>
&lt;p>混合策略的實作複雜度介於兩者之間。本地檔案只在溢出時使用，正常情況下不產生磁碟 I/O。&lt;/p>
&lt;h2 id="恢復後補發">恢復後補發&lt;/h2>
&lt;p>網路恢復後補發離線期間累積的事件，需要處理三個問題：&lt;/p>
&lt;h3 id="補發順序">補發順序&lt;/h3>
&lt;p>離線事件按 timestamp 順序補發，保持事件的時間順序。Collector 端收到的事件 timestamp 可能比當前時間早數小時 — 這是正常的離線補發，collector 應該根據事件的 timestamp 處理，不依賴收到時間。&lt;/p>
&lt;h3 id="補發速率">補發速率&lt;/h3>
&lt;p>一次送出大量離線事件可能讓 collector 過載。分批補發（每批 50-100 筆，間隔 1-2 秒），讓 collector 有時間處理。&lt;/p>
&lt;h3 id="去重">去重&lt;/h3>
&lt;p>同一筆事件可能同時存在於記憶體 buffer 和本地檔案中（寫入本地檔案時 buffer 中也有一份）。Collector 端用事件的唯一識別（timestamp + session_id + name 的組合，或 SDK 產生的 event_id UUID）做去重。&lt;/p>
&lt;h2 id="本地儲存容量管理">本地儲存容量管理&lt;/h2>
&lt;p>本地 persistence 需要設定磁碟使用上限。上限取決於事件大小和保留時間。&lt;/p>
&lt;p>以平均每筆事件 500 bytes 估算：&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>1 MB&lt;/td>
 &lt;td>~2,000&lt;/td>
 &lt;td>約 3 小時（每分鐘 10 筆）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10 MB&lt;/td>
 &lt;td>~20,000&lt;/td>
 &lt;td>約 33 小時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>50 MB&lt;/td>
 &lt;td>~100,000&lt;/td>
 &lt;td>約 7 天&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>自用工具 1 MB 足夠（離線場景少）。行動 app 10-50 MB 合理（使用者可能整天離線）。超過上限時用 FIFO 丟棄最舊的本地檔案。&lt;/p>
&lt;h2 id="各平台的本地儲存路徑">各平台的本地儲存路徑&lt;/h2>
&lt;p>本地 persistence 的檔案路徑和格式因平台而異。MVP 階段全用記憶體 FIFO（最簡單策略），本地 persistence 標為第二階段。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>平台&lt;/th>
 &lt;th>建議路徑&lt;/th>
 &lt;th>檔案格式&lt;/th>
 &lt;th>備註&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Flutter&lt;/td>
 &lt;td>&lt;code>getApplicationSupportDirectory()&lt;/code>&lt;/td>
 &lt;td>JSONL&lt;/td>
 &lt;td>不會被 iCloud 備份（和 Documents 不同）、不會被系統自動清理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Python&lt;/td>
 &lt;td>&lt;code>~/.cache/monitor/&lt;/code> 或 &lt;code>platformdirs.user_cache_dir('monitor')&lt;/code>&lt;/td>
 &lt;td>JSONL&lt;/td>
 &lt;td>遵循 XDG 標準、&lt;code>platformdirs&lt;/code> 套件處理跨平台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JS/Web&lt;/td>
 &lt;td>&lt;code>localStorage&lt;/code> 或 &lt;code>IndexedDB&lt;/code>&lt;/td>
 &lt;td>JSON&lt;/td>
 &lt;td>localStorage 有 5MB 限制、IndexedDB 更大但 API 較複雜&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>App 被強制終止時（iOS 的 &lt;code>kill&lt;/code>、Android 的 process death），記憶體 buffer 中未 flush 的事件會遺失。Flutter 的 &lt;code>AppLifecycleState.detached&lt;/code> 不保證有時間執行 flush。接受這個遺失 — 強制終止是極端情境，下次啟動時 SDK 重新開始收集。&lt;/p></description><content:encoded><![CDATA[<p>離線 buffer 處理的是「事件產生時網路不可用」的場景。記憶體 buffer 有容量上限，離線時間超過 buffer 容量時需要決策：丟棄舊事件、持久化到本地儲存、或兩者混合。每種策略有不同的複雜度和資料保留量的取捨。</p>
<h2 id="三種策略">三種策略</h2>
<h3 id="fifo-丟棄最簡單">FIFO 丟棄（最簡單）</h3>
<p>Buffer 滿時丟棄最舊的事件，保留最新的。整個 buffer 在記憶體中，不做本地 persistence。</p>
<p>優點：實作最簡單（array + 容量檢查），不需要檔案系統存取，不增加磁碟 I/O。</p>
<p>代價：離線超過 buffer 容量時，較舊的事件永久遺失。如果離線 30 分鐘、buffer 容量 200 筆、事件產生速率每分鐘 10 筆，前 100 筆（前 10 分鐘）的事件被丟棄。</p>
<p>適合場景：自用工具（離線場景少、遺失部分事件影響低）、SDK 初期版本（先用最簡單的策略上線）。</p>
<h3 id="本地-persistence最完整">本地 persistence（最完整）</h3>
<p>Buffer 滿時把事件寫入本地檔案（SQLite、JSONL 檔案、SharedPreferences / UserDefaults）。網路恢復後從本地檔案讀取並補發。</p>
<p>優點：離線期間的事件不會遺失（在本地儲存容量內）。</p>
<p>代價：實作複雜度高 — 需要處理檔案讀寫、並發存取（多執行緒安全）、本地儲存容量管理（磁碟空間上限）、補發時的去重（同一筆事件可能已在記憶體 buffer 中被 flush 過）。</p>
<p>適合場景：商業產品（使用者在地鐵、電梯、飛航模式下使用）、離線時間長且事件不可遺失的需求。</p>
<h3 id="混合策略">混合策略</h3>
<p>記憶體 buffer 處理正常情況和短暫離線。離線超過記憶體 buffer 容量時，溢出的事件寫入本地檔案。網路恢復後先 flush 記憶體 buffer（最新事件），再補發本地檔案中的事件（較舊事件）。</p>
<p>混合策略的實作複雜度介於兩者之間。本地檔案只在溢出時使用，正常情況下不產生磁碟 I/O。</p>
<h2 id="恢復後補發">恢復後補發</h2>
<p>網路恢復後補發離線期間累積的事件，需要處理三個問題：</p>
<h3 id="補發順序">補發順序</h3>
<p>離線事件按 timestamp 順序補發，保持事件的時間順序。Collector 端收到的事件 timestamp 可能比當前時間早數小時 — 這是正常的離線補發，collector 應該根據事件的 timestamp 處理，不依賴收到時間。</p>
<h3 id="補發速率">補發速率</h3>
<p>一次送出大量離線事件可能讓 collector 過載。分批補發（每批 50-100 筆，間隔 1-2 秒），讓 collector 有時間處理。</p>
<h3 id="去重">去重</h3>
<p>同一筆事件可能同時存在於記憶體 buffer 和本地檔案中（寫入本地檔案時 buffer 中也有一份）。Collector 端用事件的唯一識別（timestamp + session_id + name 的組合，或 SDK 產生的 event_id UUID）做去重。</p>
<h2 id="本地儲存容量管理">本地儲存容量管理</h2>
<p>本地 persistence 需要設定磁碟使用上限。上限取決於事件大小和保留時間。</p>
<p>以平均每筆事件 500 bytes 估算：</p>
<table>
  <thead>
      <tr>
          <th>上限</th>
          <th>可儲存事件數</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 MB</td>
          <td>~2,000</td>
          <td>約 3 小時（每分鐘 10 筆）</td>
      </tr>
      <tr>
          <td>10 MB</td>
          <td>~20,000</td>
          <td>約 33 小時</td>
      </tr>
      <tr>
          <td>50 MB</td>
          <td>~100,000</td>
          <td>約 7 天</td>
      </tr>
  </tbody>
</table>
<p>自用工具 1 MB 足夠（離線場景少）。行動 app 10-50 MB 合理（使用者可能整天離線）。超過上限時用 FIFO 丟棄最舊的本地檔案。</p>
<h2 id="各平台的本地儲存路徑">各平台的本地儲存路徑</h2>
<p>本地 persistence 的檔案路徑和格式因平台而異。MVP 階段全用記憶體 FIFO（最簡單策略），本地 persistence 標為第二階段。</p>
<table>
  <thead>
      <tr>
          <th>平台</th>
          <th>建議路徑</th>
          <th>檔案格式</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Flutter</td>
          <td><code>getApplicationSupportDirectory()</code></td>
          <td>JSONL</td>
          <td>不會被 iCloud 備份（和 Documents 不同）、不會被系統自動清理</td>
      </tr>
      <tr>
          <td>Python</td>
          <td><code>~/.cache/monitor/</code> 或 <code>platformdirs.user_cache_dir('monitor')</code></td>
          <td>JSONL</td>
          <td>遵循 XDG 標準、<code>platformdirs</code> 套件處理跨平台</td>
      </tr>
      <tr>
          <td>JS/Web</td>
          <td><code>localStorage</code> 或 <code>IndexedDB</code></td>
          <td>JSON</td>
          <td>localStorage 有 5MB 限制、IndexedDB 更大但 API 較複雜</td>
      </tr>
  </tbody>
</table>
<p>App 被強制終止時（iOS 的 <code>kill</code>、Android 的 process death），記憶體 buffer 中未 flush 的事件會遺失。Flutter 的 <code>AppLifecycleState.detached</code> 不保證有時間執行 flush。接受這個遺失 — 強制終止是極端情境，下次啟動時 SDK 重新開始收集。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>攢批送出策略 → <a href="/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出策略</a></li>
<li>SDK 端的資料脫敏 → <a href="/blog/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></li>
<li>Collector 端如何處理補發事件 → <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">模組四 Collector 設計</a></li>
<li>從 SDK 到 storage 的端到端資料損失地圖 → <a href="/blog/monitoring/04-collector/data-integrity/" data-link-title="端到端資料完整性" data-link-desc="從 SDK 到 storage 的資料損失地圖 — 每個環節的損失類型、控制策略、完整性指標、被自己 SDK DDoS 的防護">端到端資料完整性</a></li>
</ul>
]]></content:encoded></item><item><title>SDK redaction helper</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/redaction-helper/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/redaction-helper/</guid><description>&lt;p>SDK &lt;a href="https://tarrragon.github.io/blog/monitoring/knowledge-cards/redaction/" data-link-title="Redaction" data-link-desc="說明在事件資料離開 client 之前把敏感欄位的值替換成遮罩或移除的機制">redaction&lt;/a> helper 在事件離開 SDK（進入 HTTP POST payload）前掃描事件內容，把匹配敏感資訊 pattern 的欄位值替換為 &lt;code>[REDACTED]&lt;/code>。Redaction 在 SDK 端執行，確保敏感資訊不會經過網路傳輸到 collector — 即使 transport 層被攔截，攻擊者看到的也是脫敏後的資料。&lt;/p>
&lt;h2 id="預設-redaction-rule">預設 redaction rule&lt;/h2>
&lt;p>SDK 內建一組預設 rule，處理常見的敏感資訊 pattern：&lt;/p>
&lt;h3 id="密碼欄位">密碼欄位&lt;/h3>
&lt;p>匹配 data 物件中 key 包含 &lt;code>password&lt;/code>、&lt;code>passwd&lt;/code>、&lt;code>secret&lt;/code>、&lt;code>token&lt;/code>、&lt;code>api_key&lt;/code>、&lt;code>apiKey&lt;/code>、&lt;code>authorization&lt;/code> 的欄位。匹配方式是 key 名稱的子字串比對（case-insensitive）。&lt;/p>
&lt;h3 id="url-中的認證資訊">URL 中的認證資訊&lt;/h3>
&lt;p>匹配 &lt;code>https://user:password@host&lt;/code> 格式的 URL，把 &lt;code>user:password&lt;/code> 部分替換為 &lt;code>[REDACTED]&lt;/code>。&lt;/p>
&lt;h3 id="stack-trace-中的檔案路徑">Stack trace 中的檔案路徑&lt;/h3>
&lt;p>匹配 stack trace 字串中的使用者目錄路徑（&lt;code>/Users/username/&lt;/code>、&lt;code>/home/username/&lt;/code>、&lt;code>C:\Users\username\&lt;/code>），替換為 &lt;code>[USER_HOME]/&lt;/code>。避免使用者名稱從 stack trace 洩漏。&lt;/p>
&lt;h2 id="自訂-redaction-rule">自訂 redaction rule&lt;/h2>
&lt;p>業務特定的敏感資訊（信用卡號、身分證字號、醫療資料）不在預設 rule 的範圍內。SDK 提供 API 讓開發者在 init 時註冊自訂 rule。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Monitor.init({
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> redactionRules: [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> { pattern: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/, replace: &amp;#39;[CARD]&amp;#39; },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> { keyPattern: /^ssn$/i, replace: &amp;#39;[REDACTED]&amp;#39; },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> ],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">})&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>自訂 rule 和預設 rule 一起執行。如果同一個值被多個 rule 匹配，第一個匹配的 rule 生效（rule 的執行順序：預設 rule 先，自訂 rule 後）。&lt;/p>
&lt;h2 id="redaction-的執行時機">Redaction 的執行時機&lt;/h2>
&lt;p>Redaction 在事件進入 flush payload 的那一刻執行 — buffer 中的事件保持原始內容，flush 時複製一份並在複製上執行 redaction。&lt;/p>
&lt;p>在 buffer 中保持原始內容的理由是 debug：開發者在本地 console 看到的 log 應該包含完整資訊（開發環境不需要脫敏），只有離開 SDK 時才脫敏。SDK 可以提供 &lt;code>debugMode&lt;/code> flag — debugMode 開啟時 console log 印出原始內容，HTTP POST 仍送出脫敏後的內容。&lt;/p>
&lt;h2 id="redaction-和模組七的關係">Redaction 和模組七的關係&lt;/h2>
&lt;p>SDK redaction helper 是&lt;a href="https://tarrragon.github.io/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">模組七 資安與隱私&lt;/a>中 redaction 策略的實作層。模組七定義「什麼資訊需要被保護」（策略），本章定義「SDK 如何在程式碼中實現這個保護」（實作）。&lt;/p>
&lt;p>兩者的分工：&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>策略層&lt;/td>
 &lt;td>哪些欄位需要 redaction、哪些 pattern 敏感&lt;/td>
 &lt;td>模組七&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>實作層&lt;/td>
 &lt;td>預設 rule、自訂 rule API、執行時機&lt;/td>
 &lt;td>本章&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>驗證層&lt;/td>
 &lt;td>確認脫敏後的事件不包含敏感資訊&lt;/td>
 &lt;td>collector 端&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Collector 端可以做第二道檢查（re-scan 收到的事件是否仍包含敏感 pattern），作為 SDK 端 redaction 的備援。但主要的脫敏責任在 SDK 端 — 資料離開 SDK 後經過網路，已經暴露在傳輸風險中。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>SDK 公開 API → &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/public-api/" data-link-title="SDK 公開 API 設計" data-link-desc="init / event / error / metric / flush / close 六個方法構成 SDK 的完整生命週期 — 跨平台共用相同 API 介面">SDK 公開 API 設計&lt;/a>&lt;/li>
&lt;li>資安與隱私的完整策略 → &lt;a href="https://tarrragon.github.io/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">模組七 資安與隱私&lt;/a>&lt;/li>
&lt;li>自動攔截的 error 也需要 redaction → &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>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>SDK <a href="/blog/monitoring/knowledge-cards/redaction/" data-link-title="Redaction" data-link-desc="說明在事件資料離開 client 之前把敏感欄位的值替換成遮罩或移除的機制">redaction</a> helper 在事件離開 SDK（進入 HTTP POST payload）前掃描事件內容，把匹配敏感資訊 pattern 的欄位值替換為 <code>[REDACTED]</code>。Redaction 在 SDK 端執行，確保敏感資訊不會經過網路傳輸到 collector — 即使 transport 層被攔截，攻擊者看到的也是脫敏後的資料。</p>
<h2 id="預設-redaction-rule">預設 redaction rule</h2>
<p>SDK 內建一組預設 rule，處理常見的敏感資訊 pattern：</p>
<h3 id="密碼欄位">密碼欄位</h3>
<p>匹配 data 物件中 key 包含 <code>password</code>、<code>passwd</code>、<code>secret</code>、<code>token</code>、<code>api_key</code>、<code>apiKey</code>、<code>authorization</code> 的欄位。匹配方式是 key 名稱的子字串比對（case-insensitive）。</p>
<h3 id="url-中的認證資訊">URL 中的認證資訊</h3>
<p>匹配 <code>https://user:password@host</code> 格式的 URL，把 <code>user:password</code> 部分替換為 <code>[REDACTED]</code>。</p>
<h3 id="stack-trace-中的檔案路徑">Stack trace 中的檔案路徑</h3>
<p>匹配 stack trace 字串中的使用者目錄路徑（<code>/Users/username/</code>、<code>/home/username/</code>、<code>C:\Users\username\</code>），替換為 <code>[USER_HOME]/</code>。避免使用者名稱從 stack trace 洩漏。</p>
<h2 id="自訂-redaction-rule">自訂 redaction rule</h2>
<p>業務特定的敏感資訊（信用卡號、身分證字號、醫療資料）不在預設 rule 的範圍內。SDK 提供 API 讓開發者在 init 時註冊自訂 rule。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Monitor.init({
</span></span><span class="line"><span class="ln">2</span><span class="cl">  redactionRules: [
</span></span><span class="line"><span class="ln">3</span><span class="cl">    { pattern: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/, replace: &#39;[CARD]&#39; },
</span></span><span class="line"><span class="ln">4</span><span class="cl">    { keyPattern: /^ssn$/i, replace: &#39;[REDACTED]&#39; },
</span></span><span class="line"><span class="ln">5</span><span class="cl">  ],
</span></span><span class="line"><span class="ln">6</span><span class="cl">})</span></span></code></pre></div><p>自訂 rule 和預設 rule 一起執行。如果同一個值被多個 rule 匹配，第一個匹配的 rule 生效（rule 的執行順序：預設 rule 先，自訂 rule 後）。</p>
<h2 id="redaction-的執行時機">Redaction 的執行時機</h2>
<p>Redaction 在事件進入 flush payload 的那一刻執行 — buffer 中的事件保持原始內容，flush 時複製一份並在複製上執行 redaction。</p>
<p>在 buffer 中保持原始內容的理由是 debug：開發者在本地 console 看到的 log 應該包含完整資訊（開發環境不需要脫敏），只有離開 SDK 時才脫敏。SDK 可以提供 <code>debugMode</code> flag — debugMode 開啟時 console log 印出原始內容，HTTP POST 仍送出脫敏後的內容。</p>
<h2 id="redaction-和模組七的關係">Redaction 和模組七的關係</h2>
<p>SDK redaction helper 是<a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">模組七 資安與隱私</a>中 redaction 策略的實作層。模組七定義「什麼資訊需要被保護」（策略），本章定義「SDK 如何在程式碼中實現這個保護」（實作）。</p>
<p>兩者的分工：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>職責</th>
          <th>定義在</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>策略層</td>
          <td>哪些欄位需要 redaction、哪些 pattern 敏感</td>
          <td>模組七</td>
      </tr>
      <tr>
          <td>實作層</td>
          <td>預設 rule、自訂 rule API、執行時機</td>
          <td>本章</td>
      </tr>
      <tr>
          <td>驗證層</td>
          <td>確認脫敏後的事件不包含敏感資訊</td>
          <td>collector 端</td>
      </tr>
  </tbody>
</table>
<p>Collector 端可以做第二道檢查（re-scan 收到的事件是否仍包含敏感 pattern），作為 SDK 端 redaction 的備援。但主要的脫敏責任在 SDK 端 — 資料離開 SDK 後經過網路，已經暴露在傳輸風險中。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>SDK 公開 API → <a href="/blog/monitoring/03-sdk-design/public-api/" data-link-title="SDK 公開 API 設計" data-link-desc="init / event / error / metric / flush / close 六個方法構成 SDK 的完整生命週期 — 跨平台共用相同 API 介面">SDK 公開 API 設計</a></li>
<li>資安與隱私的完整策略 → <a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">模組七 資安與隱私</a></li>
<li>自動攔截的 error 也需要 redaction → <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>
</ul>
]]></content:encoded></item><item><title>Sampling</title><link>https://tarrragon.github.io/blog/monitoring/knowledge-cards/sampling/</link><pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/knowledge-cards/sampling/</guid><description>&lt;p>取樣（sampling）的通用概念見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">Backend 知識卡：Sampling&lt;/a> — 只保留部分觀測資料以控制成本。本卡聚焦監控 SDK 中的具體實作：在事件產生階段按比例丟棄部分事件，降低後續管線（buffer → transport → collector → storage）的負載。取樣是設計內的損失 — 取樣率是明確的 config 參數，損失量可預測。可先對照 &lt;a href="https://tarrragon.github.io/blog/monitoring/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="下游處理能力不足時向上游回傳「慢下來」訊號的流量控制機制 — 監控系統中 collector 用 HTTP 429 向 SDK 傳遞背壓">backpressure&lt;/a>（觸發動態取樣的訊號來源）和 &lt;a href="https://tarrragon.github.io/blog/monitoring/knowledge-cards/rate-limiting/" data-link-title="Rate Limiting" data-link-desc="限制每個 client 在單位時間內可送出的事件數量 — 防止單一 SDK bug 或偽造流量消耗整個 collector 的處理能力">rate limiting&lt;/a>（collector 端的 per-client 限制）。&lt;/p>
&lt;h2 id="兩種取樣">兩種取樣&lt;/h2>
&lt;p>&lt;strong>靜態取樣&lt;/strong>：SDK config 中設定固定比例（例如 metric 類 0.1 = 每 10 筆只收 1 筆），在 SDK 整個生命週期保持不變。適合已知高頻但單筆 debug 價值低的事件（render.frame_time、scroll.position）。&lt;/p>
&lt;p>&lt;strong>動態取樣&lt;/strong>：SDK 在收到 collector 的 HTTP 429 後自動降低取樣率，collector 恢復正常後逐步回升。動態取樣在正常情況下不生效（取樣率 = 1.0），只在 collector 過載時啟用。和靜態取樣互補 — 靜態控制基線負載，動態應對突發。&lt;/p>
&lt;h2 id="取樣校正">取樣校正&lt;/h2>
&lt;p>分析時用取樣率還原原始量級。取樣率 0.1 時收到 100 筆事件，推估原始量為 100 / 0.1 = 1000 筆。SDK 端的 &lt;code>sdk.sampling.rate&lt;/code> 指標記錄當前取樣率，讓下游分析知道如何校正。取樣校正對 funnel 和 cohort 分析有效（趨勢和比例不變），對個別事件追蹤無效（被丟棄的事件無法回復）。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>取樣承擔的設計責任是「在可觀測性覆蓋率和系統負載之間找到平衡」。Error 類事件不做取樣（每筆都可能是需要修的 bug），metric 類事件適合高比例取樣（丟幾筆不影響趨勢），event 類和 lifecycle 類取決於分析需求。&lt;/p>
&lt;h2 id="完整章節">完整章節&lt;/h2>
&lt;p>靜態取樣率的設定 → &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/sensor-lifecycle-management/" data-link-title="感測器生命週期管理" data-link-desc="產品生命週期的五個階段各啟用什麼感測器 — feature flag 整合、取樣率動態調整、感測器開關的可觀察性">感測器生命週期管理&lt;/a>。動態取樣在四層防線中的位置 → &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling&lt;/a>。取樣造成的損失量化和控制 → &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/data-integrity/" data-link-title="端到端資料完整性" data-link-desc="從 SDK 到 storage 的資料損失地圖 — 每個環節的損失類型、控制策略、完整性指標、被自己 SDK DDoS 的防護">端到端資料完整性&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>取樣（sampling）的通用概念見 <a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">Backend 知識卡：Sampling</a> — 只保留部分觀測資料以控制成本。本卡聚焦監控 SDK 中的具體實作：在事件產生階段按比例丟棄部分事件，降低後續管線（buffer → transport → collector → storage）的負載。取樣是設計內的損失 — 取樣率是明確的 config 參數，損失量可預測。可先對照 <a href="/blog/monitoring/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="下游處理能力不足時向上游回傳「慢下來」訊號的流量控制機制 — 監控系統中 collector 用 HTTP 429 向 SDK 傳遞背壓">backpressure</a>（觸發動態取樣的訊號來源）和 <a href="/blog/monitoring/knowledge-cards/rate-limiting/" data-link-title="Rate Limiting" data-link-desc="限制每個 client 在單位時間內可送出的事件數量 — 防止單一 SDK bug 或偽造流量消耗整個 collector 的處理能力">rate limiting</a>（collector 端的 per-client 限制）。</p>
<h2 id="兩種取樣">兩種取樣</h2>
<p><strong>靜態取樣</strong>：SDK config 中設定固定比例（例如 metric 類 0.1 = 每 10 筆只收 1 筆），在 SDK 整個生命週期保持不變。適合已知高頻但單筆 debug 價值低的事件（render.frame_time、scroll.position）。</p>
<p><strong>動態取樣</strong>：SDK 在收到 collector 的 HTTP 429 後自動降低取樣率，collector 恢復正常後逐步回升。動態取樣在正常情況下不生效（取樣率 = 1.0），只在 collector 過載時啟用。和靜態取樣互補 — 靜態控制基線負載，動態應對突發。</p>
<h2 id="取樣校正">取樣校正</h2>
<p>分析時用取樣率還原原始量級。取樣率 0.1 時收到 100 筆事件，推估原始量為 100 / 0.1 = 1000 筆。SDK 端的 <code>sdk.sampling.rate</code> 指標記錄當前取樣率，讓下游分析知道如何校正。取樣校正對 funnel 和 cohort 分析有效（趨勢和比例不變），對個別事件追蹤無效（被丟棄的事件無法回復）。</p>
<h2 id="設計責任">設計責任</h2>
<p>取樣承擔的設計責任是「在可觀測性覆蓋率和系統負載之間找到平衡」。Error 類事件不做取樣（每筆都可能是需要修的 bug），metric 類事件適合高比例取樣（丟幾筆不影響趨勢），event 類和 lifecycle 類取決於分析需求。</p>
<h2 id="完整章節">完整章節</h2>
<p>靜態取樣率的設定 → <a href="/blog/monitoring/03-sdk-design/sensor-lifecycle-management/" data-link-title="感測器生命週期管理" data-link-desc="產品生命週期的五個階段各啟用什麼感測器 — feature flag 整合、取樣率動態調整、感測器開關的可觀察性">感測器生命週期管理</a>。動態取樣在四層防線中的位置 → <a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling</a>。取樣造成的損失量化和控制 → <a href="/blog/monitoring/04-collector/data-integrity/" data-link-title="端到端資料完整性" data-link-desc="從 SDK 到 storage 的資料損失地圖 — 每個環節的損失類型、控制策略、完整性指標、被自己 SDK DDoS 的防護">端到端資料完整性</a>。</p>
]]></content:encoded></item><item><title>Client-side SDK 認證的根本限制</title><link>https://tarrragon.github.io/blog/monitoring/07-security-privacy/client-sdk-authentication/</link><pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/07-security-privacy/client-sdk-authentication/</guid><description>&lt;p>當監控 SDK 部署在使用者裝置上（瀏覽器、手機 app、本機腳本），collector 的 ingestion endpoint 就暴露在外部網路 — 認證機制需要面對 credential 必然可被提取的前提。Client-side SDK 的認證和 server-side API 的認證面對的是結構性不同的問題。Server-side 的 API key 存在環境變數或 secret store 裡，只有 server process 能讀取。Client-side SDK 的 credential 必須嵌入到使用者手上的程式碼中 — JS bundle、APK、Python script — 使用者（或攻擊者）可以直接讀取。&lt;/p>
&lt;p>這個限制來自 architecture，和 implementation 無關。混淆 JS、ProGuard 混淆 APK、編譯 Python 成 &lt;code>.pyc&lt;/code>，都只增加提取成本，不改變「credential 在 client 端」的事實。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/monitoring/07-security-privacy/collector-access-control/" data-link-title="Collector Access Control 實作" data-link-desc="認證（誰在送資料）/ 授權（允許送什麼）/ access log（誰在什麼時候送了什麼）— collector 端的三層存取控制">Collector Access Control&lt;/a> 討論了 API key 和 mTLS 的認證機制，&lt;a href="https://tarrragon.github.io/blog/monitoring/07-security-privacy/transport-security/" data-link-title="Transport 安全" data-link-desc="HTTPS / basic auth / 同區網也要加密的理由 — 監控資料在傳輸途中的保護機制">Transport 安全&lt;/a> 討論了傳輸層加密。兩者的前提是 credential 被妥善保管。本章處理的是那個前提不成立時 — credential 已被提取或必然可被提取 — 的緩解策略。&lt;/p>
&lt;h2 id="商業方案的處理方式">商業方案的處理方式&lt;/h2>
&lt;p>所有主流的 client-side telemetry 方案都面對同樣的限制。它們的共同策略是：承認 client credential 會暴露，把防線從「保護 credential」轉移到「限制 credential 被濫用的影響」。&lt;/p>
&lt;p>&lt;strong>Google Analytics 4&lt;/strong>：Measurement ID（G-XXXXXXXXXX）直接寫在網頁的 JS snippet 中，任何人檢視網頁原始碼都能取得。GA4 的防護在 server-side — Google 用 domain 白名單過濾來源，加上自動的 bot traffic 偵測剔除機器流量。Measurement Protocol（server-to-server）需要額外的 API secret，但 client-side 的 gtag.js 不需要。&lt;/p>
&lt;p>&lt;strong>Sentry&lt;/strong>：DSN（Data Source Name）包含 project ID 和 public key，直接嵌在 SDK init 的程式碼中。Sentry 官方文件明確標示 DSN 是 public 的 — 攻擊者取得 DSN 只能送事件，不能讀取已收集的資料。防護靠 rate limit（每個 project 的 events/sec 上限）、allowed domains（只接受來自白名單 domain 的事件）、和 server-side 的 event 去重。&lt;/p>
&lt;p>&lt;strong>Firebase&lt;/strong>：整個 &lt;code>google-services.json&lt;/code> / &lt;code>GoogleService-Info.plist&lt;/code> 的內容 — 包含 apiKey、projectId、appId — 都視為公開資訊。Firebase 的安全模型不依賴這些 key 的保密性；它們的功能是識別（identify）而非授權（authorize）。需要保護的資源靠 Firebase Security Rules 和 App Check（device attestation）處理。&lt;/p>
&lt;p>&lt;strong>Datadog RUM&lt;/strong>：Client token 是獨立於 API key 的 credential。API key 可以讀寫所有 Datadog 資料，必須保護在 server-side；client token 只能寫入 RUM 事件，設計上可以暴露在 client 端。Datadog 建議搭配 intake proxy（collector 前面加一層自己的 server），讓 client token 不直接出現在瀏覽器中。&lt;/p></description><content:encoded><![CDATA[<p>當監控 SDK 部署在使用者裝置上（瀏覽器、手機 app、本機腳本），collector 的 ingestion endpoint 就暴露在外部網路 — 認證機制需要面對 credential 必然可被提取的前提。Client-side SDK 的認證和 server-side API 的認證面對的是結構性不同的問題。Server-side 的 API key 存在環境變數或 secret store 裡，只有 server process 能讀取。Client-side SDK 的 credential 必須嵌入到使用者手上的程式碼中 — JS bundle、APK、Python script — 使用者（或攻擊者）可以直接讀取。</p>
<p>這個限制來自 architecture，和 implementation 無關。混淆 JS、ProGuard 混淆 APK、編譯 Python 成 <code>.pyc</code>，都只增加提取成本，不改變「credential 在 client 端」的事實。</p>
<p><a href="/blog/monitoring/07-security-privacy/collector-access-control/" data-link-title="Collector Access Control 實作" data-link-desc="認證（誰在送資料）/ 授權（允許送什麼）/ access log（誰在什麼時候送了什麼）— collector 端的三層存取控制">Collector Access Control</a> 討論了 API key 和 mTLS 的認證機制，<a href="/blog/monitoring/07-security-privacy/transport-security/" data-link-title="Transport 安全" data-link-desc="HTTPS / basic auth / 同區網也要加密的理由 — 監控資料在傳輸途中的保護機制">Transport 安全</a> 討論了傳輸層加密。兩者的前提是 credential 被妥善保管。本章處理的是那個前提不成立時 — credential 已被提取或必然可被提取 — 的緩解策略。</p>
<h2 id="商業方案的處理方式">商業方案的處理方式</h2>
<p>所有主流的 client-side telemetry 方案都面對同樣的限制。它們的共同策略是：承認 client credential 會暴露，把防線從「保護 credential」轉移到「限制 credential 被濫用的影響」。</p>
<p><strong>Google Analytics 4</strong>：Measurement ID（G-XXXXXXXXXX）直接寫在網頁的 JS snippet 中，任何人檢視網頁原始碼都能取得。GA4 的防護在 server-side — Google 用 domain 白名單過濾來源，加上自動的 bot traffic 偵測剔除機器流量。Measurement Protocol（server-to-server）需要額外的 API secret，但 client-side 的 gtag.js 不需要。</p>
<p><strong>Sentry</strong>：DSN（Data Source Name）包含 project ID 和 public key，直接嵌在 SDK init 的程式碼中。Sentry 官方文件明確標示 DSN 是 public 的 — 攻擊者取得 DSN 只能送事件，不能讀取已收集的資料。防護靠 rate limit（每個 project 的 events/sec 上限）、allowed domains（只接受來自白名單 domain 的事件）、和 server-side 的 event 去重。</p>
<p><strong>Firebase</strong>：整個 <code>google-services.json</code> / <code>GoogleService-Info.plist</code> 的內容 — 包含 apiKey、projectId、appId — 都視為公開資訊。Firebase 的安全模型不依賴這些 key 的保密性；它們的功能是識別（identify）而非授權（authorize）。需要保護的資源靠 Firebase Security Rules 和 App Check（device attestation）處理。</p>
<p><strong>Datadog RUM</strong>：Client token 是獨立於 API key 的 credential。API key 可以讀寫所有 Datadog 資料，必須保護在 server-side；client token 只能寫入 RUM 事件，設計上可以暴露在 client 端。Datadog 建議搭配 intake proxy（collector 前面加一層自己的 server），讓 client token 不直接出現在瀏覽器中。</p>
<p>這些方案的共同模式：client-side credential 的角色是「識別來源」而非「授權存取」。即使被提取，攻擊者能做的事被限縮在「寫入事件」— 影響可控。</p>
<h2 id="認證天花板識別-vs-授權">認證天花板：識別 vs 授權</h2>
<p><a href="/blog/monitoring/07-security-privacy/collector-access-control/" data-link-title="Collector Access Control 實作" data-link-desc="認證（誰在送資料）/ 授權（允許送什麼）/ access log（誰在什麼時候送了什麼）— collector 端的三層存取控制">Collector Access Control</a> 的 API key 同時承擔識別和授權 — 有 key 就能寫入，沒 key 就被拒絕。在 server-side 場景下這沒有問題，因為 key 不會暴露。</p>
<p>Client-side 場景需要拆開這兩個功能：</p>
<p><strong>識別（identification）</strong>：這個 request 來自哪個 app、哪個 SDK、哪個部署版本。識別資訊可以公開 — 它的價值是讓 collector 知道事件來自哪裡，用於 access log、per-app rate limit、和事件標記。</p>
<p><strong>授權（authorization）</strong>：這個 request 有沒有權限執行寫入操作。授權依賴 credential 的保密性 — 在 client-side 場景下，credential 保密性的天花板很低。</p>
<p>接受這個區分後，client-side SDK 的 API key 更接近「識別 token」。它的洩漏不是安全事件（像 server-side API key 洩漏那樣），而是預期中的狀態。防護的重點從「防止 key 洩漏」轉移到「限制 key 被濫用時的影響」。</p>
<h2 id="多層緩解策略">多層緩解策略</h2>
<p>以下各層按實作成本遞增排列。前面的層在多數場景下足夠，後面的層在 endpoint 暴露在公開網路且面對主動攻擊時才需要。</p>
<h3 id="第一層寫入限制collector-已有">第一層：寫入限制（collector 已有）</h3>
<p><a href="/blog/monitoring/07-security-privacy/collector-access-control/" data-link-title="Collector Access Control 實作" data-link-desc="認證（誰在送資料）/ 授權（允許送什麼）/ access log（誰在什麼時候送了什麼）— collector 端的三層存取控制">Collector Access Control</a> 的寫入限制 — rate limit、payload size limit、schema validation — 是第一層防護。這些機制不區分「合法 SDK」和「偽造 client」，對所有寫入請求一視同仁地施加約束。</p>
<p>Rate limit 限制每個 API key 的事件速率。Schema validation 拒絕不符合 <a href="/blog/monitoring/02-log-schema/event-schema-fields/" data-link-title="event.schema.json 完整欄位解說" data-link-desc="監控事件的 JSON Schema 定義 — 每個欄位的語意、必填/選填、資料型別和設計理由">event.schema.json</a> 結構的 payload。兩者合起來把偽造流量的影響限制在「每秒 N 筆符合 schema 的事件」— 這個量級的資料汙染對 error tracking 的影響有限（error 事件靠 stack trace fingerprint 去重），對 funnel 分析的影響較大（行為事件的計數會被灌水）。</p>
<h3 id="第二層origin-驗證">第二層：Origin 驗證</h3>
<p>Web SDK 的 HTTP request 帶有瀏覽器自動附加的 <code>Origin</code> header。Collector 可以檢查 Origin 是否在白名單中。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">originCheck</span><span class="p">(</span><span class="nx">next</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span><span class="p">,</span> <span class="nx">allowed</span> <span class="p">[]</span><span class="kt">string</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">allowedSet</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">bool</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">o</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">allowed</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nx">allowedSet</span><span class="p">[</span><span class="nx">o</span><span class="p">]</span> <span class="p">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">return</span> <span class="nx">http</span><span class="p">.</span><span class="nf">HandlerFunc</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">origin</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Header</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;Origin&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">if</span> <span class="nx">origin</span> <span class="o">!=</span> <span class="s">&#34;&#34;</span> <span class="o">&amp;&amp;</span> <span class="p">!</span><span class="nx">allowedSet</span><span class="p">[</span><span class="nx">origin</span><span class="p">]</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;forbidden origin&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusForbidden</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">next</span><span class="p">.</span><span class="nf">ServeHTTP</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Origin 驗證擋住的是「從瀏覽器中跨域呼叫」的場景 — 攻擊者在自己的網站用 JS 向你的 collector 發 request，瀏覽器會帶上攻擊者網站的 Origin，被 collector 拒絕。</p>
<p><strong>天花板</strong>：Origin header 只有瀏覽器會自動附加。用 <code>curl</code>、Postman、或任何非瀏覽器 HTTP client 發 request 時，可以自行設定任意 Origin 值。Origin 驗證擋得住瀏覽器中的跨域呼叫，擋不住直接用 HTTP client 偽造的 request。</p>
<p>Mobile SDK（Flutter / native app）的 request 不帶 Origin header。Origin 驗證只對 Web SDK 有效。</p>
<h3 id="第三層request-signing">第三層：Request signing</h3>
<p>SDK 用 HMAC 對每個 request 簽章，collector 驗證簽章有效性。簽章的輸入包含 timestamp 和 payload hash，防止 replay attack 和 payload 竄改。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">X-Signature: a3f8c2e1b7d94f06...  (HMAC-SHA256 結果的 hex 編碼)
</span></span><span class="line"><span class="ln">2</span><span class="cl">X-Timestamp: 1719216000</span></span></code></pre></div><p>SDK 計算方式：<code>HMAC-SHA256(secret, timestamp + &quot;.&quot; + SHA256(body))</code>，結果轉 hex 字串放入 <code>X-Signature</code> header。</p>
<p>Collector 端的驗證邏輯：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">verifySignature</span><span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">,</span> <span class="nx">secret</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">ts</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Header</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;X-Timestamp&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">sig</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Header</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;X-Signature&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="c1">// 拒絕超過 5 分鐘的 request timestamp（防 replay）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="c1">// 5 分鐘容忍 client-server 時鐘漂移和網路延遲；行動裝置偏差大的環境可放寬到 10 分鐘</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="c1">// 此處的 timestamp 是 HTTP request 發出時間，和事件的 timestamp 欄位（事件產生時間）無關</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">tsInt</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nf">ParseInt</span><span class="p">(</span><span class="nx">ts</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">64</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="o">||</span> <span class="nf">abs</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">().</span><span class="nf">Unix</span><span class="p">()</span><span class="o">-</span><span class="nx">tsInt</span><span class="p">)</span> <span class="p">&gt;</span> <span class="mi">300</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">return</span> <span class="kc">false</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">body</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">io</span><span class="p">.</span><span class="nf">ReadAll</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">Body</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">bodyHash</span> <span class="o">:=</span> <span class="nx">sha256</span><span class="p">.</span><span class="nf">Sum256</span><span class="p">(</span><span class="nx">body</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nx">expected</span> <span class="o">:=</span> <span class="nx">hmac</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">sha256</span><span class="p">.</span><span class="nx">New</span><span class="p">,</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">secret</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">expected</span><span class="p">.</span><span class="nf">Write</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">ts</span> <span class="o">+</span> <span class="s">&#34;.&#34;</span> <span class="o">+</span> <span class="nx">hex</span><span class="p">.</span><span class="nf">EncodeToString</span><span class="p">(</span><span class="nx">bodyHash</span><span class="p">[:])))</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nx">sigBytes</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">hex</span><span class="p">.</span><span class="nf">DecodeString</span><span class="p">(</span><span class="nx">sig</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="k">return</span> <span class="kc">false</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="k">return</span> <span class="nx">hmac</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">sigBytes</span><span class="p">,</span> <span class="nx">expected</span><span class="p">.</span><span class="nf">Sum</span><span class="p">(</span><span class="kc">nil</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Request signing 增加偽造成本 — 攻擊者需要提取 HMAC secret 並實作簽章邏輯，而非直接複製一個 API key 貼到 curl 指令。</p>
<p>HMAC secret 和 API key 一樣嵌在 client 端程式碼中，反編譯 APK 或閱讀 JS bundle 可以提取。Signing 增加的是攻擊者的工程投入（需要理解簽章算法並正確實作），而非理論上的安全性。對 casual attacker（看到 API key 就想試試的人）有效，對 motivated attacker（願意花時間逆向工程的人）無效。</p>
<h3 id="第四層行為分析異常偵測">第四層：行為分析異常偵測</h3>
<p>Collector 端統計每個 API key（或 source.app）的事件模式，建立 baseline 後偵測偏離。</p>
<p>正常 SDK 的行為有可預測的特徵：</p>
<table>
  <thead>
      <tr>
          <th>特徵</th>
          <th>正常 SDK 的 pattern</th>
          <th>偽造流量的 pattern</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事件類型分布</td>
          <td>error / event / lifecycle / metric 四類混合</td>
          <td>可能只有單一類型</td>
      </tr>
      <tr>
          <td>事件間隔</td>
          <td>攢批送出，interval 接近 SDK config 的 flush interval</td>
          <td>固定間隔或連續送出</td>
      </tr>
      <tr>
          <td>Payload 結構</td>
          <td><code>source.sdk</code> / <code>source.platform</code> / <code>source.app</code> 值穩定</td>
          <td>可能缺少 SDK 自動填入的欄位</td>
      </tr>
      <tr>
          <td>Session 行為</td>
          <td>有 lifecycle 事件（session.begin / session.end）</td>
          <td>可能沒有 session 邊界</td>
      </tr>
      <tr>
          <td>時間分布</td>
          <td>跟使用者活動時段相關（工作時間 / 使用高峰）</td>
          <td>可能 24 小時均勻分布</td>
      </tr>
  </tbody>
</table>
<p>Collector 可以用 rule engine 偵測異常模式：</p>
<ul>
<li>單一 API key 的事件量在 10 分鐘內超過過去 24 小時平均值的 10 倍</li>
<li>連續 N 個 request 的事件全是同一個 type</li>
<li><code>source.sdk</code> 欄位的值不在已知的 SDK 版本清單中</li>
</ul>
<p>偵測到異常後的處理方式是標記而非丟棄 — 在事件中加入 <code>_flags.suspicious = true</code> flag，讓 dashboard 和分析查詢可以過濾。直接丟棄有誤殺正常流量的風險（例如行銷活動導致的真實流量暴增）。</p>
<p>攻擊者如果研究過正常 SDK 的行為模式（事件類型分布、送出間隔、payload 結構），可以模擬出相似的流量。行為分析依賴「偽造流量和正常流量有可偵測的差異」這個前提 — 對低投入的攻擊者成立，對高投入的攻擊者不一定。</p>
<h3 id="第五層device-attestation">第五層：Device attestation</h3>
<p>由作業系統或平台層驗證 client 的合法性，提供 SDK 自身無法產生的證明。</p>
<p><strong>Firebase App Check</strong>：整合 DeviceCheck（iOS）、Play Integrity（Android）、reCAPTCHA Enterprise（Web），由裝置平台出具 attestation token。Collector 向 Firebase 驗證 token 的有效性。</p>
<p><strong>Apple DeviceCheck / App Attest</strong>：iOS 裝置向 Apple server 請求 attestation，證明 request 來自一台真實的、未被篡改的 iOS 裝置上的合法 app。</p>
<p><strong>Google Play Integrity</strong>：驗證 request 來自 Google Play 安裝的 app、在未 root 的裝置上、由合法使用者操作。</p>
<p>Device attestation 提供的保證比前四層都強 — 它依賴裝置硬體和平台服務（難以偽造），而非 SDK 嵌入的 secret（可提取）。</p>
<p><strong>天花板</strong>：</p>
<ul>
<li>平台綁定 — 每個平台（iOS / Android / Web）需要各自整合不同的 attestation 服務，跨平台 SDK 的實作成本高</li>
<li>Root / 越獄裝置上 attestation 可能失敗或被繞過</li>
<li>Web 端的 reCAPTCHA 驗證依賴 Google 服務，有隱私和可用性的考量</li>
<li>自架 collector 需要額外整合 Firebase Admin SDK 或各平台的驗證 API</li>
</ul>
<p>Device attestation 適合商業產品級的 mobile app，對自架監控工具而言實作成本通常超出收益。</p>
<h2 id="自架方案的規模對應">自架方案的規模對應</h2>
<p>不同部署規模下，需要做到哪一層取決於 endpoint 的暴露程度和偽造流量的影響大小。</p>
<table>
  <thead>
      <tr>
          <th>部署場景</th>
          <th>暴露程度</th>
          <th>建議做到的層級</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自用（1 人，同機 / 同網段）</td>
          <td>低 — endpoint 不對外</td>
          <td>HTTPS + basic auth</td>
          <td>攻擊面只有同網段，認證足夠</td>
      </tr>
      <tr>
          <td>小型團隊（&lt; 100 人，VPN 內）</td>
          <td>低 — endpoint 在 VPN 後</td>
          <td>API key + rate limit</td>
          <td>VPN 已限制存取範圍，rate limit 防 SDK bug</td>
      </tr>
      <tr>
          <td>公開 endpoint（VPS / 雲端）</td>
          <td>高 — 任何人可存取</td>
          <td>第一到第四層 + WAF</td>
          <td>rate limit + origin + signing + 行為分析 + CDN/WAF 的 IP reputation 過濾</td>
      </tr>
      <tr>
          <td>商業產品（app store 發佈）</td>
          <td>高 — APK 可反編譯，JS 可檢視原始碼</td>
          <td>第一到第五層 + intake proxy</td>
          <td>需要 device attestation 和 proxy 層把 credential 從 client 端移除</td>
      </tr>
  </tbody>
</table>
<p><strong>Intake proxy 架構</strong>：在公開 endpoint 和商業產品場景下，可以在 collector 前面加一層自己的 server（proxy），SDK 送事件到 proxy，proxy 用 server-side API key 轉發到 collector。Client 端的 credential 只指向 proxy，proxy 的 API key 指向 collector — credential 分層，client 端的 key 洩漏不影響 collector 的認證。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">SDK ──(client token)──→ Intake Proxy ──(server API key)──→ Collector</span></span></code></pre></div><p>Proxy 的額外成本是多一個 server 和網路跳躍。自用場景下不需要；endpoint 公開時值得考慮。</p>
<h2 id="偽造流量的影響分析">偽造流量的影響分析</h2>
<p>偽造流量進入 collector 後，對不同類型的分析影響不同。</p>
<p><strong>Error tracking 影響較低</strong>：error 事件的價值在 stack trace 和 error message。偽造的 error 事件缺少真實的 stack trace — 即使格式正確，內容是編造的。Error 去重靠 fingerprint（error type + message + stack trace top frame），偽造事件產生的 fingerprint 不會和真實 error 碰撞，在 dashboard 上是獨立的 error group，容易識別和過濾。</p>
<p><strong>行為分析影響較高</strong>：funnel 和 cohort 分析依賴事件計數的準確性。偽造的 <code>page.view</code> 和 <code>button.click</code> 事件直接灌水計數，導致轉換率失真。偽造事件越接近真實事件的結構（正確的 event name、合理的 timestamp），影響越大。</p>
<p><strong>資源消耗是固定成本</strong>：無論事件內容是否真實，每筆事件都消耗 collector 的寫入 I/O、儲存空間、和查詢時間。Rate limit 把這個成本限制在可控範圍 — 每秒 N 筆是上限，無論來源是否合法。</p>
<h3 id="事後標記策略">事後標記策略</h3>
<p>偵測到可疑流量後，collector 在事件中加入標記欄位而非直接丟棄。丟棄有誤殺風險 — 行銷活動的流量暴增、SDK 版本升級改變了事件模式、新平台的 SDK 上線 — 這些正常場景可能觸發異常偵測。</p>
<p>標記方式是在 collector 寫入時，對符合異常條件的事件附加 metadata：</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;v&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;event&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;button.click&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;sdk&#34;</span><span class="p">:</span> <span class="s2">&#34;js&#34;</span><span class="p">,</span> <span class="nt">&#34;platform&#34;</span><span class="p">:</span> <span class="s2">&#34;web&#34;</span><span class="p">,</span> <span class="nt">&#34;app&#34;</span><span class="p">:</span> <span class="s2">&#34;main-site&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nt">&#34;_flags&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;suspicious&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nt">&#34;reason&#34;</span><span class="p">:</span> <span class="s2">&#34;rate_anomaly&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Dashboard 查詢預設排除 <code>_flags.suspicious = true</code> 的事件。需要調查時可以包含 — 看可疑事件的模式有助於判斷是攻擊還是誤判。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Collector 端的認證和授權機制 → <a href="/blog/monitoring/07-security-privacy/collector-access-control/" data-link-title="Collector Access Control 實作" data-link-desc="認證（誰在送資料）/ 授權（允許送什麼）/ access log（誰在什麼時候送了什麼）— collector 端的三層存取控制">Collector Access Control 實作</a></li>
<li>Transport 層的加密保護 → <a href="/blog/monitoring/07-security-privacy/transport-security/" data-link-title="Transport 安全" data-link-desc="HTTPS / basic auth / 同區網也要加密的理由 — 監控資料在傳輸途中的保護機制">Transport 安全</a></li>
<li>Endpoint 濫用的威脅分析 → <a href="/blog/monitoring/07-security-privacy/monitoring-data-threat-model/" data-link-title="監控資料洩漏的 Threat Model" data-link-desc="監控系統本身是攻擊面 — 四個威脅場景（傳輸竊聽 / 儲存入侵 / endpoint 濫用 / 內部越權存取）的風險評估和防護措施">監控資料洩漏的 Threat Model</a></li>
<li>SDK 端的寫入速率控制 → <a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling</a></li>
<li>行為分析和 rule engine → <a href="/blog/monitoring/04-collector/rule-engine/" data-link-title="Rule engine 設計" data-link-desc="條件 → 動作 → 模板的三段式規則結構 — 讓 collector 從被動儲存變成主動回應">Rule Engine 設計</a></li>
<li>偽造流量對資料完整性的影響 → <a href="/blog/monitoring/04-collector/data-integrity/" data-link-title="端到端資料完整性" data-link-desc="從 SDK 到 storage 的資料損失地圖 — 每個環節的損失類型、控制策略、完整性指標、被自己 SDK DDoS 的防護">端到端資料完整性</a></li>
<li>Error fingerprint 讓偽造 error 容易辨識 → <a href="/blog/monitoring/04-collector/error-fingerprint/" data-link-title="Error Fingerprint 與去重分群" data-link-desc="把大量 error 事件歸組成可管理的 issue 列表 — fingerprint 演算法、message normalization、error_groups 表設計、自架方案的務實邊界">Error Fingerprint 與去重分群</a></li>
</ul>
]]></content:encoded></item></channel></rss>