feat(api): Sentry Session Replay UX 自動監控

Phase 19 UX 監控 - 善用 Sentry Session Replay:
- SentryService: 新增 list_replays, get_ux_audit_summary
- 偵測: 憤怒點擊 (Rage Clicks) + 死亡點擊 (Dead Clicks)
- 偵測: 有錯誤的 Session Replay
- 偵測: UI 相關錯誤 (TypeError/render)
- API: GET /api/v1/errors/ux-audit 端點
- 腳本: audit_ux_sentry.py CLI 工具

統帥回饋: "AI都要全自動化!" 

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-29 01:48:59 +08:00
parent 8fa99209c3
commit d9a6f9d066
3 changed files with 343 additions and 0 deletions

View File

@@ -188,6 +188,157 @@ class SentryService:
return await self._request(f"issues/{issue_id}/events/", params=params)
# =========================================================================
# Session Replay APIs (2026-03-29 Phase 19 UX 監控)
# =========================================================================
async def list_replays(
self,
project: str | None = None,
query: str = "",
limit: int = 25,
sort: str = "-started_at",
) -> list[dict] | None:
"""
列出 Session Replay 錄製
Args:
project: 專案 slug
query: Sentry Discover 語法 (如 user.email:xxx)
limit: 每頁數量
sort: 排序欄位
Returns:
Replay 列表,包含 id, urls, duration, error_count 等
"""
project_slug = project or self.project
params: dict[str, Any] = {"limit": limit, "sort": sort}
if query:
params["query"] = query
return await self._request(
f"projects/{self.org}/{project_slug}/replays/",
params=params,
)
async def get_replay(
self,
replay_id: str,
project: str | None = None,
) -> dict | None:
"""取得單一 Replay 詳情"""
project_slug = project or self.project
return await self._request(
f"projects/{self.org}/{project_slug}/replays/{replay_id}/"
)
async def list_ui_errors(
self,
project: str | None = None,
limit: int = 50,
) -> list[dict] | None:
"""
查詢 UI 相關錯誤 (click, interaction, render)
專門抓取可能影響 UX 的前端錯誤:
- TypeError: Cannot read property
- Unhandled rejection
- ResizeObserver
- Click/Touch handler errors
"""
ui_query = (
"is:unresolved "
"level:error "
"(message:*click* OR message:*touch* OR message:*render* "
"OR message:*TypeError* OR message:*Cannot read*)"
)
return await self.list_issues(
project=project,
query=ui_query,
limit=limit,
)
async def get_ux_audit_summary(
self,
project: str | None = None,
) -> dict:
"""
取得 UX 審計摘要
Returns:
{
"replays_with_errors": int, # 有錯誤的 replay 數
"rage_clicks": int, # 憤怒點擊數
"ui_errors": int, # UI 錯誤數
"dead_clicks": int, # 無效點擊數
"details": [...] # 細節列表
}
"""
project_slug = project or self.project
# 並行查詢多種 UX 問題
results = {
"replays_with_errors": 0,
"rage_clicks": 0,
"ui_errors": 0,
"dead_clicks": 0,
"details": [],
}
# 1. 查詢有錯誤的 Replay
error_replays = await self.list_replays(
project=project_slug,
query="count_errors:>0",
limit=10,
)
if error_replays:
results["replays_with_errors"] = len(error_replays)
for replay in error_replays:
results["details"].append({
"type": "replay_with_errors",
"replay_id": replay.get("id"),
"url": self.get_replay_url(replay.get("id", "")),
"error_count": replay.get("count_errors", 0),
"urls": replay.get("urls", [])[:3], # 前 3 個訪問頁面
})
# 2. 查詢 UI 錯誤
ui_errors = await self.list_ui_errors(project=project_slug, limit=10)
if ui_errors:
results["ui_errors"] = len(ui_errors)
for issue in ui_errors:
results["details"].append({
"type": "ui_error",
"issue_id": issue.get("id"),
"url": self.get_issue_url(issue.get("id", "")),
"title": issue.get("title", "")[:100],
"count": issue.get("count", 0),
})
# 3. 查詢憤怒點擊 (Rage Clicks)
rage_replays = await self.list_replays(
project=project_slug,
query="count_rage_clicks:>0",
limit=10,
)
if rage_replays:
results["rage_clicks"] = sum(
r.get("count_rage_clicks", 0) for r in rage_replays
)
# 4. 查詢死亡點擊 (Dead Clicks)
dead_replays = await self.list_replays(
project=project_slug,
query="count_dead_clicks:>0",
limit=10,
)
if dead_replays:
results["dead_clicks"] = sum(
r.get("count_dead_clicks", 0) for r in dead_replays
)
return results
# =========================================================================
# Helper Methods
# =========================================================================
@@ -196,6 +347,10 @@ class SentryService:
"""取得 Issue 在 Sentry UI 的連結"""
return f"{self.base_url}/organizations/{self.org}/issues/{issue_id}/"
def get_replay_url(self, replay_id: str) -> str:
"""取得 Replay 在 Sentry UI 的連結"""
return f"{self.base_url}/organizations/{self.org}/replays/{replay_id}/"
# =========================================================================
# Dedup (Phase 10.2.1 - 2026-03-27)
# =========================================================================