mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 07:57:19 -05:00
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.
This commit is contained in:
380
backend/app/routers/sentry.py
Normal file
380
backend/app/routers/sentry.py
Normal file
@@ -0,0 +1,380 @@
|
||||
"""
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user