---
title: "Bugsink 隨產品出貨 — 需求討論稿 (FR-133)"
brand: "Guidant AI · **FR-133** 錯誤追蹤站隨產品出貨"
eyebrow: "FR-133 · 需求討論稿 · 2026-10-09（v2 · D1～D8 全部定案）"
h1: "客戶機器上就看得到程式錯誤"
subtitle: "Bugsink 打進安裝包，裝機全自動設好，後端與前端錯誤都收得到"
lede: "產品要進客戶 UAT（使用者驗收測試），客戶環境不受我們控制。出錯時要在**客戶自己的機器上**就看得到：錯在程式哪一行（堆疊）、當下變數值、誰踩的、哪一筆請求。決策者選定 **Bugsink**（單一容器、與 Sentry SDK 相容的錯誤追蹤站）**隨產品出貨**，裝機時自動建好帳號、專案與連線字串，**裝機人員不必開任何畫面、不必輸入任何東西**。本稿盤點現況四個缺口、記錄已定案的八項決策與連線面，並拆成六棒。"
chips: [
  {text: "D1～D8 與過濾開關全部定案", kind: ok},
  {text: "只用 443：子路徑看站、登入後轉送前端錯誤", kind: accent},
  {text: "B0 必測：子路徑、轉送、主機名", kind: warn},
  {text: "六棒：A／B0／B／C／D／E", kind: accent},
  {text: "跨 3 repo：BE／FE／jedi-log", kind: accent},
  {text: "陷阱：一次登入失敗在站台變三條", kind: crit}
]
footer: "FR-133 · Bugsink 隨產品出貨 — 需求討論稿 · 2026-10-09 v1 · 前作：FR-071（診斷包與錯誤收集站設計，[design.md](../FR-071-2609-support-diagnostic-bundle/design.md)）· 現況依據：`core/plugins/api_log.py`、jedi-log 1.3.3 `jedi_api_log/error_tracking/`、`jedi_common/handler/`、`infra/support/diag_event_fetcher.py`、`docker/production/docker-compose.yml`、`scripts/build/build_bundle.sh`、`scripts/installer/install.sh` 實檔查證；Bugsink 事實取自 2.6.1 官方文件與原始碼"
---

## 分工概述（30 秒版） {#overview nav="概述"}

整案一句話：**每套 Guidant AI 都自帶一個錯誤追蹤站，裝好就能用；後端與前端的程式錯誤自動送進去，業務性的「使用者打錯」不送。**

給非工程讀者的背景：程式出錯時，工程師要知道**錯在哪一行、當時變數是什麼值、是誰、在做什麼**。錯誤追蹤站（Error tracking，像一個專收錯誤報告的信箱網站）會把同一種錯誤歸成一條、附上這些資訊。後端已經會「寄信」，缺的是**每個客戶機器上要有一個收信的站**，以及「哪些該寄、哪些不該寄」的篩選。

::: {.callout .decided}
**六棒，A 與 B0 先跑、可平行**

A（過濾開關）與 B0（Bugsink 實測）互不相干，可同時開工。B 等 B0 的實測結果；C、D 等 B；E 收尾文件等全部。
:::

