""" Beta Monitoring — SaaS Turf Prédictions IA Sprint 8 — QA, Beta Fermee, Go/No-Go Ticket: HRT-34 Ce module : - Collecte les feedbacks beta via l'API in-app - Envoie des alertes Telegram en cas d'erreur détectée pendant la beta - Génère le rapport beta final (bugs, UX, NPS) Usage : # Démarrer le monitoring beta python tests/beta_monitor.py --watch --interval 60 # Générer le rapport beta final python tests/beta_monitor.py --report # Test d'envoi Telegram python tests/beta_monitor.py --test-telegram """ import os import sys import json import time import sqlite3 import requests import argparse from datetime import datetime, timedelta from pathlib import Path # ============================================================ # Configuration # ============================================================ BASE_URL = os.environ.get("APP_URL", "http://localhost:8792") TELEGRAM_TOKEN = os.environ.get( "TELEGRAM_TOKEN", "8649773134:AAFqzZVtSHfPPFDadcte1B-1h23nZ8DmdYE" ) TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "") # À configurer BETA_DB_PATH = os.environ.get("BETA_DB_PATH", "/home/h3r7/turf_saas/turf_saas.db") REPORTS_DIR = Path("tests/reports") REPORTS_DIR.mkdir(parents=True, exist_ok=True) # Seuils d'alerte ERROR_RATE_THRESHOLD = 0.01 # 1% d'erreurs → alerte LATENCY_P95_THRESHOLD_MS = 500 # p95 > 500ms → alerte BETA_MIN_USERS = 10 # Minimum d'utilisateurs beta requis NPS_TARGET = 7.0 # NPS cible (sur 10) # ============================================================ # Alertes Telegram # ============================================================ def send_telegram(message: str, parse_mode: str = "Markdown") -> bool: """Envoie un message Telegram d'alerte.""" if not TELEGRAM_TOKEN or not TELEGRAM_CHAT_ID: print(f"⚠️ Telegram non configuré. Message: {message[:100]}") return False try: resp = requests.post( f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage", json={ "chat_id": TELEGRAM_CHAT_ID, "text": message, "parse_mode": parse_mode, }, timeout=10, ) if resp.status_code == 200: print(f"✅ Alerte Telegram envoyée") return True else: print(f"❌ Telegram erreur: {resp.status_code} — {resp.text}") return False except Exception as e: print(f"❌ Telegram exception: {e}") return False def alert_error(endpoint: str, status_code: int, message: str): """Alerte Telegram sur erreur critique.""" text = ( f"🚨 *ALERTE BETA — SaaS Turf IA*\n\n" f"Erreur détectée sur `{endpoint}`\n" f"Status: `{status_code}`\n" f"Message: {message[:200]}\n" f"Heure: {datetime.now().strftime('%H:%M:%S')}\n\n" f"_Ticket: HRT-34_" ) send_telegram(text) def alert_performance(p95_ms: float, error_rate: float): """Alerte Telegram sur dégradation de performance.""" text = ( f"⚠️ *ALERTE PERFORMANCE — SaaS Turf IA*\n\n" f"p95 latence: `{p95_ms:.0f}ms` (seuil: {LATENCY_P95_THRESHOLD_MS}ms)\n" f"Error rate: `{error_rate * 100:.2f}%` (seuil: {ERROR_RATE_THRESHOLD * 100:.1f}%)\n" f"Heure: {datetime.now().strftime('%H:%M:%S')}\n\n" f"_Ticket: HRT-34_" ) send_telegram(text) # ============================================================ # Collecte de métriques # ============================================================ class BetaMonitor: """Moniteur actif pendant la beta fermée.""" ENDPOINTS_TO_CHECK = [ "/api", "/api/races", "/api/scoring", "/dashboard", "/", ] def __init__(self, base_url: str = BASE_URL): self.base_url = base_url.rstrip("/") self.errors: list[dict] = [] self.latencies: list[float] = [] self.check_count = 0 def check_endpoint(self, path: str) -> dict: """Vérifie un endpoint et retourne le résultat.""" start = time.time() try: resp = requests.get(f"{self.base_url}{path}", timeout=10) latency_ms = (time.time() - start) * 1000 return { "path": path, "status": resp.status_code, "latency_ms": latency_ms, "ok": resp.status_code < 500, "timestamp": datetime.now().isoformat(), } except requests.exceptions.ConnectionError as e: return { "path": path, "status": 0, "latency_ms": 0, "ok": False, "error": str(e), "timestamp": datetime.now().isoformat(), } except Exception as e: return { "path": path, "status": 0, "latency_ms": 0, "ok": False, "error": str(e), "timestamp": datetime.now().isoformat(), } def run_checks(self) -> dict: """Exécute tous les checks et retourne un résumé.""" results = [self.check_endpoint(p) for p in self.ENDPOINTS_TO_CHECK] self.check_count += 1 failures = [r for r in results if not r["ok"]] latencies = [r["latency_ms"] for r in results if r["latency_ms"] > 0] p95 = ( sorted(latencies)[int(len(latencies) * 0.95)] if len(latencies) >= 2 else (latencies[0] if latencies else 0) ) error_rate = len(failures) / len(results) if results else 0 # Stocker pour rapport self.latencies.extend(latencies) self.errors.extend(failures) return { "check_number": self.check_count, "timestamp": datetime.now().isoformat(), "total_checks": len(results), "failures": len(failures), "error_rate": error_rate, "p95_ms": p95, "results": results, } def watch(self, interval_seconds: int = 60): """Surveillance continue avec alertes Telegram.""" print(f"🔍 Beta monitoring démarré — {self.base_url}") print(f" Intervalle: {interval_seconds}s") print(f" Endpoints: {len(self.ENDPOINTS_TO_CHECK)}") print(f" Ctrl+C pour arrêter\n") consecutive_errors = 0 try: while True: summary = self.run_checks() timestamp = datetime.now().strftime("%H:%M:%S") status_icon = "✅" if summary["error_rate"] == 0 else "❌" print( f"[{timestamp}] {status_icon} " f"Check #{summary['check_number']} — " f"p95={summary['p95_ms']:.0f}ms, " f"errors={summary['failures']}/{summary['total_checks']}" ) # Alertes if summary["error_rate"] > ERROR_RATE_THRESHOLD: consecutive_errors += 1 if consecutive_errors >= 2: # 2 checks consécutifs en erreur for failure in summary["results"]: if not failure["ok"]: alert_error( failure["path"], failure.get("status", 0), failure.get("error", "Non-2xx response"), ) else: consecutive_errors = 0 if summary["p95_ms"] > LATENCY_P95_THRESHOLD_MS: print(f"⚠️ Latence p95 élevée: {summary['p95_ms']:.0f}ms") if summary["p95_ms"] > LATENCY_P95_THRESHOLD_MS * 2: alert_performance(summary["p95_ms"], summary["error_rate"]) # Sauvegarder les résultats log_file = REPORTS_DIR / "beta_monitor_log.jsonl" with open(log_file, "a") as f: f.write(json.dumps(summary) + "\n") time.sleep(interval_seconds) except KeyboardInterrupt: print(f"\n⏹️ Monitoring arrêté après {self.check_count} checks") self.generate_report() # ============================================================ # Rapport beta final # ============================================================ class BetaReport: """Générateur de rapport beta fermée.""" def __init__(self, base_url: str = BASE_URL): self.base_url = base_url self.timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") def collect_feedback_from_db(self) -> list[dict]: """Collecte les feedbacks depuis la BDD (table beta_feedback si elle existe).""" try: conn = sqlite3.connect(BETA_DB_PATH) c = conn.cursor() c.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='beta_feedback'" ) if not c.fetchone(): conn.close() return [] c.execute("SELECT * FROM beta_feedback ORDER BY created_at DESC") rows = c.fetchall() conn.close() return [dict(zip([col[0] for col in c.description], row)) for row in rows] except Exception as e: print(f"⚠️ Impossible de lire beta_feedback: {e}") return [] def collect_monitor_logs(self) -> list[dict]: """Lit les logs du monitoring beta.""" log_file = REPORTS_DIR / "beta_monitor_log.jsonl" if not log_file.exists(): return [] entries = [] with open(log_file) as f: for line in f: try: entries.append(json.loads(line)) except Exception: pass return entries def generate(self) -> str: """Génère le rapport complet et le sauvegarde.""" feedbacks = self.collect_feedback_from_db() monitor_logs = self.collect_monitor_logs() # Calculer NPS depuis les feedbacks nps_scores = [ f.get("nps_score") for f in feedbacks if f.get("nps_score") is not None ] avg_nps = sum(nps_scores) / len(nps_scores) if nps_scores else None # Statistiques monitoring if monitor_logs: all_latencies = [] total_errors = 0 total_checks = 0 for entry in monitor_logs: all_latencies.extend( [ r["latency_ms"] for r in entry.get("results", []) if r.get("latency_ms", 0) > 0 ] ) total_errors += entry.get("failures", 0) total_checks += entry.get("total_checks", 0) avg_latency = ( sum(all_latencies) / len(all_latencies) if all_latencies else 0 ) overall_error_rate = total_errors / total_checks if total_checks > 0 else 0 else: avg_latency = 0 overall_error_rate = 0 total_checks = 0 # Construire le rapport report = [] report.append("=" * 60) report.append("RAPPORT BETA FERMÉE — SaaS Turf Prédictions IA") report.append(f"Généré le : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") report.append(f"Ticket : HRT-34") report.append("=" * 60) report.append("") report.append("## 1. PARTICIPANTS BETA") report.append(f" Feedbacks reçus : {len(feedbacks)}") report.append( f" NPS moyen : {avg_nps:.1f}/10" if avg_nps else " NPS moyen : (en attente feedbacks)" ) report.append(f" Cible NPS : ≥ {NPS_TARGET}/10") nps_ok = avg_nps is not None and avg_nps >= NPS_TARGET report.append( f" Statut NPS : {'✅ OBJECTIF ATTEINT' if nps_ok else '⏳ En attente' if avg_nps is None else '❌ OBJECTIF NON ATTEINT'}" ) report.append("") report.append("## 2. BUGS SIGNALÉS") bugs = [f for f in feedbacks if f.get("type") == "bug"] critical_bugs = [b for b in bugs if b.get("severity") in ("critical", "high")] report.append(f" Total bugs : {len(bugs)}") report.append(f" Critiques/High : {len(critical_bugs)}") report.append( f" Statut : {'✅ 0 bug critique' if len(critical_bugs) == 0 else f'❌ {len(critical_bugs)} bug(s) critique(s)'}" ) report.append("") report.append("## 3. PERFORMANCE RÉELLE (monitoring)") report.append(f" Checks effectués: {total_checks}") report.append(f" Latence moyenne : {avg_latency:.1f}ms") report.append(f" Error rate : {overall_error_rate * 100:.2f}%") report.append(f" Seuil latence : {LATENCY_P95_THRESHOLD_MS}ms") perf_ok = ( avg_latency < LATENCY_P95_THRESHOLD_MS and overall_error_rate < ERROR_RATE_THRESHOLD ) report.append( f" Statut : {'✅ OBJECTIF ATTEINT' if perf_ok else '⏳ Données insuffisantes' if total_checks == 0 else '❌ OBJECTIF NON ATTEINT'}" ) report.append("") report.append("## 4. FEEDBACKS UX") ux_feedbacks = [f for f in feedbacks if f.get("type") == "ux"] report.append(f" Retours UX : {len(ux_feedbacks)}") if ux_feedbacks: for fb in ux_feedbacks[:5]: # Top 5 report.append(f" - {fb.get('comment', '')[:100]}") report.append("") report.append("## 5. VERDICT BETA FERMÉE") users_ok = len(feedbacks) >= 5 # Au moins 5 feedbacks = 5 users satisfaits verdict = all([users_ok, nps_ok, len(critical_bugs) == 0]) report.append( f" Participants suffisants (≥5) : {'✅' if users_ok else '❌'}" ) report.append(f" NPS ≥ 7/10 : {'✅' if nps_ok else '❌'}") report.append( f" 0 bug critique : {'✅' if len(critical_bugs) == 0 else '❌'}" ) report.append("") report.append( f" VERDICT GLOBAL : {'✅ GO — Beta réussie' if verdict else '❌ NO-GO — Conditions non remplies'}" ) report.append("=" * 60) report_text = "\n".join(report) # Sauvegarder report_file = REPORTS_DIR / f"beta_report_{self.timestamp}.txt" with open(report_file, "w") as f: f.write(report_text) print(report_text) print(f"\nRapport sauvegardé : {report_file}") return report_text # ============================================================ # CLI # ============================================================ def main(): parser = argparse.ArgumentParser(description="Beta Monitor — SaaS Turf IA") parser.add_argument("--watch", action="store_true", help="Surveillance continue") parser.add_argument( "--interval", type=int, default=60, help="Intervalle en secondes (défaut: 60)" ) parser.add_argument( "--report", action="store_true", help="Générer le rapport beta final" ) parser.add_argument( "--test-telegram", action="store_true", help="Tester l'envoi Telegram" ) parser.add_argument( "--url", default=BASE_URL, help=f"URL de l'app (défaut: {BASE_URL})" ) args = parser.parse_args() if args.test_telegram: print("Test d'envoi Telegram...") ok = send_telegram( "✅ *Test alerte Beta* — SaaS Turf IA\n_Ceci est un test du système d'alertes QA_\nTicket: HRT-34" ) sys.exit(0 if ok else 1) if args.report: reporter = BetaReport(args.url) reporter.generate() sys.exit(0) if args.watch: monitor = BetaMonitor(args.url) monitor.watch(interval_seconds=args.interval) sys.exit(0) parser.print_help() if __name__ == "__main__": main()