<?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>Completeness on Tarragon</title><link>https://tarrragon.github.io/blog/tags/completeness/</link><description>Recent content in Completeness 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/completeness/index.xml" rel="self" type="application/rss+xml"/><item><title>事件枚舉與補齊檢查</title><link>https://tarrragon.github.io/blog/monitoring/01-mental-model/event-enumeration-method/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/01-mental-model/event-enumeration-method/</guid><description>&lt;p>事件枚舉的目的是為一個服務建立完整的事件清單 — 每個事件有明確的類型、名稱、觸發時機和 data schema。枚舉的方法從操作盤點出發，經過四類補齊檢查，產出可以直接實作 SDK 埋點的事件表。&lt;/p>
&lt;h2 id="從操作盤點推導事件">從操作盤點推導事件&lt;/h2>
&lt;p>每個使用者操作（BDD 操作盤點的產物）至少對應一個 event 類型的事件。操作的失敗路徑對應 error 類型。操作涉及的效能測量對應 metric 類型。操作觸發的系統狀態轉換對應 lifecycle 類型。&lt;/p>
&lt;p>推導鏈：操作 → 四類事件候選 → 命名 → data schema。&lt;/p>
&lt;p>以一個透過 WebSocket 連接遠端終端機的 app 為例，「連線到終端機」這個操作推導出的事件：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>四類&lt;/th>
 &lt;th>事件名稱&lt;/th>
 &lt;th>觸發時機&lt;/th>
 &lt;th>data schema&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>event&lt;/td>
 &lt;td>terminal.connect.start&lt;/td>
 &lt;td>使用者點擊連線按鈕&lt;/td>
 &lt;td>&lt;code>{url, trigger: &amp;quot;manual&amp;quot; | &amp;quot;auto&amp;quot;}&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>event&lt;/td>
 &lt;td>terminal.connect.done&lt;/td>
 &lt;td>連線成功、開始接收 output&lt;/td>
 &lt;td>&lt;code>{url, duration_ms}&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>error&lt;/td>
 &lt;td>terminal.connect.failed&lt;/td>
 &lt;td>連線失敗（逾時、拒絕、認證失敗）&lt;/td>
 &lt;td>&lt;code>{url, error, step}&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>metric&lt;/td>
 &lt;td>terminal.connect.duration&lt;/td>
 &lt;td>連線完成（成功或失敗）&lt;/td>
 &lt;td>&lt;code>{duration_ms, success: bool}&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>lifecycle&lt;/td>
 &lt;td>ws.connected&lt;/td>
 &lt;td>WebSocket 連線狀態轉換&lt;/td>
 &lt;td>&lt;code>{url}&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>lifecycle&lt;/td>
 &lt;td>ws.disconnected&lt;/td>
 &lt;td>WebSocket 斷線&lt;/td>
 &lt;td>&lt;code>{url, reason, code}&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>一個操作推導出六個事件 — 因為這個操作跨越了使用者行為（event）、可能失敗（error）、有效能測量（metric）、涉及系統狀態轉換（lifecycle）四個面向。其中 &lt;code>connect.done&lt;/code> 和 &lt;code>connect.duration&lt;/code> 記錄的是同一事實的兩個面向（見下方邊界案例段），自用場景合併成 &lt;code>connect.done&lt;/code> 帶 &lt;code>duration_ms&lt;/code> 欄位更簡潔。&lt;/p>
&lt;h2 id="四類補齊檢查">四類補齊檢查&lt;/h2>
&lt;p>列完所有操作的事件後，對每個功能區域跑一次四類補齊檢查 — 逐列確認每一類是否都有對應的事件。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>功能區域&lt;/th>
 &lt;th>event&lt;/th>
 &lt;th>error&lt;/th>
 &lt;th>metric&lt;/th>
 &lt;th>lifecycle&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>連線&lt;/td>
 &lt;td>connect.start / connect.done&lt;/td>
 &lt;td>connect.failed&lt;/td>
 &lt;td>connect.duration&lt;/td>
 &lt;td>ws.connected / ws.disconnected&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>認證&lt;/td>
 &lt;td>auth.biometric.attempt&lt;/td>
 &lt;td>auth.biometric.failed&lt;/td>
 &lt;td>auth.duration&lt;/td>
 &lt;td>auth.state_changed&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>輸入&lt;/td>
 &lt;td>input.submit&lt;/td>
 &lt;td>input.parse_error&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>—&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>配對&lt;/td>
 &lt;td>enrollment.qr.scan / enrollment.done&lt;/td>
 &lt;td>enrollment.failed&lt;/td>
 &lt;td>enrollment.duration&lt;/td>
 &lt;td>—&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>空格是候選遺漏。每個空格問一個問題：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>event 空&lt;/strong>：「這個功能區域有使用者操作嗎？」有 → 補事件；沒有（純系統內部）→ 合理的空格&lt;/li>