| 棒 | 做什麼 | 產出 | 完成怎麼判定（決策者檢查法） |
|---|---|---|---|
| **A 過濾開關** | 業務錯誤（打錯密碼、查無資料這類 4xx）預設不送站台；`logger.error` 那一行 log 預設不另開一條，只當附帶紀錄 | 兩個環境變數、jedi-log 1.3.4、後端釘新版 | DEV 開著站台，故意打錯密碼登入三次 → 站台**一條都不出現**；再故意打一支會 500 的 API → 站台**只出現一條**，不是三條 |
| **B0 可行性實跑** | 在 DEV 起 Bugsink 2.6.1，實測「用指令建專案 → 讀出連線字串 → 後端送得進去 → 用 API 讀得回來」整條，另外必測三件：站台能不能掛在 443 的子路徑、前端錯誤經後端轉送進不進得去、主機名從哪讀與改了會怎樣 | 一段可直接搬進 install.sh 的 shell 片段＋實測紀錄（含子路徑通或不通的結論） | 打開實測紀錄，清單九項每項都有實際輸出貼在上面；照片段在空白機器重跑一次，站台上看得到後端送進來的錯誤 |
| **B 進出貨包** | 安裝包多帶一顆 Bugsink；首次安裝自動建管理員、專案、連線字串；升級時缺了才補；維運指令加清理與改密碼 | compose、bundle、install.sh、guidantai 子命令 | 拿新包在乾淨機器裝好 → 瀏覽器開 `https://<主機>/bugsink/`（B0 子路徑測不通時改 `https://<主機>:8443`）用管理員登入，看得到 `guidant-ai` 專案 → 在產品故意打一支會 500 的 API → **一分鐘內站台出現該筆**，點進去有堆疊 |
| **C 前端錯誤接回** | 前端的程式錯誤經後端一支「登入後才能打」的轉送端點送進同一個站，帶版號與 commit，能跟後端那一筆串起來 | FE 接線、後端轉送端點、登入後端點多帶 DSN 一欄 | 登入產品後在瀏覽器故意觸發一個前端錯誤 → 站台出現該筆，版本欄有版號＋commit、有 request_id；**登出狀態下**打轉送端點回 401 |
| **D 診斷包改用 Bugsink API** | 診斷包撈錯誤事件改打 Bugsink 自己的 API | `diag_event_fetcher.py` 改寫 | 在客戶機器跑一次診斷包匯出 → 解開包，錯誤事件那一份有內容，與站台上看到的一致 |
| **E 文件** | 設定範本、部署文件、客戶手冊、release note | 四份文件 | 客戶手冊裡找得到「站台怎麼進、密碼在哪、事件保留多久、改設定後要用 start 不是 restart」四件事 |

依賴：A、B0 平行 → B → C、D 平行 → E。

---

## 需求背景 {#background nav="背景"}

### 後端已經會送，缺的是收的站

後端錯誤追蹤的接線已存在，v1.26.0 起 sentry-sdk（送錯誤報告的程式庫）隨包內建：

| 落點 | 現況 |
|---|---|
| 主專案 `core/plugins/api_log.py:234` | `init_error_tracking()` 讀環境變數 `SENTRY_DSN`（DSN＝連線字串，告訴程式錯誤要送去哪個站、哪個專案），交給 jedi-log 套件 |
| jedi-log 1.3.3 `jedi_api_log/error_tracking/__init__.py` | 真正初始化 SDK 的地方 |
| 三個後端容器 | api／socketio／worker 都經 `create_app` 走同一條，一次接好三個都生效 |
| 遮罩 `masking.py` | 送出前先過濾：憑證欄位遮掉、四支敏感 headers 整支拿掉、堆疊裡的區域變數也掃 |
| 節流 | 每分鐘 60 筆（`ERROR_TRACKING_RATE_LIMIT`） |

遮罩與節流都在「送出前」（before_send）做，**跟收的站是哪一套無關**——換成 Bugsink 不必重做。

### 現況四個缺口（皆經實檔查證）

::: {.callout .crit}
**🔴 缺口一：一次登入失敗，站台上變三條**

打錯一次密碼，站台會出現三條不同的 issue（issue＝站台上歸成一類的錯誤）：

1. jedi-iam `login_domain_service.py:143` 的 `logger.error`——SDK 的 logging 整合會把 error 等級的 log 當成獨立事件
2. `UnauthorizedError` 這個例外本身
3. jedi-common `handler.py:62` 錯誤處理器再 `logger.error` 一次

不只登入：`jedi_common/handler/exception.py` 的 `ClientError` 家族八種（BadRequest／Unauthorized／Forbidden／NotFound／MethodNotAllowed／Conflict／PreconditionFailed／TooManyRequests）全部會進站台。**UAT 時使用者的正常操作失誤會淹掉真正的程式錯誤，還會吃光每分鐘 60 筆的節流額度**——真的 500 反而送不進去。
:::

::: {.callout .crit}
**🔴 缺口二：前端錯誤回報已整個拆掉**

2026-10-05 資安項 U3-⑤（CM-2495，FE `7e136266`、BE `4b3a04334`）拆掉了前端的 `@sentry/vue`。原因：連線字串由免登入端點 `/client-config` 供給，另有免登入的 `/error-tunnel` 轉送器，兩者被判為**任何人不用登入就能打的攻擊面**。守衛測試 `test/security/code/test_version_module_no_error_tracking_endpoints.py` 擋這兩支復活。

所以前端接回時**不能走原路**，見 D4。FR-071 設計稿（`design.md` 第 247、341、650 行附近）有「前端 release 帶版號＋commit、用 trace 標頭串前後端」的做法可沿用。
:::

