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:
@@ -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)
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user