跳轉到

Server Metrics 連線人數 Integration Guide — Frontend

Backend branch: feat/connected-user-metricscortex/dev,語意修正 fixed/connected-counts-monotonicity(#1242) API base: SIT(admin Bearer token 必要) 生效時間:merge 後 cortex-api --reload 立即生效,無 migration、無需重啟 scheduler

Consumer:cortex-management「系統監控 / 伺服器狀態」頁(同一支端點的既有下鑽契約見 server-metrics-access-log-drilldown.md)。


1. 欄位改名對照表(Breaking)

無。本次是純 additive,既有欄位的名稱、型別、語意一律未動。

2. 新欄位(additive)

Endpoint 新欄位 型別 語意
GET /api/admin/server-metrics users.connected_members_in_range number 查詢區間內留下請求紀錄的 distinct 登入會員數
GET /api/admin/server-metrics users.connected_anonymous_ips_in_range number 查詢區間內未登入請求的 distinct client IP 數
GET /api/admin/server-metrics users.connected_total_in_range number 上述兩者相加。人數的上界,非精確人數,見 §5

三個欄位都必定存在(非 optional、不會是 null),無資料時為 0

與既有 users.* 欄位的關係

既有欄位全部源自 refresh_tokens,結構上看不到未登入者。新欄位源自 api_access_log,兩組數字不可互相推導、也不該相加

資料來源 時間語意 涵蓋範圍
online_now / valid_access_token_sessions refresh_tokens 呼叫當下 僅登入者,且 members.status = 'active'
logged_in_today / _this_week / _this_month / _in_range refresh_tokens.created_at 對應期間 僅登入者
connected_*_in_range api_access_log query_range(上界截到 generated_at 登入 + 未登入,不檢查會員現況

3. 行為改變(Status code / shape)

情境 修前 修後
任何成功查詢 200users 有 6 個 key 200users 有 9 個 key(多 3 個,既有 6 個不變)
from_date > to_date 422 422(不變)
區間超過 31 天 422 422(不變)
非 admin 403 403(不變)

沒有任何 status code 改變。前端若用嚴格模式解析(例如 zod.strict()),新欄位會讓舊 schema 驗證失敗——這是唯一需要動的地方。

4. 六情境 response(connected_* 三欄由 committed 測試斷言)

以下三個 connected_* 欄位的值不是手寫、也不是一次性截圖,而是由 investment-agent/backend/tests/integration/test_admin_server_metrics_connections_pg.py::test_connection_counts_match_seeded_rows_in_each_window 對真實 PostgreSQL 逐項 assert 的值。查詢語意若改變,該測試會紅,這份文件不會靜默過期。

其餘六個 token-based 欄位未被斷言——它們在這個 scratch DB 恆為 0,因為該庫沒有 refresh_tokens 資料。那是環境結果,不是新行為。

fixture 落在 2001 年(api_access_log 由 migration 051 於 2026 建立,真實列不可能落在該區間),匿名位址用 RFC 5737 保留段。

fixture 內容(day 1 = 2001-01-01,day 2 = 01-02,day 3 = 01-03):

  • 會員 A(day 1 兩個位址 203.0.113.1/.2)、會員 B(day 1 203.0.113.3)、會員 C(day 1 與 day 2 皆 203.0.113.4
  • 匿名 192.0.2.10(day 1 兩筆)、192.0.2.11(day 1)、192.0.2.30(day 3)
  • 203.0.113.7:day 1 同時有 B 的登入請求與一筆匿名請求(重疊位址
  • 198.51.100.5:day 1 匿名、day 2 由 A 登入(漂移位址
  • 一筆 client_ip IS NULL 的匿名請求
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
  "$API_BASE/api/admin/server-metrics?from_date=2001-01-01&to_date=2001-01-01"

4-1 登入與未登入都有,且含重疊位址(happy path)

from_date=2001-01-01&to_date=2001-01-01

{
  "online_now": 0,
  "valid_access_token_sessions": 0,
  "logged_in_today": 0,
  "logged_in_this_week": 0,
  "logged_in_this_month": 0,
  "logged_in_in_range": 0,
  "connected_members_in_range": 3,
  "connected_anonymous_ips_in_range": 4,
  "connected_total_in_range": 7
}

匿名 4 = 192.0.2.10(兩筆收斂成 1)、192.0.2.11203.0.113.7198.51.100.5client_ip IS NULL 那筆不算。

203.0.113.7 同時被算進兩半——會員 B 當天從該位址登入過。這是刻意的:見 §5。

4-2 放大區間(單調性)

from_date=2001-01-01&to_date=2001-01-02

{
  "connected_members_in_range": 3,
  "connected_anonymous_ips_in_range": 4,
  "connected_total_in_range": 7
}

day 2 沒有帶進任何 day 1 沒有的呼叫端,所以三個數字都持平;198.51.100.5 仍然計入匿名半邊,即使它在 day 2 帶著登入身分出現。舊版在這裡會把匿名從 3 降到 2、total 從 6 降到 5(實測值)。放大區間絕不會讓任何一個數字下降test_widening_the_range_never_lowers_a_count 釘住)。

4-3 只有登入流量

from_date=2001-01-02&to_date=2001-01-02

{
  "connected_members_in_range": 2,
  "connected_anonymous_ips_in_range": 0,
  "connected_total_in_range": 2
}

4-4 只有未登入流量

from_date=2001-01-03&to_date=2001-01-03

{
  "connected_members_in_range": 0,
  "connected_anonymous_ips_in_range": 1,
  "connected_total_in_range": 1
}

4-5 區間內無任何流量

from_date=2001-01-04&to_date=2001-01-04

{
  "connected_members_in_range": 0,
  "connected_anonymous_ips_in_range": 0,
  "connected_total_in_range": 0
}

無資料是 200 + 全零,不是 404

4-6 錯誤情境:起日晚於訖日

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
  "$API_BASE/api/admin/server-metrics?from_date=2001-02-01&to_date=2001-01-01"
{
  "detail": "from_date must be on or before to_date"
}

HTTP 422。錯誤 shape 與既有行為一致。註:只有 status code 被測試釘住(tests/test_admin_server_metrics_route.py),detail 字串目前無測試斷言,可能隨實作調整。

5. 這些數字是上界,不是精確人數

兩半可能重疊。public 端點無 token 即可呼叫,已登入會員的呼叫端若在這些請求上省略 Authorization header,同一人會同時出現在兩半(見 §4-1 的 203.0.113.7)。

早期版本曾用 anti-join 扣除重疊,已於 #1242 移除,原因有二:

  1. 它讓聚合非單調。 anti-join 只看得到查詢區間,所以某位址在區間頭幾天匿名、後幾天登入,放大區間會讓它從匿名半邊消失——超集區間報出比子集更小的數字,逐日長條圖與週月彙總互相矛盾。
  2. 它只在同一 IP 成立。 同一人換網路(4G ↔ Wi-Fi、多裝置)仍然被算成兩個,去重根本沒接到最常見的情況。

以壞掉的聚合換取部分去重不划算,所以現在保留重疊並如實標示。前端不需要、也無法自行修正這個重疊——後端沒有匿名身分可供比對。

6. Consumer 端 type diff

 export interface ServerMetricsUsers {
   online_now: number;
   valid_access_token_sessions: number;
   logged_in_today: number;
   logged_in_this_week: number;
   logged_in_this_month: number;
   logged_in_in_range: number;
+  /** 區間內留下請求紀錄的 distinct 登入會員數。 */
+  connected_members_in_range: number;
+  /** 區間內未登入請求的 distinct client IP 數。
+   *  單位是 IP 不是人:同一 NAT 後方多人算 1,同一人換網路算 2。
+   *  可能與 connected_members_in_range 重疊,見 §5。 */
+  connected_anonymous_ips_in_range: number;
+  /** 兩者相加。人數的上界,不是精確人數。 */
+  connected_total_in_range: number;
 }

7. UI 呈現注意事項

  • 不要標成「訪客人數」connected_anonymous_ips_in_range 是 IP 數,欄位名帶 ips 就是為了不在名字上失真。建議標成「未登入連線端(以 IP 計)」並附 tooltip。
  • connected_total_in_range 混合單位:人 + 位址。一位會員用三台裝置貢獻 1,一位匿名者換三個網路貢獻 3,所以登入比例改變時,總數的移動方向取決於呼叫端組成:同一人原本用 N 個位址、登入後收斂成 1,總數下降;但 NAT 後方多人共用一個位址時,其中一人登入會把原本合併的 1 拆成「1 位會員 + 該位址其餘的人」而使總數上升。不可當趨勢指標。
  • 不要和 online_now 並列成同一組指標。前者是區間累計、後者是當下快照,並排會讓人誤以為可比較。
  • 逐日數字不可相加COUNT(DISTINCT) 本質不可加,七天各自查詢的和不低於一次七天區間查詢的結果(各日呼叫端完全不重疊時才相等)。要「本週不重複」就直接查該區間。
  • connected_* 不做「現在在線」判定。若 UI 需要「目前同時在線」,那仍然只有 online_now(且僅涵蓋登入者)。

8. 已知限制(需在 UI 或文件向管理員說明)

  • logging.access_log.exclude_paths/health//docs/openapi.json/redoc)不寫入 access log,只造訪這些路徑的呼叫端不會被計入。
  • 保留天數依 DEPLOY_ENV 而異:dev 7 天、SIT/UAT 14 天、prod 180 天。在 SIT 上只能回溯兩週,超出區間會回全零而非錯誤——UI 應避免讓管理員以為「兩週前真的沒人來」。
  • client_ip 的真實性依部署而定:SIT 經 FORWARDED_ALLOW_IPS 解析 Cloudflare Tunnel 的 X-Forwarded-For,記到真實客戶端 IP;本機 dev compose 未設該變數,所有請求塌成同一個 docker gateway 位址,匿名計數在 dev 無參考價值。
  • 刪除會員會改變歷史數字api_access_log.member_idON DELETE SET NULL,該員過去的請求會脫離 member 半邊。其中 client_ip 非 NULL 的列改由匿名半邊計(若該位址尚未被計過),client_ip IS NULL 的列則兩半都不計,因此總數可能淨減。同一組查詢參數在刪除前後會得到不同結果。停權(status 改變)則不影響。

9. Consumer 遷移 checklist

  • [ ] grep ServerMetricsUsers(或等價的 response type 定義),依 §6 補上三個欄位
  • [ ] 若使用 zod / io-ts 等執行期驗證且為 strict 模式,更新 schema 否則新欄位會驗證失敗
  • [ ] 「系統監控 / 伺服器狀態」頁加上連線人數區塊,文案依 §7 避免把 IP 數說成人數
  • [ ] 為 SIT 的 14 天保留期加上區間提示
  • [ ] 確認既有 online_now 的顯示位置與新指標視覺上分開(快照 vs 區間累計)
  • [ ] 若要做趨勢圖,確認未把逐日數字相加當成期間總計(§7)

10. 參考

  • 後端規格:docs/api-reference.md §26B「連線人數定義」
  • OpenAPI:docs/assets/openapi.jsonServerMetricsResponseMetricsUsers
  • 數值來源與真實 DB 行為測試:investment-agent/backend/tests/integration/test_admin_server_metrics_connections_pg.py
  • 去重移除的裁決:issue #1242