Files
blackroad-operating-system/backend/app/routers/sentry.py
Claude 84ab793177 Add comprehensive multi-API integration support
This commit adds extensive API integration capabilities for deployment,
payments, communications, and monitoring to BlackRoad OS.

New API Integrations:
- Railway API: Cloud deployment management (GraphQL)
- Vercel API: Serverless deployment platform (REST)
- Stripe API: Payment processing and billing
- Twilio API: SMS, Voice, and WhatsApp messaging
- Slack API: Team collaboration and notifications
- Discord API: Community messaging and notifications
- Sentry API: Error tracking and application monitoring

Core Features:
- Centralized API client manager with health checking
- Comprehensive health monitoring endpoint (/api/health/*)
- Automatic retry logic and rate limit handling
- Unified status monitoring for all integrations

Infrastructure:
- Railway deployment configuration (railway.json, railway.toml)
- Enhanced GitHub Actions workflows:
  * backend-tests.yml: Comprehensive test suite with PostgreSQL/Redis
  * railway-deploy.yml: Automated Railway deployment with notifications
- Docker build validation in CI/CD pipeline

Testing:
- Comprehensive test suite for all API integrations
- API connectivity verification in CI/CD
- Mock-friendly architecture for testing without credentials

Configuration:
- Updated .env.example with all new API keys
- Added stripe and sentry-sdk to requirements.txt
- Registered all new routers in main.py
- Updated API info endpoint with new integrations

Documentation:
- API_INTEGRATIONS.md: Complete setup and usage guide
- Interactive API docs at /api/docs with all endpoints
- Health check endpoints for monitoring

All APIs are optional and gracefully handle missing credentials.
The system provides clear status messages for configuration requirements.
2025-11-16 09:34:14 +00:00

381 lines
11 KiB
Python

"""
Sentry Error Tracking Integration Router
Provides endpoints for error tracking, performance monitoring, and release management.
Sentry is an application monitoring and error tracking platform.
"""
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from datetime import datetime
import httpx
import os
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/sentry", tags=["sentry"])
# Sentry API configuration
SENTRY_AUTH_TOKEN = os.getenv("SENTRY_AUTH_TOKEN")
SENTRY_ORG = os.getenv("SENTRY_ORG")
SENTRY_DSN = os.getenv("SENTRY_DSN")
class SentryError(BaseModel):
"""Sentry error/event model"""
message: str
level: str = "error" # debug, info, warning, error, fatal
tags: Optional[Dict[str, str]] = None
extra: Optional[Dict[str, Any]] = None
class SentryRelease(BaseModel):
"""Sentry release model"""
version: str
projects: List[str]
ref: Optional[str] = None
class SentryClient:
"""Sentry REST API client"""
def __init__(
self,
auth_token: Optional[str] = None,
org: Optional[str] = None
):
self.auth_token = auth_token or SENTRY_AUTH_TOKEN
self.org = org or SENTRY_ORG
self.base_url = "https://sentry.io/api/0"
def _get_headers(self) -> Dict[str, str]:
"""Get API request headers"""
if not self.auth_token:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Sentry auth token not configured"
)
return {
"Authorization": f"Bearer {self.auth_token}",
"Content-Type": "application/json"
}
async def _request(
self,
method: str,
endpoint: str,
json_data: Optional[Dict] = None,
params: Optional[Dict] = None
) -> Dict[str, Any]:
"""Make API request"""
headers = self._get_headers()
url = f"{self.base_url}{endpoint}"
async with httpx.AsyncClient() as client:
try:
response = await client.request(
method,
url,
headers=headers,
json=json_data,
params=params,
timeout=30.0
)
response.raise_for_status()
# Handle 204 No Content
if response.status_code == 204:
return {"success": True}
return response.json()
except httpx.HTTPStatusError as e:
logger.error(f"Sentry API error: {e.response.text}")
raise HTTPException(
status_code=e.response.status_code,
detail=f"Sentry API error: {e.response.text}"
)
except httpx.HTTPError as e:
logger.error(f"Sentry API request failed: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Sentry API request failed: {str(e)}"
)
async def get_projects(self) -> List[Dict[str, Any]]:
"""Get all projects in organization"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
return await self._request("GET", f"/organizations/{self.org}/projects/")
async def get_issues(
self,
project: str,
limit: int = 25
) -> List[Dict[str, Any]]:
"""Get issues for a project"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
params = {"limit": limit}
return await self._request(
"GET",
f"/projects/{self.org}/{project}/issues/",
params=params
)
async def get_events(
self,
project: str,
limit: int = 25
) -> List[Dict[str, Any]]:
"""Get events for a project"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
params = {"limit": limit}
return await self._request(
"GET",
f"/projects/{self.org}/{project}/events/",
params=params
)
async def create_release(
self,
version: str,
projects: List[str],
ref: Optional[str] = None
) -> Dict[str, Any]:
"""Create a new release"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
data = {
"version": version,
"projects": projects
}
if ref:
data["ref"] = ref
return await self._request(
"POST",
f"/organizations/{self.org}/releases/",
json_data=data
)
async def list_releases(
self,
project: str,
limit: int = 25
) -> List[Dict[str, Any]]:
"""List releases for a project"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
params = {"limit": limit}
return await self._request(
"GET",
f"/projects/{self.org}/{project}/releases/",
params=params
)
async def get_stats(
self,
project: str,
stat: str = "received"
) -> List[Dict[str, Any]]:
"""Get project statistics"""
if not self.org:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Sentry organization not configured"
)
params = {"stat": stat}
return await self._request(
"GET",
f"/projects/{self.org}/{project}/stats/",
params=params
)
# Initialize client
sentry_client = SentryClient()
@router.get("/status")
async def get_sentry_status():
"""Get Sentry API connection status"""
if not SENTRY_AUTH_TOKEN:
return {
"connected": False,
"message": "Sentry auth token not configured. Set SENTRY_AUTH_TOKEN environment variable.",
"org_configured": bool(SENTRY_ORG),
"dsn_configured": bool(SENTRY_DSN)
}
try:
# Test API connection
projects = await sentry_client.get_projects()
return {
"connected": True,
"message": "Sentry API connected successfully",
"organization": SENTRY_ORG,
"project_count": len(projects),
"dsn_configured": bool(SENTRY_DSN)
}
except Exception as e:
return {
"connected": False,
"message": f"Sentry API connection failed: {str(e)}",
"org_configured": bool(SENTRY_ORG),
"dsn_configured": bool(SENTRY_DSN)
}
@router.get("/projects")
async def list_projects():
"""List all Sentry projects"""
try:
projects = await sentry_client.get_projects()
return {
"projects": projects,
"count": len(projects)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching projects: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch projects: {str(e)}"
)
@router.get("/projects/{project}/issues")
async def list_issues(project: str, limit: int = 25):
"""List issues for a project"""
try:
issues = await sentry_client.get_issues(project, limit)
return {
"issues": issues,
"count": len(issues)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching issues: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch issues: {str(e)}"
)
@router.get("/projects/{project}/events")
async def list_events(project: str, limit: int = 25):
"""List events for a project"""
try:
events = await sentry_client.get_events(project, limit)
return {
"events": events,
"count": len(events)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching events: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch events: {str(e)}"
)
@router.post("/releases")
async def create_release(release: SentryRelease):
"""Create a new release"""
try:
result = await sentry_client.create_release(
version=release.version,
projects=release.projects,
ref=release.ref
)
return {
"success": True,
"release": result
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating release: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create release: {str(e)}"
)
@router.get("/projects/{project}/releases")
async def list_releases(project: str, limit: int = 25):
"""List releases for a project"""
try:
releases = await sentry_client.list_releases(project, limit)
return {
"releases": releases,
"count": len(releases)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching releases: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch releases: {str(e)}"
)
@router.get("/projects/{project}/stats")
async def get_stats(project: str, stat: str = "received"):
"""Get project statistics"""
try:
stats = await sentry_client.get_stats(project, stat)
return {
"stats": stats,
"stat_type": stat
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching stats: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch stats: {str(e)}"
)
@router.get("/health")
async def sentry_health_check():
"""Sentry API health check endpoint"""
return {
"service": "sentry",
"status": "operational" if SENTRY_AUTH_TOKEN else "not_configured",
"timestamp": datetime.utcnow().isoformat()
}