fix: replace silent exception handlers with explicit error logging
Every bare `except Exception: pass` or `except Exception: return sentinel` now logs via logger.exception() or logger.warning() so errors surface in the application log instead of disappearing silently. Changes per file: - admin/routes.py: add logger; log in _inject_admin_sidebar_data(), email_detail() Resend enrichment, audiences() contact count loop, audience_contacts() Resend fetch - core.py: log in _get_or_create_resend_audience(), capture_waitlist_email() DB insert, and capture_waitlist_email() Resend contact sync (warning level since that path is documented as non-critical) - analytics.py: log DuckDB query failures before returning [] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
Admin domain: role-based admin panel for managing users, tasks, etc.
|
Admin domain: role-based admin panel for managing users, tasks, etc.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ from ..core import (
|
|||||||
utcnow_iso,
|
utcnow_iso,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Blueprint with its own template folder
|
# Blueprint with its own template folder
|
||||||
bp = Blueprint(
|
bp = Blueprint(
|
||||||
"admin",
|
"admin",
|
||||||
@@ -50,6 +53,7 @@ async def _inject_admin_sidebar_data():
|
|||||||
row = await fetch_one("SELECT COUNT(*) as cnt FROM inbound_emails WHERE is_read = 0")
|
row = await fetch_one("SELECT COUNT(*) as cnt FROM inbound_emails WHERE is_read = 0")
|
||||||
g.admin_unread_count = row["cnt"] if row else 0
|
g.admin_unread_count = row["cnt"] if row else 0
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logger.exception("Failed to load admin sidebar unread count")
|
||||||
g.admin_unread_count = 0
|
g.admin_unread_count = 0
|
||||||
|
|
||||||
|
|
||||||
@@ -1041,7 +1045,7 @@ async def email_detail(email_id: int):
|
|||||||
else:
|
else:
|
||||||
enriched_html = getattr(result, "html", "")
|
enriched_html = getattr(result, "html", "")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Metadata-only fallback
|
logger.warning("Failed to fetch email body from Resend for %s", email["resend_id"], exc_info=True)
|
||||||
|
|
||||||
return await render_template(
|
return await render_template(
|
||||||
"admin/email_detail.html",
|
"admin/email_detail.html",
|
||||||
@@ -1194,7 +1198,7 @@ async def audiences():
|
|||||||
data = getattr(contacts, "data", [])
|
data = getattr(contacts, "data", [])
|
||||||
a["contact_count"] = len(data) if data else 0
|
a["contact_count"] = len(data) if data else 0
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
logger.warning("Failed to fetch contact count for audience %s", a.get("audience_id"), exc_info=True)
|
||||||
|
|
||||||
return await render_template("admin/audiences.html", audiences=audience_list)
|
return await render_template("admin/audiences.html", audiences=audience_list)
|
||||||
|
|
||||||
@@ -1220,6 +1224,7 @@ async def audience_contacts(audience_id: str):
|
|||||||
else:
|
else:
|
||||||
contacts = getattr(result, "data", []) or []
|
contacts = getattr(result, "data", []) or []
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logger.exception("Failed to fetch contacts from Resend for audience %s", audience_id)
|
||||||
await flash("Failed to fetch contacts from Resend.", "error")
|
await flash("Failed to fetch contacts from Resend.", "error")
|
||||||
|
|
||||||
return await render_template(
|
return await render_template(
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ Usage:
|
|||||||
rows = await fetch_analytics("SELECT * FROM serving.planner_defaults WHERE city_slug = ?", ["berlin"])
|
rows = await fetch_analytics("SELECT * FROM serving.planner_defaults WHERE city_slug = ?", ["berlin"])
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_conn = None # duckdb.DuckDBPyConnection | None — lazy import
|
_conn = None # duckdb.DuckDBPyConnection | None — lazy import
|
||||||
_DUCKDB_PATH = os.environ.get("SERVING_DUCKDB_PATH", "data/analytics.duckdb")
|
_DUCKDB_PATH = os.environ.get("SERVING_DUCKDB_PATH", "data/analytics.duckdb")
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@ async def fetch_analytics(sql: str, params: list | None = None) -> list[dict[str
|
|||||||
Run a read-only DuckDB query and return rows as dicts.
|
Run a read-only DuckDB query and return rows as dicts.
|
||||||
|
|
||||||
Returns [] if analytics DB is unavailable (not yet built, or DUCKDB_PATH unset).
|
Returns [] if analytics DB is unavailable (not yet built, or DUCKDB_PATH unset).
|
||||||
Never raises — callers should treat empty results as "no data yet".
|
Returns [] on query error after logging — callers treat empty results as "no data yet".
|
||||||
"""
|
"""
|
||||||
assert sql, "sql must not be empty"
|
assert sql, "sql must not be empty"
|
||||||
|
|
||||||
@@ -62,4 +65,5 @@ async def fetch_analytics(sql: str, params: list | None = None) -> list[dict[str
|
|||||||
try:
|
try:
|
||||||
return await asyncio.to_thread(_run)
|
return await asyncio.to_thread(_run)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logger.exception("DuckDB analytics query failed: %.200s", sql)
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ async def _get_or_create_resend_audience(name: str) -> str | None:
|
|||||||
)
|
)
|
||||||
return audience_id
|
return audience_id
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logger.exception("Failed to create Resend audience %r", name)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -495,7 +496,8 @@ async def capture_waitlist_email(
|
|||||||
)
|
)
|
||||||
is_new = cursor_result > 0
|
is_new = cursor_result > 0
|
||||||
except Exception:
|
except Exception:
|
||||||
# If anything fails, treat as not-new to avoid double-sending
|
logger.exception("Failed to insert waitlist entry for %r", email)
|
||||||
|
# Treat as not-new to avoid double-sending on retry
|
||||||
is_new = False
|
is_new = False
|
||||||
|
|
||||||
# Enqueue confirmation email only if new
|
# Enqueue confirmation email only if new
|
||||||
@@ -517,7 +519,8 @@ async def capture_waitlist_email(
|
|||||||
resend.api_key = config.RESEND_API_KEY
|
resend.api_key = config.RESEND_API_KEY
|
||||||
resend.Contacts.create({"email": email, "audience_id": audience_id})
|
resend.Contacts.create({"email": email, "audience_id": audience_id})
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Silent fail
|
# Non-critical: audience sync failure doesn't affect waitlist capture
|
||||||
|
logger.warning("Failed to add %r to Resend audience %r", email, audience_name, exc_info=True)
|
||||||
|
|
||||||
return is_new
|
return is_new
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user