::: {.callout .warn}
**缺口三：診斷包撈事件的 API 對不上**

`infra/support/diag_event_fetcher.py` 是對著 GlitchTip 的 Sentry 式 API（`/api/0/organizations/<org>/issues/`）寫的。Bugsink 的讀取 API 不是這個形狀（見下節），要改寫。
:::

::: {.callout .warn}
**缺口四：改了設定用 restart 不會生效**

POC（189）實例：在 `guidant.env` 加了 `SENTRY_DSN` 後跑 `guidantai restart`，不生效。原因是 restart 底層是 `docker compose restart`，**只重啟容器、不重讀設定檔**；要用 `guidantai start`（`up -d`，會重建容器）。`FR-132-STATE.md` 第 19 行與 release note v1.26.0 §2b 的寫法要修正。
:::

---

## Bugsink 是什麼、已知事實 {#bugsink nav="Bugsink"}

| 面向 | 事實 |
|---|---|
| 版本與大小 | image `bugsink/bugsink:2.6.1`（2026-09-25），amd64 壓縮約 78 MB，多架構 |
| 組成 | **單一容器**，自帶背景處理程式（monofy 同時跑網站 gunicorn 與背景 snappea），不需要 Redis |
| 執行身分 | 基底 python:3.12-slim，uid 14237，資料目錄 `/data` |
| 資料庫 | `DATABASE_URL=postgresql://...`，Postgres ≥ 14 |
| 佇列 | snappea 的工作佇列是 SQLite，寫死在 `/tmp`——**容器重啟會丟掉還沒處理完的事件** |
| 必填設定 | `SECRET_KEY`（≥ 50 字）、`BASE_URL`（決定 DSN 長相，不可有結尾斜線） |
| 反向代理 | 要送 `Host`／`X-Forwarded-Proto`／`X-Real-IP` 三個 header，設 `BEHIND_HTTPS_PROXY=True`；上傳上限 `client_max_body_size` ≥ 20M |
| 管理員 | `CREATE_SUPERUSER=email:password`，只在資料庫裡**還沒有任何使用者**時建 |
| 專案與 token | **沒有**建專案的指令或環境變數（官方 issue #240 開著無回應）；API token 官方只能從畫面建 |
| DSN 組成 | `build_dsn(BASE_URL, project.id, sentry_key.hex)`；`sentry_key` 是建專案時產的隨機 UUID，可從資料庫讀出 |
| 註冊 | `USER_REGISTRATION=CB_NOBODY` 關閉自行註冊；`SINGLE_USER=True` 為單人模式 |
| 保留 | 每專案預設 1 萬筆（smart retention，依重要性挑留）；`MAX_EVENT_AGE_DAYS` 需另排程跑 `bugsink-manage vacuum` |
| 速率限制 | 預設 5 分鐘 1000 筆 |
| 讀取 API | `/api/canonical/0/`：`issues/?project=`、`events/?issue=`、`events/{id}/stacktrace/`；Bearer token；2.6.0 起 token 帶權限（`issues:read`／`events:read`）。Sentry 相容的只有「收事件」那一面 |
| 收事件 | `/api/<project_id>/envelope/`，相容 sentry-sdk 2.x；跨網域（CORS）只知 1.3.0 起有支援，細節查不到 |
| 告警 | 觸發：新 issue／舊錯誤復發／取消靜音；管道：Email（SMTP 變數）、Slack／Teams／Telegram／自訂 webhook；`ALERTS_WEBHOOK_DENY_NON_GLOBAL` 預設擋內網 webhook |
| 升級 | 容器啟動時自動跑資料庫遷移，換 tag 即可；健康檢查 `/health/ready`，image 內建 HEALTHCHECK 寫死 8000 埠 |
| 授權 | PolyForm Shield 1.0.0：可商用散布，**不得拿去做與 Bugsink 競爭的產品** |

---

## 已定案（決策者 2026-10-09） {#decided nav="已定案"}

::: {.callout .decided}
**以下全部已拍板，本段為記錄，本案沒有待決策項。**

共同原則：客戶防火牆常只開 443，所以**客戶需要連的東西一律走 443**，不要求客戶多開 port。
:::

