Spaces:
Sleeping
Sleeping
| """Developer alert notifier. | |
| Single source of truth for immediate infrastructure/system error email alerts | |
| sent to the developer team when no Mail ID is available to attribute the error | |
| to. Processing-error alerts (Mail ID available) flow through | |
| processing_error_handler → daily_brief instead. | |
| Callers MUST await send_developer_alert() before continuing so delivery is | |
| confirmed before the loop or job resumes. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from app.config import Settings | |
| from app.lib.graph.auth import GraphAuthProvider | |
| from app.lib.graph.client import GraphClient | |
| logger = logging.getLogger(__name__) | |
| def _build_sendmail_payload( | |
| recipients: list[str], | |
| subject: str, | |
| body_html: str, | |
| mailbox_user: str, | |
| ) -> tuple[str, dict]: | |
| """Build a (path, payload) pair ready for GraphClient.post(). | |
| Shared by send_developer_alert and scheduler._send_daily_brief to avoid | |
| duplicating the sendMail envelope shape. | |
| """ | |
| path = f"/users/{mailbox_user}/sendMail" | |
| payload = { | |
| "message": { | |
| "subject": subject, | |
| "body": {"contentType": "HTML", "content": body_html}, | |
| "toRecipients": [ | |
| {"emailAddress": {"address": r}} for r in recipients | |
| ], | |
| }, | |
| "saveToSentItems": False, | |
| } | |
| return path, payload | |
| async def send_developer_alert( | |
| settings: Settings, | |
| subject: str, | |
| body: str, | |
| ) -> None: | |
| """Send an immediate alert email to DEV_ALERT_RECIPIENTS. | |
| The body is expected to contain both the exception message and the full | |
| stack trace (formatted via traceback.format_exc()) so the developer has | |
| complete context in the email. | |
| Creates its own Graph client on demand — safe to call from the processor | |
| (which has no shared client) and from failure paths where the existing | |
| client may be broken. | |
| If sending fails the exception is logged and swallowed — this function | |
| must never recurse or propagate, as all callers are already in an error | |
| path. | |
| """ | |
| recipients = settings.dev_alert_recipient_list() | |
| if not recipients: | |
| logger.warning( | |
| "send_developer_alert: DEV_ALERT_RECIPIENTS not configured; " | |
| "alert not sent. Subject: %s", | |
| subject, | |
| ) | |
| return | |
| client: GraphClient | None = None | |
| try: | |
| auth = GraphAuthProvider(settings) | |
| client = GraphClient(auth) | |
| path, payload = _build_sendmail_payload( | |
| recipients=recipients, | |
| subject=subject, | |
| body_html=f"<pre>{body}</pre>", | |
| mailbox_user=settings.mailbox_user, | |
| ) | |
| await client.post(path, payload) | |
| logger.info( | |
| "Developer alert sent to %d recipient(s). Subject: %s", | |
| len(recipients), | |
| subject, | |
| ) | |
| except Exception: | |
| logger.exception( | |
| "send_developer_alert: failed to send alert. Subject: %s", subject | |
| ) | |
| finally: | |
| if client is not None: | |
| try: | |
| await client.aclose() | |
| except Exception: | |
| pass | |