feat(api): Sentry + OpenClaw + Telegram 整合 (Phase 10)
- 新增 Telegram 告警發送 (send_sentry_telegram_alert) - 新增 Approval 創建 (create_sentry_approval) - 整合 analyze_and_comment 流程: 1. OpenClaw AI 分析 2. 建立 Approval 記錄 3. 發送 Telegram 告警 (含 Y/n 按鈕) 4. 回寫 Sentry Comment Memory: project_sentry_openclaw_v2.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,15 +14,26 @@ AWOOOI API - Sentry Webhook Handler
|
||||
🔴 HARD RULE: 時間顯示使用 Asia/Taipei (UTC+8)
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from src.utils.timezone import now_taipei, now_taipei_iso
|
||||
import uuid
|
||||
|
||||
import httpx
|
||||
import structlog
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from src.utils.timezone import now_taipei_iso
|
||||
from src.services.telegram_gateway import get_telegram_gateway
|
||||
from src.services.approval_db import get_approval_service
|
||||
from src.models.approval import (
|
||||
ApprovalRequestCreate,
|
||||
RiskLevel,
|
||||
BlastRadius,
|
||||
DataImpact,
|
||||
DryRunCheck,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/webhooks/sentry", tags=["Sentry Webhook"])
|
||||
|
||||
@@ -30,6 +41,14 @@ router = APIRouter(prefix="/webhooks/sentry", tags=["Sentry Webhook"])
|
||||
OPENCLAW_URL = "http://192.168.0.188:8088"
|
||||
SENTRY_API_URL = "http://192.168.0.110:9000"
|
||||
|
||||
# Sentry Level → Risk Level 映射
|
||||
SENTRY_LEVEL_TO_RISK = {
|
||||
"fatal": RiskLevel.CRITICAL,
|
||||
"error": RiskLevel.HIGH,
|
||||
"warning": RiskLevel.MEDIUM,
|
||||
"info": RiskLevel.LOW,
|
||||
}
|
||||
|
||||
|
||||
class SentryIssuePayload(BaseModel):
|
||||
"""Sentry Issue Alert Payload (簡化版)"""
|
||||
@@ -151,27 +170,50 @@ async def analyze_and_comment(
|
||||
project_slug: str
|
||||
):
|
||||
"""
|
||||
背景任務:分析錯誤並回寫 Sentry Comment
|
||||
背景任務:分析錯誤 + Telegram 告警 + 建立 Approval
|
||||
|
||||
Phase 10: Sentry + OpenClaw AI 整合
|
||||
1. 呼叫 OpenClaw 分析
|
||||
2. 發送 Telegram 告警
|
||||
3. 建立 Approval 供人工審核
|
||||
4. 回寫 Sentry Comment
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starting AI analysis for issue {issue_id}")
|
||||
logger.info("sentry_analysis_started", issue_id=issue_id)
|
||||
|
||||
# 呼叫 OpenClaw 分析
|
||||
# 1. 呼叫 OpenClaw 分析
|
||||
analysis = await call_openclaw_analyzer(error_context)
|
||||
|
||||
# 2. 建立 Approval
|
||||
approval_id = await create_sentry_approval(
|
||||
error_context=error_context,
|
||||
analysis=analysis,
|
||||
)
|
||||
|
||||
# 3. 發送 Telegram 告警
|
||||
await send_sentry_telegram_alert(
|
||||
error_context=error_context,
|
||||
analysis=analysis,
|
||||
approval_id=approval_id,
|
||||
)
|
||||
|
||||
# 4. 回寫 Sentry Comment (如果分析成功)
|
||||
if analysis:
|
||||
# 回寫 Sentry Comment
|
||||
await post_sentry_comment(
|
||||
project_slug=project_slug,
|
||||
issue_id=issue_id,
|
||||
analysis=analysis
|
||||
)
|
||||
logger.info(f"Analysis completed for issue {issue_id}")
|
||||
else:
|
||||
logger.warning(f"Analysis returned empty for issue {issue_id}")
|
||||
|
||||
logger.info(
|
||||
"sentry_analysis_completed",
|
||||
issue_id=issue_id,
|
||||
approval_id=approval_id,
|
||||
has_analysis=analysis is not None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Analysis failed for issue {issue_id}: {e}")
|
||||
logger.exception("sentry_analysis_failed", issue_id=issue_id, error=str(e))
|
||||
|
||||
|
||||
async def call_openclaw_analyzer(error_context: dict) -> ErrorAnalysisResult | None:
|
||||
@@ -248,4 +290,128 @@ async def post_sentry_comment(
|
||||
# )
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to post Sentry comment: {e}")
|
||||
logger.exception("sentry_comment_failed", issue_id=issue_id, error=str(e))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Telegram 告警 (Phase 10: Sentry + OpenClaw)
|
||||
# =============================================================================
|
||||
|
||||
async def send_sentry_telegram_alert(
|
||||
error_context: dict,
|
||||
analysis: ErrorAnalysisResult | None,
|
||||
approval_id: str,
|
||||
):
|
||||
"""
|
||||
發送 Sentry 錯誤告警到 Telegram
|
||||
|
||||
格式 (project_sentry_openclaw_v2.md):
|
||||
═══════════════════════════
|
||||
🐛 SENTRY 錯誤告警
|
||||
═══════════════════════════
|
||||
📍 components/dashboard.tsx:142
|
||||
❌ TypeError: Cannot read property 'x' of null
|
||||
👥 影響用戶: 12
|
||||
🔄 發生次數: 42
|
||||
───────────────────────────
|
||||
🧠 OpenClaw 分析:
|
||||
「這是 null check 問題...」
|
||||
───────────────────────────
|
||||
[ Y 建立修復任務 ] [ n 忽略 ]
|
||||
"""
|
||||
try:
|
||||
telegram = get_telegram_gateway()
|
||||
await telegram.initialize()
|
||||
|
||||
# 提取錯誤資訊
|
||||
title = error_context.get("title", "Unknown Error")
|
||||
culprit = error_context.get("culprit", "unknown")
|
||||
level = error_context.get("level", "error")
|
||||
|
||||
# 發送 Sentry 告警卡片 (含 Y/n 按鈕)
|
||||
await telegram.send_approval_card(
|
||||
approval_id=approval_id,
|
||||
risk_level="high" if level in ["fatal", "error"] else "medium",
|
||||
resource_name=culprit,
|
||||
root_cause=analysis.root_cause if analysis else title,
|
||||
suggested_action=analysis.fix_suggestion if analysis else "待分析",
|
||||
primary_responsibility="FE" if "tsx" in culprit or "jsx" in culprit else "BE",
|
||||
confidence=analysis.confidence if analysis else 0.0,
|
||||
namespace="sentry",
|
||||
)
|
||||
|
||||
logger.info("sentry_telegram_sent", approval_id=approval_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("sentry_telegram_failed", error=str(e))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Approval 創建 (Phase 10: Sentry + OpenClaw)
|
||||
# =============================================================================
|
||||
|
||||
async def create_sentry_approval(
|
||||
error_context: dict,
|
||||
analysis: ErrorAnalysisResult | None,
|
||||
) -> str:
|
||||
"""
|
||||
為 Sentry 錯誤建立 Approval 記錄
|
||||
|
||||
Returns:
|
||||
str: Approval ID
|
||||
"""
|
||||
try:
|
||||
approval_service = get_approval_service()
|
||||
|
||||
# 決定風險等級
|
||||
level = error_context.get("level", "error")
|
||||
risk_level = SENTRY_LEVEL_TO_RISK.get(level, RiskLevel.MEDIUM)
|
||||
|
||||
# 組裝 Approval 請求
|
||||
title = error_context.get("title", "Unknown Error")
|
||||
culprit = error_context.get("culprit", "unknown")
|
||||
project = error_context.get("project", "unknown")
|
||||
issue_id = error_context.get("issue_id", "unknown")
|
||||
|
||||
approval_request = ApprovalRequestCreate(
|
||||
source="sentry",
|
||||
alert_type=f"sentry_{level}",
|
||||
target_resource=culprit,
|
||||
namespace=project,
|
||||
risk_level=risk_level,
|
||||
root_cause=analysis.root_cause if analysis else title,
|
||||
suggestion=analysis.fix_suggestion if analysis else "待 AI 分析",
|
||||
blast_radius=BlastRadius.SERVICE, # Sentry 錯誤通常影響單一服務
|
||||
data_impact=DataImpact.READ_ONLY,
|
||||
dry_run_check=DryRunCheck.PASSED,
|
||||
llm_provider=analysis.analyzed_by if analysis else "pending",
|
||||
llm_confidence=analysis.confidence if analysis else 0.0,
|
||||
metadata={
|
||||
"sentry_issue_id": issue_id,
|
||||
"sentry_project": project,
|
||||
"error_count": error_context.get("count", 1),
|
||||
"first_seen": error_context.get("first_seen"),
|
||||
"stacktrace": error_context.get("stacktrace", []),
|
||||
},
|
||||
)
|
||||
|
||||
# 創建 Approval
|
||||
approval_id = str(uuid.uuid4())
|
||||
await approval_service.create_approval(
|
||||
approval_id=approval_id,
|
||||
request=approval_request,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"sentry_approval_created",
|
||||
approval_id=approval_id,
|
||||
issue_id=issue_id,
|
||||
risk_level=risk_level.value,
|
||||
)
|
||||
|
||||
return approval_id
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("sentry_approval_creation_failed", error=str(e))
|
||||
# 即使創建失敗,也返回一個臨時 ID
|
||||
return f"temp-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
Reference in New Issue
Block a user