| # | 決策 | 內容 | 理由 |
|---|---|---|---|
| **D1** | 自動建專案的做法 | 用 `bugsink-manage shell` 跑 Django ORM（Bugsink 內部的資料存取層）建 team／project／API token；DSN 用 SQL 從資料庫讀出，寫進 `guidant.env` 的 `SENTRY_DSN` | 官方沒有建專案的指令。跟著 Bugsink 自己的資料模型走，比直接寫 SQL 插資料穩，版本升級時欄位異動由它自己吸收 |
| **D2** | 站台位址 | **優先掛在 443 的子路徑** `https://<客戶主機>/bugsink/`（nginx 轉給容器）；**備案**才對外開 8443（`BASE_URL=https://<客戶主機>:8443`），兩者都沿用 stack 既有 HTTPS 憑證。Bugsink 的 `BASE_URL` 會推導 `FORCE_SCRIPT_NAME`（Django 用來支援子路徑的設定），代表有子路徑支援但未實證——**B0 必測**，測通就不開 8443，測不通才退 8443 並在手冊寫明要客戶開 port | 客戶不必多開 port；8443 留作退路，不讓子路徑的不確定性卡住整案 |
| **D3** | 授權 | PolyForm Shield 可接受，不過法務 | 本產品是合規管理系統，不是錯誤追蹤產品，不構成競爭 |
| **D4** | 前端錯誤怎麼送 | **一律走後端轉送**，不做瀏覽器直送。FE `@sentry/vue` 設 `tunnel`（SDK 的轉送選項）指向後端一支**登入後才能打**的端點，後端原樣轉給 Bugsink 容器（docker 內網直連）。DSN 由登入後既有端點的回應多帶一欄取得。不復活免登入的 `/error-tunnel`／`/client-config`，守衛測試照擋 | 轉送只依賴 443 與登入狀態，不必驗跨網域（CORS）。代價是**登入前的前端錯誤收不到**，決策者接受 |
| **D5** | 管理員帳密 | 帳號固定 `admin@guidantai.com`；密碼**所有客戶同一組**，**build 時烙進安裝包**（CI 的遮罩變數 → 包內一個權限 600 的檔），install.sh 讀它建管理員，並另存一份到 `guidant.env` 同目錄的檔案。裝機人員不輸入任何東西 | 裝機人員不具備操作能力；原廠維護時要能直接登入。決策者明確排除「每台隨機產生」與「install.conf 自帶」兩案 |
| **D6** | 資料庫落點 | stack 內 `guidant-db` 另開 `bugsink` 庫與獨立帳號，不用 SQLite | 不多起一顆 Postgres；獨立庫與帳號在權限上與產品資料互不相通；官方不建議 SQLite 放在 volume |
| **D7** | 事件保留 | 沿用預設每專案 1 萬筆（挑重要的留），不設天數上限；`guidantai bugsink-vacuum` 子命令手動清 | UAT 期間錯誤的價值在「是不是一直發生」，砍天數反而看不出趨勢；筆數上限已把磁碟用量封頂 |
| **D8** | 告警 | 第一版不做，靠人登入看站台；Email／webhook 列為下一版 | 第一版目標是「裝得起來、收得到」；告警牽涉客戶 SMTP 有沒有設、寄給誰，且客戶內網多半連不到 Slack／Teams |
| **過濾** | 送不送的開關 | 做成環境變數，**預設只收程式錯誤**、可開回全收（細節見拆分表棒 A） | 解掉缺口一；保留全收是為了某些客戶要查「為什麼一直 403」時能臨時打開 |

::: {.callout .warn}
**D5 的已知風險（如實記錄，決策已定）**

同一組密碼烙在每一份安裝包裡。**任何一台客戶機器的安裝目錄外洩，所有客戶的錯誤追蹤站都能用這組登入**——站上有堆疊與變數值（已遮罩，但不是零業務資料）。緩解靠兩件事：站台入口只在客戶內網可達、維運指令提供改密碼子命令（棒 B），客戶或原廠可在單台上改掉。
:::

**明確不做**：原廠集中收錯誤（本案是每台客戶機器自架，用不到）、讓客戶自訂 DSN 的畫面、站台設定畫面。

---

## 連線面 {#connections nav="連線面"}

誰連誰、走哪條路。客戶需要放行的只有 443。

