diff --git a/apps/api/src/api/v1/sentry_webhook.py b/apps/api/src/api/v1/sentry_webhook.py index 2f8b8f007..ed4c58918 100644 --- a/apps/api/src/api/v1/sentry_webhook.py +++ b/apps/api/src/api/v1/sentry_webhook.py @@ -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]}"