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:
Deeman
2026-02-24 19:29:59 +01:00
parent 3dc7a7fc02
commit dc38972d68
3 changed files with 17 additions and 5 deletions

View File

@@ -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(

View File

@@ -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 []

View File

@@ -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