| 誰 | 連到哪 | 走哪條路 | 經不經客戶防火牆 |
|---|---|---|---|
| 後端三容器（api／socketio／worker） | Bugsink 收事件端點 | docker 內網直連容器（`http://guidant-bugsink:8000`），DSN 指向內網位址 | 不經 |
| 客戶瀏覽器（已登入） | 後端轉送端點 → Bugsink | 443 → 後端（驗登入）→ docker 內網 → Bugsink | 只經 443 |
| 客戶瀏覽器（未登入） | — | 轉送端點回 401，錯誤不收 | — |
| 原廠人員看站台 | Bugsink 網頁介面 | 443 子路徑 `https://<主機>/bugsink/`；備案 8443 | 只經 443（備案時多一個 8443） |
| 診斷包匯出 | Bugsink 讀取 API | docker 內網直連，帶 install.sh 建好的 token | 不經 |

::: {.callout .warn}
**陷阱：DSN 有兩個位址角色**

Bugsink 用 `BASE_URL` 組 DSN，而 `BASE_URL` 是給瀏覽器看站台用的外部位址（子路徑或 8443）。後端送事件要走內網，不能用這個外部位址繞出去再繞回來。B0 要實測「`BASE_URL` 設外部位址、後端 DSN 改寫成內網位址」送不送得進去；送不進去就要在後端初始化時另外處理。
:::

## 端到端流程 {#flow nav="流程"}

### 裝機時：全自動建好站台並接上後端

```{.mermaid cap="圖 1 — 首次安裝時序：先起 Bugsink、建專案、讀出 DSN，最後才起後端"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant I as install.sh
    participant DB as guidant-db（Postgres）
    participant BS as guidant-bugsink
    participant ENV as guidant.env
    participant BE as 後端三容器

    I->>I: 讀安裝包內烙好的管理員密碼檔
    I->>DB: 建 bugsink 庫與帳號（D6）
    I->>BS: compose up guidant-bugsink（CREATE_SUPERUSER）
    BS->>DB: 自動遷移資料表、建管理員
    I->>BS: 等 /health/ready 回應
    I->>BS: bugsink-manage shell 建 team／project／API token
    I->>DB: SQL 讀出 project.id 與 sentry_key
    I->>ENV: 組好 DSN 寫入 SENTRY_DSN
    I->>BE: compose up 後端
    BE->>BS: 開始送錯誤事件
```

升級（`--upgrade`）時照產品既有慣例：**設定檔不重寫，缺了才補**（範例 `install.sh` 第 2734 行 `ensure_classifier_env()`）。舊客戶第一次升到含 Bugsink 的版本時，走的就是上圖整條；已經有的不動。

### 執行期：錯誤從哪裡送、誰來讀

```{.mermaid cap="圖 2 — 執行期事件流：後端內網直連；瀏覽器只走 443，登入後經後端轉送"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  BR["瀏覽器（已登入）<br/>@sentry/vue tunnel"] -->|"443"| NG["nginx<br/>443 入口"]
  OP["原廠人員"] -->|"443 /bugsink/<br/>（備案 8443）"| NG
  NG -->|"轉送端點"| API
  NG -->|"/bugsink/ 子路徑"| BS
  subgraph BEG["後端三容器"]
    API["api<br/>（含驗登入的轉送端點）"]
    SIO["socketio"]
    WK["worker"]
  end
  API -->|"內網直連<br/>過濾→遮罩→節流"| BS["guidant-bugsink<br/>:8000"]
  SIO -->|"內網直連"| BS
  WK -->|"內網直連"| BS
  BS --> PG[("guidant-db<br/>bugsink 庫")]
  DIAG["診斷包匯出"] -->|"canonical API＋token"| BS
```

---

## 現況接入點盤點 {#inventory nav="現況盤點"}

