diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 076c2c8..c83bf6a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -37,7 +37,7 @@ function RedirectToSession() { let cancelled = false; const routeAfterInitCheck = async () => { - if (authRequired && user?.role !== 'admin') { + if (authRequired && user !== null && user?.role !== 'admin') { if (!cancelled) setReady('chat'); return; } diff --git a/frontend/src/pages/AlertDetailPage.tsx b/frontend/src/pages/AlertDetailPage.tsx index 37df994..9e5f83d 100644 --- a/frontend/src/pages/AlertDetailPage.tsx +++ b/frontend/src/pages/AlertDetailPage.tsx @@ -1,9 +1,53 @@ import { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { ArrowLeft, Clock, Send, MessageSquare, Loader2 } from 'lucide-react'; +import { ArrowLeft, Clock, MessageSquare, Loader2, CheckCircle2, XCircle, Radio } from 'lucide-react'; import { cn } from '../lib/utils'; import { fetchAlert } from '../lib/api'; -import type { Alert } from '../types'; +import type { Alert, AlertNotification } from '../types'; + +const CHANNEL_LABELS: Record = { + slack: 'Slack', + sns: 'SNS', + telegram: 'Telegram', + email: 'Email', + pagerduty: 'PagerDuty', +}; + +const CHANNEL_ICONS: Record = { + slack: 'πŸ’¬', + sns: 'πŸ“‘', + telegram: '✈️', + email: 'πŸ“§', + pagerduty: 'πŸ“Ÿ', +}; + +function NotificationRow({ n }: { n: AlertNotification }) { + const label = CHANNEL_LABELS[n.channel] ?? n.channel; + const icon = CHANNEL_ICONS[n.channel] ?? 'πŸ””'; + return ( +
+ {icon} + {label} + {n.status === 'delivered' + ? + : n.status === 'failed' + ? + : + } + + {n.status} + + {n.error && ( + {n.error} + )} +
+ ); +} export default function AlertDetailPage() { const { alertId } = useParams<{ alertId: string }>(); @@ -73,9 +117,9 @@ export default function AlertDetailPage() {
{new Date(alert.timestamp).toLocaleString()} - {alert.sns_sent && ( - - SNS notified + {alert.trigger_source && ( + + {alert.trigger_source === 'poller' ? '⏱ Poller' : '⚑ Event consumer'} )}
@@ -102,6 +146,17 @@ export default function AlertDetailPage() { )} + {alert.notifications && alert.notifications.length > 0 && ( +
+

Notified via

+
+ {alert.notifications.map((n, i) => ( + + ))} +
+
+ )} + ); diff --git a/frontend/src/pages/InitPage.tsx b/frontend/src/pages/InitPage.tsx index e8aec13..cefa2bc 100644 --- a/frontend/src/pages/InitPage.tsx +++ b/frontend/src/pages/InitPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { ArrowRight, Loader2, Terminal, Eye, EyeOff, CheckCircle, XCircle, AlertTriangle, ChevronRight, Check } from 'lucide-react'; +import { ArrowRight, ArrowLeft, Loader2, Terminal, Eye, EyeOff, CheckCircle, XCircle, AlertTriangle, ChevronRight, Check } from 'lucide-react'; import { cn } from '../lib/utils'; import { useAuth } from '../context/AuthContext'; import { apiFetch } from '../lib/api'; @@ -65,7 +65,8 @@ export default function InitPage() { // Step 4 const [infraDone, setInfraDone] = useState(false); const [queueUrl, setQueueUrl] = useState(''); - const [showSkipDisclaimer, setShowSkipDisclaimer] = useState(false); + const [showSkipDisclaimer, setShowSkipDisclaimer] = useState(false); + const [showSkipDisclaimer2, setShowSkipDisclaimer2] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); @@ -298,10 +299,34 @@ export default function InitPage() { className="w-full mt-10 flex items-center justify-center gap-2 bg-gray-900 dark:bg-white text-white dark:text-gray-900 text-base font-semibold py-4 rounded-xl hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors disabled:opacity-50"> {loading ? : <>Continue } - + {showSkipDisclaimer2 && ( +
+

Skipping this step disables event monitoring

+
    + {['Automatic incident detection will not run', 'Monitoring page will have no alerts', 'Slack / SNS notifications will not fire'].map(item => ( +
  • + {item} +
  • + ))} +
+

+ You can enable this anytime from Settings β†’ AWS Configuration.{' '} + + Learn more in monitoring.md + +

+ +
+ )} )} @@ -366,8 +391,14 @@ export default function InitPage() { {!requiredPassed && (
- Some required permissions are missing. Event monitoring may not work correctly. - You can still continue β€” fix the IAM permissions and re-check in Settings. + Some required permissions are missing. Event monitoring will not work until these are fixed. + You can still continue and fix IAM permissions later β€” re-check in{' '} + Settings β†’ AWS Configuration.{' '} + + monitoring.md +
)} @@ -378,10 +409,16 @@ export default function InitPage() { Continue anyway - +
+ + +
)} @@ -423,6 +460,11 @@ export default function InitPage() { } + + {/* Skip option */} + )} + + + + + {/* Coming soon integrations */} +
+ {COMING_SOON_INTEGRATIONS.map((intg, i) => ( +
{intg.icon}
@@ -246,17 +314,18 @@ export default function SettingsPage() {
{intg.name}
{intg.desc}
- + + Coming soon +
))} + Browse available models on OpenRouter - + )} {/* AWS Configuration tab */} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 202ca3e..c7793cd 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -98,6 +98,13 @@ export interface MessageRecord { created_at: string; } +export interface AlertNotification { + channel: string; + status: 'delivered' | 'failed' | 'attempted'; + error: string | null; + sent_at: string; +} + export interface Alert { id: string; service: string; @@ -108,6 +115,8 @@ export interface Alert { sns_sent: boolean; timestamp: string; session_id?: string; + trigger_source?: 'poller' | 'event_consumer'; + notifications?: AlertNotification[]; } export interface ServiceStatus { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 9e1cb1b..fc45b90 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -20,8 +20,9 @@ export default defineConfig({ '/api/settings': 'http://localhost:8000', '/api/history': 'http://localhost:8000', '/debug': 'http://localhost:8000', - '/api/monitoring': 'http://localhost:8000', - '/api/init': 'http://localhost:8000', + '/api/monitoring': 'http://localhost:8000', + '/api/init': 'http://localhost:8000', + '/api/integrations': 'http://localhost:8000', }, }, build: { diff --git a/migrations/011_trigger_source_notifications.sql b/migrations/011_trigger_source_notifications.sql new file mode 100644 index 0000000..63089d0 --- /dev/null +++ b/migrations/011_trigger_source_notifications.sql @@ -0,0 +1,14 @@ +-- Track which detection path triggered each alert +ALTER TABLE alerts ADD COLUMN IF NOT EXISTS trigger_source TEXT; -- 'poller' | 'event_consumer' + +-- Per-alert, per-channel delivery log +CREATE TABLE IF NOT EXISTS alert_notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + alert_id UUID NOT NULL REFERENCES alerts(id) ON DELETE CASCADE, + channel TEXT NOT NULL, -- 'slack' | 'sns' | 'telegram' | ... + status TEXT NOT NULL DEFAULT 'attempted', -- 'delivered' | 'failed' | 'attempted' + error TEXT, + sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_alert_notifications_alert_id ON alert_notifications (alert_id); diff --git a/src/agent/db/base.py b/src/agent/db/base.py index 40620ef..be79daf 100644 --- a/src/agent/db/base.py +++ b/src/agent/db/base.py @@ -140,9 +140,19 @@ async def add_alert( dedup_key: str | None = None, status: str = "completed", session_id: str | None = None, + trigger_source: str | None = None, ) -> str: return "" + async def add_notification( + self, + alert_id: str, + channel: str, + status: str = "attempted", + error: str | None = None, + ) -> None: + pass + async def is_recent_alert(self, dedup_key: str, within_minutes: int = 3) -> bool: return False diff --git a/src/agent/db/postgres.py b/src/agent/db/postgres.py index bc0c8ca..31b0ac9 100644 --- a/src/agent/db/postgres.py +++ b/src/agent/db/postgres.py @@ -577,16 +577,32 @@ async def add_alert( dedup_key: str | None = None, status: str = "completed", session_id: str | None = None, + trigger_source: str | None = None, ) -> str: row = await self._fetchrow( "INSERT INTO alerts" - " (service, error, resolution, confidence, sns_sent, dedup_key, status, session_id)" - " VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING id", + " (service, error, resolution, confidence, sns_sent, dedup_key, status," + " session_id, trigger_source)" + " VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id", service, error, resolution, confidence, sns_sent, dedup_key, status, uuid.UUID(session_id) if session_id else None, + trigger_source, ) return str(row["id"]) if row else "" + async def add_notification( + self, + alert_id: str, + channel: str, + status: str = "attempted", + error: str | None = None, + ) -> None: + await self._exec( + "INSERT INTO alert_notifications (alert_id, channel, status, error)" + " VALUES (%s, %s, %s, %s)", + uuid.UUID(alert_id), channel, status, error, + ) + async def is_recent_alert(self, dedup_key: str, within_minutes: int = 3) -> bool: row = await self._fetchrow( "SELECT 1 FROM alerts WHERE dedup_key = %s" @@ -597,8 +613,9 @@ async def is_recent_alert(self, dedup_key: str, within_minutes: int = 3) -> bool async def get_alerts(self, limit: int = 50) -> list[dict]: rows = await self._fetchall( - "SELECT id, service, error, resolution, confidence, sns_sent, status, created_at, session_id " - "FROM alerts ORDER BY created_at DESC LIMIT %s", + "SELECT id, service, error, resolution, confidence, sns_sent, status," + " created_at, session_id, trigger_source" + " FROM alerts ORDER BY created_at DESC LIMIT %s", min(limit, 200), ) return [ @@ -612,18 +629,25 @@ async def get_alerts(self, limit: int = 50) -> list[dict]: "status": r["status"], "timestamp": r["created_at"].isoformat() if r["created_at"] else None, "session_id": str(r["session_id"]) if r["session_id"] else None, + "trigger_source": r["trigger_source"], } for r in rows ] async def get_alert(self, alert_id: str) -> dict | None: row = await self._fetchrow( - "SELECT id, service, error, resolution, confidence, sns_sent, status, created_at, session_id " - "FROM alerts WHERE id = %s", + "SELECT id, service, error, resolution, confidence, sns_sent, status," + " created_at, session_id, trigger_source" + " FROM alerts WHERE id = %s", uuid.UUID(alert_id), ) if not row: return None + notif_rows = await self._fetchall( + "SELECT channel, status, error, sent_at FROM alert_notifications" + " WHERE alert_id = %s ORDER BY sent_at ASC", + uuid.UUID(alert_id), + ) return { "id": str(row["id"]), "service": row["service"], @@ -634,6 +658,16 @@ async def get_alert(self, alert_id: str) -> dict | None: "status": row["status"], "timestamp": row["created_at"].isoformat() if row["created_at"] else None, "session_id": str(row["session_id"]) if row["session_id"] else None, + "trigger_source": row["trigger_source"], + "notifications": [ + { + "channel": n["channel"], + "status": n["status"], + "error": n["error"], + "sent_at": n["sent_at"].isoformat() if n["sent_at"] else None, + } + for n in notif_rows + ], } async def get_app_config(self, key: str) -> dict | None: diff --git a/src/agent/event_consumer.py b/src/agent/event_consumer.py index a00c243..5e4c462 100644 --- a/src/agent/event_consumer.py +++ b/src/agent/event_consumer.py @@ -16,7 +16,7 @@ get_runtime_sns_topic_arn, get_runtime_sqs_queue_url, ) -from agent.monitor_store import add_alert, is_recent_alert, update_service +from agent.monitor_store import add_alert, add_notification, is_recent_alert, update_service from config import settings # In-memory set of dedup keys currently being investigated. @@ -286,7 +286,9 @@ async def _deliver(result: dict[str, Any], event: dict, dedup_key: str, session_ sns_arn = get_runtime_sns_topic_arn() sns_sent = False + sns_attempted = False if sns_arn: + sns_attempted = True try: from tools.sns import publish_sns_alert @@ -314,25 +316,25 @@ async def _deliver(result: dict[str, Any], event: dict, dedup_key: str, session_ except Exception as e: logger.error("SNS delivery failed: {}", e) - # Also notify Slack (keeps parity with the poller) + slack_sent = False if settings.slack_webhook_url: try: from integrations.slack_webhook import post_failed_investigation, post_investigation if status == "failed": - await post_failed_investigation( + slack_sent = await post_failed_investigation( settings.slack_webhook_url, service, root_cause, session_id, aws_error=aws_error, is_test=is_test, ) else: - await post_investigation( + slack_sent = await post_investigation( settings.slack_webhook_url, result, session_id, is_test=is_test ) except Exception as e: logger.error("Slack delivery failed from event consumer: {}", e) update_service(service, status, root_cause) - await add_alert( + alert_id = await add_alert( service=service, error=root_cause, resolution=resolution, @@ -341,7 +343,13 @@ async def _deliver(result: dict[str, Any], event: dict, dedup_key: str, session_ dedup_key=dedup_key, status=status, session_id=session_id, + trigger_source="event_consumer", ) + if alert_id: + if sns_attempted: + await add_notification(alert_id, "sns", "delivered" if sns_sent else "failed") + if settings.slack_webhook_url: + await add_notification(alert_id, "slack", "delivered" if slack_sent else "failed") logger.info("Alert delivered: service={} confidence={} sns={}", service, confidence, sns_sent) @@ -405,6 +413,7 @@ async def _process_event(event: dict) -> None: session_id = str(uuid.uuid4()) try: + logger.debug("Event consumer: starting investigation for {} / {} (session {})", source, detail_type, session_id[:8]) result = await _run_investigation(prompt, session_id) if not result: logger.warning("Investigation produced no result for {} / {}", source, detail_type) diff --git a/src/agent/monitor_store.py b/src/agent/monitor_store.py index 09d934a..9ddc2c6 100644 --- a/src/agent/monitor_store.py +++ b/src/agent/monitor_store.py @@ -19,19 +19,36 @@ async def add_alert( dedup_key: str | None = None, status: str = "completed", session_id: str | None = None, + trigger_source: str | None = None, ) -> str: """Persist an alert to the DB backend.""" from agent.db import db try: return await db.add_alert( - service, error, resolution, confidence, sns_sent, dedup_key, status, session_id + service, error, resolution, confidence, sns_sent, + dedup_key, status, session_id, trigger_source, ) except Exception as e: logger.error("Failed to persist alert: {}", e) return "" +async def add_notification( + alert_id: str, + channel: str, + status: str = "attempted", + error: str | None = None, +) -> None: + """Record a delivery attempt for a channel against an alert.""" + from agent.db import db + + try: + await db.add_notification(alert_id, channel, status, error) + except Exception as e: + logger.error("Failed to persist notification: {}", e) + + async def is_recent_alert(dedup_key: str, within_minutes: int = 3) -> bool: """Return True if an alert with this dedup_key was saved within the last N minutes.""" from agent.db import db diff --git a/src/agent/poller.py b/src/agent/poller.py index 9ca0362..6f0d98f 100644 --- a/src/agent/poller.py +++ b/src/agent/poller.py @@ -164,11 +164,9 @@ def _f(obj: Any, key: str, default: Any = None) -> Any: async def _persist_and_notify( result: dict[str, Any], tool_calls_log: list[dict[str, Any]], session_id: str, dedup_key: str ) -> None: - from agent.monitor_store import add_alert, update_service + from agent.monitor_store import add_alert, add_notification, update_service from agent.turns import notify_slack - await notify_slack(session_id, tool_calls_log) - status = result.pop("_status", "completed") services_affected = result.get("services_affected", []) service = services_affected[0] if services_affected else "unknown" @@ -176,8 +174,19 @@ async def _persist_and_notify( mitigation = result.get("mitigation_steps", []) confidence = result.get("confidence", "MEDIUM") resolution = "\n".join(mitigation) if isinstance(mitigation, list) else str(mitigation) + + slack_sent = False + if settings.slack_webhook_url: + if status == "failed": + from integrations.slack_webhook import post_failed_investigation + slack_sent = await post_failed_investigation( + settings.slack_webhook_url, service, root_cause, session_id + ) + else: + slack_sent = await notify_slack(session_id, tool_calls_log) + update_service(service, status, root_cause) - await add_alert( + alert_id = await add_alert( service=service, error=root_cause, resolution=resolution, @@ -186,7 +195,12 @@ async def _persist_and_notify( dedup_key=dedup_key, status=status, session_id=session_id, + trigger_source="poller", ) + if alert_id and settings.slack_webhook_url: + await add_notification( + alert_id, "slack", "delivered" if slack_sent else "failed" + ) async def _check_alarms() -> None: @@ -197,7 +211,9 @@ async def _check_alarms() -> None: from tools.cloudwatch import get_alarms data = await asyncio.get_event_loop().run_in_executor(None, get_alarms, "ALARM") - for alarm in data.get("alarms", []): + alarms = data.get("alarms", []) + logger.debug("Poller: {} alarm(s) in ALARM state", len(alarms)) + for alarm in alarms: name = alarm.get("name", "unknown") reason = alarm.get("reason", "") metric = alarm.get("metric", "") @@ -220,9 +236,10 @@ async def _check_alarms() -> None: "Please investigate the root cause and provide mitigation steps." ) session_id = str(uuid.uuid4()) + logger.debug("Poller: starting investigation for alarm {} (session {})", name, session_id[:8]) result, tool_calls_log = await _run_investigation(prompt, session_id) if result: - logger.info("Poller investigation complete for alarm: {}", name) + logger.info("Poller: investigation complete for alarm {} β€” {}", name, result.get("confidence", "?")) await _persist_and_notify(result, tool_calls_log, session_id, key) @@ -234,16 +251,19 @@ async def _check_lambda_errors() -> None: from tools.lambda_ import get_lambda_error_rate, list_lambda_functions funcs = await asyncio.get_event_loop().run_in_executor(None, list_lambda_functions) - for fn in funcs.get("functions", [])[:20]: # cap at 20 to avoid runaway API calls + fn_list = funcs.get("functions", [])[:20] + logger.debug("Poller: checking error rates for {} Lambda function(s)", len(fn_list)) + for fn in fn_list: # cap at 20 to avoid runaway API calls name = fn.get("name", "") if not name: continue + window_hours = max(1 / 60, settings.poll_interval_seconds / 3600) metrics = await asyncio.get_event_loop().run_in_executor( None, get_lambda_error_rate, name, - 1, # last 1 hour + window_hours, ) error_rate = metrics.get("error_rate_pct", 0) or 0 if error_rate < settings.poll_error_threshold: @@ -272,9 +292,12 @@ async def _check_lambda_errors() -> None: "Please investigate the root cause and suggest a fix." ) session_id = str(uuid.uuid4()) + logger.debug("Poller: starting investigation for Lambda {} (session {})", name, session_id[:8]) result, tool_calls_log = await _run_investigation(prompt, session_id) if result: - logger.info("Poller investigation complete for Lambda: {}", name) + logger.info( + "Poller: investigation complete for Lambda {} β€” {}", name, result.get("confidence", "?") + ) await _persist_and_notify(result, tool_calls_log, session_id, key) @@ -289,12 +312,13 @@ async def polling_loop() -> None: ) while True: await asyncio.sleep(interval) - logger.debug("Poller tick") + logger.debug("Poller tick β€” checking alarms and Lambda error rates") try: from agent.init_store import is_event_infra_enabled if not is_event_infra_enabled(): # EventBridge β†’ SQS already covers alarm state changes when infra is active await _check_alarms() await _check_lambda_errors() + logger.debug("Poller tick complete β€” next check in {}s", interval) except Exception as e: logger.error("Poller tick failed: {}", e) diff --git a/src/agent/turns.py b/src/agent/turns.py index f440d7f..641c315 100644 --- a/src/agent/turns.py +++ b/src/agent/turns.py @@ -87,13 +87,16 @@ async def save_turn( logger.error("[{}] DB save failed: {}", session_id[:8], e) -async def notify_slack(session_id: str, tool_calls_log: list[dict[str, Any]]) -> None: - """Post to Slack if submit_investigation was called and a webhook is configured.""" +async def notify_slack(session_id: str, tool_calls_log: list[dict[str, Any]]) -> bool: + """Post to Slack if submit_investigation was called and a webhook is configured. + + Returns True if the message was sent successfully, False otherwise. + """ if not settings.slack_webhook_url: - return + return False for tc in tool_calls_log: if tc.get("tool") == "submit_investigation": from integrations.slack_webhook import post_investigation - await post_investigation(settings.slack_webhook_url, tc["args"], session_id) - return + return await post_investigation(settings.slack_webhook_url, tc["args"], session_id) + return False diff --git a/src/api/app.py b/src/api/app.py index b39ebc5..3cee542 100644 --- a/src/api/app.py +++ b/src/api/app.py @@ -20,6 +20,7 @@ chat, dashboard, history, + integrations, monitoring, sessions, settings, @@ -143,6 +144,7 @@ async def lifespan(_app: FastAPI): app.include_router(auth.router) app.include_router(users.router) app.include_router(settings.router) +app.include_router(integrations.router) app.include_router(init_router.router) app.include_router(monitoring.router) diff --git a/src/api/routers/integrations.py b/src/api/routers/integrations.py new file mode 100644 index 0000000..059ce41 --- /dev/null +++ b/src/api/routers/integrations.py @@ -0,0 +1,44 @@ +"""Integrations router β€” manage third-party integration actions (e.g. Slack test).""" + +from __future__ import annotations + +import uuid +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException + +from api.auth import get_current_user +from config import settings + +router = APIRouter(prefix="/api/integrations", tags=["integrations"]) + + +@router.post("/slack/test") +async def test_slack( + _user: Annotated[dict | None, Depends(get_current_user)], +) -> dict: + """Send a test Slack message to verify the webhook is working.""" + if not settings.slack_webhook_url: + raise HTTPException(status_code=400, detail="SLACK_WEBHOOK_URL is not configured") + + from integrations.slack_webhook import post_investigation + + test_result = { + "root_cause_category": "SYSTEM_CHANGE", + "root_cause_summary": ( + "This is a test message from OpenDevOps Agent " + "to verify your Slack integration is working correctly." + ), + "confidence": "HIGH", + "evidence": ["Test triggered from Settings β†’ Integrations"], + "mitigation_steps": ["No action needed β€” this is a test"], + "services_affected": ["OpenDevOps Agent"], + } + session_id = str(uuid.uuid4()) + await post_investigation( + webhook_url=settings.slack_webhook_url, + result=test_result, + session_id=session_id, + is_test=True, + ) + return {"ok": True} diff --git a/src/integrations/slack_webhook.py b/src/integrations/slack_webhook.py index 511120b..819a335 100644 --- a/src/integrations/slack_webhook.py +++ b/src/integrations/slack_webhook.py @@ -98,15 +98,18 @@ def _bullets(items: list[str]) -> str: } -async def _post(webhook_url: str, payload: dict) -> None: +async def _post(webhook_url: str, payload: dict) -> bool: try: import httpx async with httpx.AsyncClient(timeout=10) as client: resp = await client.post(webhook_url, json=payload) if resp.status_code != 200: logger.warning("Slack webhook returned {}: {}", resp.status_code, resp.text) + return False + return True except Exception as e: logger.error("Slack notification failed: {}", e) + return False async def post_investigation( @@ -115,11 +118,13 @@ async def post_investigation( session_id: str, app_url: str | None = None, is_test: bool = False, -) -> None: - """Post a completed investigation result to Slack.""" +) -> bool: + """Post a completed investigation result to Slack. Returns True on success.""" payload = _build_payload(result, session_id, app_url, is_test=is_test) - await _post(webhook_url, payload) - logger.info("Slack notification sent for session {}", session_id[:8]) + ok = await _post(webhook_url, payload) + if ok: + logger.info("Slack notification sent for session {}", session_id[:8]) + return ok async def post_failed_investigation( @@ -129,8 +134,8 @@ async def post_failed_investigation( session_id: str, aws_error: str = "", is_test: bool = False, -) -> None: - """Post a failed investigation alert to Slack.""" +) -> bool: + """Post a failed investigation alert to Slack. Returns True on success.""" header_text = ( "πŸ§ͺ [TEST] OpenDevOps Agent β€” Investigation Failed" if is_test @@ -151,5 +156,7 @@ async def post_failed_investigation( }, ] payload = {"attachments": [{"color": "#e74c3c", "blocks": blocks}]} - await _post(webhook_url, payload) - logger.info("Slack failure notification sent for session {}", session_id[:8]) + ok = await _post(webhook_url, payload) + if ok: + logger.info("Slack failure notification sent for session {}", session_id[:8]) + return ok