&lt;li>&lt;strong>error 空&lt;/strong>：「這個功能區域能失敗嗎？」能 → 補事件；不能失敗的功能極少 → 再想一次&lt;/li>
&lt;li>&lt;strong>metric 空&lt;/strong>：「這個功能區域有值得量測的效能指標嗎？」有 → 補事件；操作瞬間完成且不涉及外部依賴 → 合理的空格&lt;/li>
&lt;li>&lt;strong>lifecycle 空&lt;/strong>：「這個功能區域涉及系統狀態轉換嗎？」有 → 補事件；純資料操作不改系統狀態 → 合理的空格&lt;/li>
&lt;/ul>
&lt;p>上表中「輸入」的 metric 和 lifecycle 空格是合理的 — 文字輸入送出不涉及效能量測和系統狀態轉換。「配對」的 lifecycle 空格也合理 — 配對完成後不改變系統的執行狀態。&lt;/p>
&lt;h2 id="粒度判準">粒度判準&lt;/h2>
&lt;p>事件粒度的判斷用一個 SRP 判準：&lt;strong>一個事件記一個事實&lt;/strong>。&lt;/p>
&lt;h3 id="拆分訊號">拆分訊號&lt;/h3>
&lt;p>一個事件記了兩個獨立的事實 → 拆成兩個事件。&lt;/p>
&lt;p>&lt;code>terminal.connect_and_auth&lt;/code> 同時記錄「連線建立」和「認證通過」。這兩個事實的失敗模式不同（連線失敗是網路問題、認證失敗是帳密問題）、觸發時機不同、消費者不同。拆成 &lt;code>terminal.connect.done&lt;/code> 和 &lt;code>auth.token.sent&lt;/code>。&lt;/p>
&lt;h3 id="合併訊號">合併訊號&lt;/h3>
&lt;p>兩個事件永遠同時觸發且消費者相同 → 合併成一個事件。&lt;/p>
&lt;p>&lt;code>terminal.input.keystroke&lt;/code> 和 &lt;code>terminal.input.keystroke_logged&lt;/code> 永遠同時觸發（每個按鍵一次），data schema 相同。合併成一個 &lt;code>terminal.input.keystroke&lt;/code>。&lt;/p>
&lt;h3 id="邊界案例">邊界案例&lt;/h3>
&lt;p>&lt;code>connect.done&lt;/code> 同時記 event 和 metric（成功事件 + duration）。這是一個事實（連線完成）的兩個面向，可以合併成一個事件帶 &lt;code>duration_ms&lt;/code> 欄位，也可以拆成 event 和 metric 兩筆。判斷依據是查詢需求 — 如果 funnel 分析和效能分析會分開查，拆開讓各自的查詢更簡單；如果都在同一個 dashboard 看，合併減少事件量。&lt;/p>
&lt;h2 id="data-schema-設計">data schema 設計&lt;/h2>
&lt;p>每個事件的 data 欄位回答「發生了什麼的 context」。設計原則：&lt;/p></description><content:encoded><![CDATA[<p>事件枚舉的目的是為一個服務建立完整的事件清單 — 每個事件有明確的類型、名稱、觸發時機和 data schema。枚舉的方法從操作盤點出發，經過四類補齊檢查，產出可以直接實作 SDK 埋點的事件表。</p>
<h2 id="從操作盤點推導事件">從操作盤點推導事件</h2>
<p>每個使用者操作（BDD 操作盤點的產物）至少對應一個 event 類型的事件。操作的失敗路徑對應 error 類型。操作涉及的效能測量對應 metric 類型。操作觸發的系統狀態轉換對應 lifecycle 類型。</p>
<p>推導鏈：操作 → 四類事件候選 → 命名 → data schema。</p>
<p>以一個透過 WebSocket 連接遠端終端機的 app 為例，「連線到終端機」這個操作推導出的事件：</p>
<table>
  <thead>
      <tr>
          <th>四類</th>
          <th>事件名稱</th>
          <th>觸發時機</th>
          <th>data schema</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>event</td>
          <td>terminal.connect.start</td>
          <td>使用者點擊連線按鈕</td>
          <td><code>{url, trigger: &quot;manual&quot; | &quot;auto&quot;}</code></td>
      </tr>
      <tr>
          <td>event</td>
          <td>terminal.connect.done</td>
          <td>連線成功、開始接收 output</td>
          <td><code>{url, duration_ms}</code></td>
      </tr>
      <tr>
          <td>error</td>
          <td>terminal.connect.failed</td>
          <td>連線失敗（逾時、拒絕、認證失敗）</td>
          <td><code>{url, error, step}</code></td>
      </tr>
      <tr>
          <td>metric</td>
          <td>terminal.connect.duration</td>
          <td>連線完成（成功或失敗）</td>
          <td><code>{duration_ms, success: bool}</code></td>
      </tr>
      <tr>
          <td>lifecycle</td>
          <td>ws.connected</td>
          <td>WebSocket 連線狀態轉換</td>
          <td><code>{url}</code></td>
      </tr>
      <tr>
          <td>lifecycle</td>
          <td>ws.disconnected</td>
          <td>WebSocket 斷線</td>
          <td><code>{url, reason, code}</code></td>
      </tr>
  </tbody>
</table>
<p>一個操作推導出六個事件 — 因為這個操作跨越了使用者行為（event）、可能失敗（error）、有效能測量（metric）、涉及系統狀態轉換（lifecycle）四個面向。其中 <code>connect.done</code> 和 <code>connect.duration</code> 記錄的是同一事實的兩個面向（見下方邊界案例段），自用場景合併成 <code>connect.done</code> 帶 <code>duration_ms</code> 欄位更簡潔。</p>
<h2 id="四類補齊檢查">四類補齊檢查</h2>
<p>列完所有操作的事件後，對每個功能區域跑一次四類補齊檢查 — 逐列確認每一類是否都有對應的事件。</p>
<table>
  <thead>
      <tr>
          <th>功能區域</th>
          <th>event</th>
          <th>error</th>
          <th>metric</th>
          <th>lifecycle</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>連線</td>
          <td>connect.start / connect.done</td>
          <td>connect.failed</td>
          <td>connect.duration</td>
          <td>ws.connected / ws.disconnected</td>
      </tr>
      <tr>
          <td>認證</td>
          <td>auth.biometric.attempt</td>
          <td>auth.biometric.failed</td>
          <td>auth.duration</td>
          <td>auth.state_changed</td>
      </tr>
      <tr>
          <td>輸入</td>
          <td>input.submit</td>
          <td>input.parse_error</td>
          <td>—</td>
          <td>—</td>
      </tr>
      <tr>
          <td>配對</td>
          <td>enrollment.qr.scan / enrollment.done</td>
          <td>enrollment.failed</td>
          <td>enrollment.duration</td>
          <td>—</td>
      </tr>
  </tbody>
</table>
<p>空格是候選遺漏。每個空格問一個問題：</p>
<ul>
<li><strong>event 空</strong>：「這個功能區域有使用者操作嗎？」有 → 補事件；沒有（純系統內部）→ 合理的空格</li>
<li><strong>error 空</strong>：「這個功能區域能失敗嗎？」能 → 補事件；不能失敗的功能極少 → 再想一次</li>
<li><strong>metric 空</strong>：「這個功能區域有值得量測的效能指標嗎？」有 → 補事件；操作瞬間完成且不涉及外部依賴 → 合理的空格</li>
<li><strong>lifecycle 空</strong>：「這個功能區域涉及系統狀態轉換嗎？」有 → 補事件；純資料操作不改系統狀態 → 合理的空格</li>
</ul>
<p>上表中「輸入」的 metric 和 lifecycle 空格是合理的 — 文字輸入送出不涉及效能量測和系統狀態轉換。「配對」的 lifecycle 空格也合理 — 配對完成後不改變系統的執行狀態。</p>
<h2 id="粒度判準">粒度判準</h2>
<p>事件粒度的判斷用一個 SRP 判準：<strong>一個事件記一個事實</strong>。</p>
<h3 id="拆分訊號">拆分訊號</h3>
<p>一個事件記了兩個獨立的事實 → 拆成兩個事件。</p>
<p><code>terminal.connect_and_auth</code> 同時記錄「連線建立」和「認證通過」。這兩個事實的失敗模式不同（連線失敗是網路問題、認證失敗是帳密問題）、觸發時機不同、消費者不同。拆成 <code>terminal.connect.done</code> 和 <code>auth.token.sent</code>。</p>
<h3 id="合併訊號">合併訊號</h3>
<p>兩個事件永遠同時觸發且消費者相同 → 合併成一個事件。</p>
<p><code>terminal.input.keystroke</code> 和 <code>terminal.input.keystroke_logged</code> 永遠同時觸發（每個按鍵一次），data schema 相同。合併成一個 <code>terminal.input.keystroke</code>。</p>
<h3 id="邊界案例">邊界案例</h3>
<p><code>connect.done</code> 同時記 event 和 metric（成功事件 + duration）。這是一個事實（連線完成）的兩個面向，可以合併成一個事件帶 <code>duration_ms</code> 欄位，也可以拆成 event 和 metric 兩筆。判斷依據是查詢需求 — 如果 funnel 分析和效能分析會分開查，拆開讓各自的查詢更簡單；如果都在同一個 dashboard 看，合併減少事件量。</p>
<h2 id="data-schema-設計">data schema 設計</h2>
<p>每個事件的 data 欄位回答「發生了什麼的 context」。設計原則：</p>
<p><strong>帶足 debug context</strong>：error 事件的 data 至少包含 error message、發生的步驟、當時的關鍵狀態值。看到這筆 error 事件時、開發者不需要再去查其他來源就能判斷問題方向。</p>
<p><strong>避免過度收集</strong>：data 只帶回答具體問題需要的欄位。<code>terminal.connect.start</code> 帶 URL 和觸發方式就夠了；不需要帶使用者的全部設定。</p>
<p><strong>敏感欄位標記 redaction</strong>：URL 可能含 IP、error message 可能含路徑中的使用者名稱。在事件設計階段標記需要 <a href="/blog/monitoring/knowledge-cards/redaction/" data-link-title="Redaction" data-link-desc="說明在事件資料離開 client 之前把敏感欄位的值替換成遮罩或移除的機制">redaction</a> 的欄位，SDK 實作時自動處理。</p>
<h2 id="事件表的產出格式">事件表的產出格式</h2>
<p>完整的事件表每列七欄：</p>
<table>
  <thead>
      <tr>
          <th>事件名稱</th>
          <th>類型</th>
          <th>觸發時機</th>
          <th>data schema</th>
          <th>redaction 欄位</th>
          <th>保留層級</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>terminal.connect.start</td>
          <td>event</td>
          <td>使用者點擊連線</td>
          <td><code>{url, trigger}</code></td>
          <td>url</td>
          <td>原始 7d</td>
          <td>funnel 第一步</td>
      </tr>
  </tbody>
</table>
<p>保留層級欄對應分層保留策略 — 哪些事件需要保留原始逐筆資料（debug 用）、哪些只需要聚合摘要（趨勢用）。</p>
<p>事件表是 SDK 埋點的 spec — 開發者照表實作，code review 時逐行勾選。和<a href="/blog/testing/02-client-observability/log-point-in-spec/" data-link-title="功能規格中的 log 點定義方法" data-link-desc="把 log 點設計從 debug 階段前移到功能規格階段 — 每個功能的規格文件新增可觀測性欄位，列出啟動 / 步驟 / 錯誤 / 完成四類 log 點">功能規格中的 log 點定義</a>互補 — log 點是開發期的 debug 設計，事件表是監控期的收集設計。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>四類事件的定義 → <a href="/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">四類事件的完整定義</a></li>
<li>事件命名規範 → <a href="/blog/monitoring/01-mental-model/event-naming-convention/" data-link-title="事件命名規範" data-link-desc="namespace.action 格式的事件命名、命名一致性的工程價值、和商業方案命名慣例的對應">事件命名規範</a></li>
<li>行為事件的 funnel 設計 → <a href="/blog/monitoring/08-business-analytics/behavior-event-design/" data-link-title="行為事件設計" data-link-desc="事件命名規範、屬性設計、funnel 定義 — 行為分析的品質取決於事件設計的品質">行為事件設計</a></li>
<li>事件 schema 的欄位定義 → <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></li>
<li>動機驅動的具體事件對應 → <a href="/blog/monitoring/01-mental-model/motivation-to-event-mapping/" data-link-title="動機驅動的事件設計" data-link-desc="Debug / 商業 / 資安 / 效能四個動機各自需要什麼事件 — 從「為什麼收」反推「收什麼」和「什麼階段啟用」">動機驅動的事件設計</a></li>
</ul>
]]></content:encoded></item></channel></rss>