| 元件 | 座標 | 現況 | 本案動作 |
|---|---|---|---|
| 後端初始化 | `core/plugins/api_log.py:234` | 讀 `SENTRY_DSN` 交 jedi-log | `_extra_before_send` 加業務錯誤過濾（棒 A） |
| jedi-log | `jedi_api_log/error_tracking/__init__.py`（1.3.3） | `LoggingIntegration` 用預設，error log 變事件 | 多一個參數餵 `event_level`，發 1.3.4（棒 A） |
| 例外家族 | `jedi_common/handler/exception.py` | `ClientError` 八種 | 只讀，作為過濾判斷依據 |
| 出貨包 | `scripts/build/build_bundle.sh` 第 98–117 行 | postgres／seaweedfs 第三方 image 釘 digest | 照抄多包一顆 bugsink（棒 B） |
| stack | `docker/production/docker-compose.yml` 第 457 行 | 只開 80／443 | 加 `guidant-bugsink` 服務；nginx 加 `/bugsink/` 子路徑（備案才開 8443）（棒 B） |
| 安裝器 | `scripts/installer/install.sh` | 產 `.env`（compose 用）與 `guidant.env`（後端用）；升級缺了才補 | 首次建站、升級補站（棒 B） |
| build 期烙印 | `scripts/gen_version_bake.py` | CI 產常數檔、不入版控 | 管理員密碼檔照這個模式（棒 B） |
| 前端 | FE `src/main.js` | `@sentry/vue` 已拆 | 復原，`tunnel` 指向登入後轉送端點，DSN 從登入後端點取（棒 C） |
| 轉送端點 | 後端新增 | 無 | 登入後才能打，原樣轉給內網 Bugsink；只收自家專案的事件，套節流（棒 C） |
| 守衛測試 | `test/security/code/test_version_module_no_error_tracking_endpoints.py` | 擋免登入端點復活 | 保留不動，棒 C 必須通過它 |
| 診斷包 | `infra/support/diag_event_fetcher.py` | 打 GlitchTip 式 API | 改打 canonical API（棒 D） |
| 文件 | `FR-132-STATE.md` 第 19 行、release note v1.26.0 §2b | 寫 `restart` | 改 `start`（棒 B 順手） |

---

## 拆分表 {#breakdown nav="拆分"}

| 棒 | 做什麼 | repo | 依賴 | 完成怎麼判定（決策者檢查法） |
|---|---|---|---|---|
| **A 過濾開關** | ① 新增 `ERROR_TRACKING_CAPTURE_CLIENT_ERRORS`（預設 false：八種 `ClientError` 不送），在後端 `_extra_before_send` 從 hint 取例外類別判斷；② 新增 `ERROR_TRACKING_LOG_EVENTS`（預設 false：`logger.error` 只當 breadcrumb，即附在事件上的前情紀錄，不另開一條），jedi-log `init_error_tracking` 多一個參數餵 `LoggingIntegration(event_level=...)`，發 1.3.4；後端釘新版；**新增前先 grep 確認沒有同義變數**；`.env.sample` 與 `deployment-env.md` 同步補一行 | jedi-log → BE | 無 | 打錯密碼三次站台零條；故意 500 一次站台一條；把兩個變數改成 true 重來，打錯密碼又會出現 |
| **B0 Bugsink 實跑** | DEV 起 `bugsink:2.6.1`＋stack 的 Postgres（`bugsink` 庫＋獨立帳號），實測清單九項：① shell 建 team／project／token ② SQL 讀 DSN ③ sentry-sdk 2.71 送進去 ④ canonical API 讀回 issue ⑤ 重啟容器後資料仍在 ⑥ **443 子路徑**：`BASE_URL=https://<host>/bugsink` 下網頁可登入、各頁連結與靜態檔不跑掉 ⑦ **轉送**：SDK `tunnel` 經一支假的登入後端點轉給內網 Bugsink，站台收到事件 ⑧ **內外位址**：DSN 改內網位址後後端仍送得進去 ⑨ **主機名**：升級模式（設定檔已存在）下 install.sh 從哪讀得到主機名；裝完改主機名後 DSN 與站台各會怎樣。產出可直接進 install.sh 的 shell 片段，並給出子路徑通／不通的結論 | BE（scripts 草稿） | 無 | 實測紀錄九項皆附實際輸出；第 ⑥ 項明寫「通」或「不通，退 8443」；照片段在空白環境重跑一次能重現 |
| **B 進 stack** | compose 加 `guidant-bugsink`（healthcheck、環境變數）；nginx 加 `/bugsink/` 子路徑（B0 第 ⑥ 項不通才改開 8443）；`guidant-db` 建 `bugsink` 庫與帳號；bundle 多包一顆釘 digest；install.sh 首次建管理員／專案、寫 DSN；`--upgrade` 缺了才補；build 期烙印管理員密碼檔；`guidantai` 子命令（`bugsink-vacuum`、改密碼）；修 FR-132-STATE 與 release note 的 restart→start | BE | B0 | 新包乾淨機器裝好，`https://<主機>/bugsink/`（或備案 8443）管理員登入看得到 `guidant-ai` 專案；故意 500 一分鐘內出現；**舊版升級**上來同樣成立且產品資料完好 |
| **C 前端接回** | `@sentry/vue` 復原，設 `tunnel` 指向後端新增的轉送端點（登入後才能打、原樣轉給內網 Bugsink、只收自家專案事件、套節流）；DSN 由登入後既有端點回應多帶一欄；release 帶版號＋commit；trace 標頭串前後端 | FE＋BE | B | tunnel 經登入後端點轉送，站台收到帶 release 與 request_id 的前端事件；未登入打該端點回 401；守衛測試仍綠 |
| **D 診斷包** | `diag_event_fetcher.py` 改打 canonical API；token 讀 install.sh 建好的那把 | BE | B0 | 匯出的診斷包內錯誤事件與站台一致；站台關掉時包內註明未取得而非整包失敗 |
| **E 文件** | `.env.sample`、`deployment-env.md`、客戶手冊（站台入口、密碼存放位置、保留規則、改設定用 start）、release note | BE | A–D | 手冊四件事都找得到；release note 寫明站台入口（子路徑或 8443） |

```{.mermaid cap="圖 3 — 棒次依賴"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  A["A 過濾開關<br/>jedi-log→BE"] --> E["E 文件"]
  B0["B0 Bugsink 實跑"] --> B["B 進出貨包"]
  B0 --> D["D 診斷包改 API"]
  B --> C["C 前端接回<br/>（登入後轉送）"]
  B --> E
  C --> E
  D --> E
```

---

## 風險與陷阱 {#risks nav="風險"}

::: {.callout .crit}
**🔴 改了 guidant.env 用 restart 不會生效**

`guidantai restart` 只重啟容器不重讀設定檔。install.sh 寫完 DSN 後必須走 `up -d`；手冊與 release note 一律寫 `guidantai start`。這條沒寫清楚，客戶會以為站台壞了。
:::

::: {.callout .warn}
**容器重啟會丟掉還沒處理的事件**

snappea 佇列寫死在容器內 `/tmp`。升級或重啟的那幾秒內進來的事件會遺失。UAT 情境可接受，但手冊要寫，免得被當成漏收。
:::

::: {.callout .warn}
**`CREATE_SUPERUSER` 只在零使用者時生效**

升級或第二次跑安裝器時它不會改密碼。所以「改密碼」必須另做維運子命令（棒 B），不能靠改環境變數重跑。
:::

::: {.callout .warn}
**image 內建健康檢查寫死 8000 埠**

容器內監聽埠要維持 8000，對外入口（子路徑或 8443）一律由 nginx 轉。改容器內埠會讓 image 自帶的 HEALTHCHECK 永遠失敗。
:::

::: {.callout .warn}
**主機名變了，站台入口與 DSN 會悄悄失效**

`BASE_URL` 含客戶主機名。客戶裝完後改主機名，站台連結會指錯；若 DSN 也是由它組出來，事件送不進去且後端不報錯。B0 第 ⑨ 項要實測影響範圍；手冊要寫「改主機名後跑哪支指令重產」。
:::

::: {.callout .warn}
**升級模式讀不讀得到主機名，尚未實證**

舊客戶第一次升到含 Bugsink 的版本時，install.sh 走「設定檔已存在、缺了才補」，不會再問主機名。要從既有設定的哪一欄讀到它，B0 第 ⑨ 項要查清楚；讀不到就要另訂來源，不能在升級時停下來問裝機人員。
:::

::: {.callout .warn}
**轉送端點不可變成開放轉發器**

後端轉送端點登入後就能打，若原樣轉發任何內容，等於讓已登入使用者能往內網 Bugsink 灌任意資料。端點只接受自家專案 DSN 的事件、大小設上限、套節流。
:::

::: {.callout .warn}
**過濾開關關掉後，403 類問題在站台上看不到**

預設不收業務錯誤，代表「權限設定錯導致一直 403」這種問題要回頭看存取紀錄或臨時把開關打開。這是刻意取捨，手冊要寫開關怎麼開。
:::

---

## 與其他需求的關係 {#relations nav="關聯"}

| FR | 關係 |
|---|---|
| [FR-071](../FR-071-2609-support-diagnostic-bundle/) | 前作：錯誤追蹤接線、遮罩、診斷包都在那裡建立；本案把收的站換成 Bugsink、改接診斷包 API，前端 release／trace 做法沿用其 design.md |
| [FR-132](../FR-132-2610-access-management/) | v1.26.0 上版文件裡的 restart 寫法由棒 B 順手修正 |
| FR-126 資安總報告 | 前端免登入端點（U3-⑤）已拆，棒 C 必須在守衛測試下接回 |
