Instrument webhooks, fix compose layout, and ship social connect.
Deploy Beta / unit-tests (push) Successful in 10s
Deploy Beta / docker (push) Successful in 15s
Deploy Beta / deploy-beta (push) Successful in 1m40s

Add Grafana-friendly webhook request logging, Quill overflow fix, New Contact flow, in-place postcard campaign compose, and functional social account connect + composer.
This commit is contained in:
2026-08-10 05:15:49 -05:00
parent b3a6ee0cd0
commit 58258f2875
10 changed files with 778 additions and 311 deletions
+102 -3
View File
@@ -286,6 +286,76 @@ def _webhook_authorized(request, *, secret: str = "", secrets: list[str] | None
return False
def _log_webhook_request(request, *, channel: str) -> None:
"""Full request dump for Grafana / log aggregation."""
try:
headers = {str(k): str(v) for k, v in request.headers.items()}
except Exception: # noqa: BLE001
headers = {"_error": "unable to serialize headers"}
try:
body_text = (request.body or b"").decode("utf-8", errors="replace")
except Exception: # noqa: BLE001
body_text = repr(request.body)
if len(body_text) > 12000:
body_text = body_text[:12000] + "…[truncated]"
logger.info(
"webhook_received channel=%s path=%s method=%s query=%s",
channel,
request.path,
request.method,
request.META.get("QUERY_STRING", ""),
)
logger.info("webhook_headers channel=%s headers=%s", channel, headers)
logger.info("webhook_body channel=%s body=%s", channel, body_text)
def _log_webhook_auth_failed(request, *, channel: str) -> None:
logger.warning(
"webhook_auth_failed channel=%s path=%s "
"missing_or_invalid_authorization_or_token",
channel,
request.path,
)
def _log_webhook_result(
*,
channel: str,
event=None,
error: str = "",
extra: str = "",
) -> None:
if error:
logger.error(
"webhook_error channel=%s error=%s %s",
channel,
error,
extra,
)
return
if not event:
logger.warning(
"webhook_unmatched channel=%s no_provider_event_created %s",
channel,
extra,
)
return
message = getattr(event, "message", None)
campaign = getattr(message, "campaign", None) if message else None
logger.info(
"webhook_processed channel=%s event_type=%s event_id=%s "
"matched=%s message_id=%s campaign_id=%s campaign_name=%s %s",
channel,
getattr(event, "event_type", ""),
getattr(event, "pk", None),
bool(message),
getattr(message, "pk", None),
getattr(campaign, "pk", None),
getattr(campaign, "name", "") or "",
extra,
)
def _pcm_webhook_secrets() -> list[str]:
"""All PCM subscription signature secrets from env."""
raw_list = (getattr(settings, "PCM_WEBHOOK_SECRETS", None) or "").strip()
@@ -675,12 +745,21 @@ def postcard_webhook(request):
URL: https://<host>/portal/messaging/webhooks/postcard/
Copy each subscription's signature secret into PCM_WEBHOOK_SECRETS
"""
channel = "postcard"
_log_webhook_request(request, channel=channel)
if not _webhook_authorized(request, secrets=_pcm_webhook_secrets()):
_log_webhook_auth_failed(request, channel=channel)
return HttpResponseForbidden("invalid webhook token")
payload = parse_webhook_payload(request)
if not payload:
payload = request.POST.dict() or {}
event = process_pcm_postcard_webhook(payload)
try:
event = process_pcm_postcard_webhook(payload)
except Exception as exc: # noqa: BLE001
logger.exception("PCM postcard webhook processing failed")
_log_webhook_result(channel=channel, error=str(exc))
return JsonResponse({"ok": False, "error": "processing_failed"}, status=200)
_log_webhook_result(channel=channel, event=event)
return JsonResponse(
{
"ok": True,
@@ -705,9 +784,12 @@ def sms_webhook(request):
Inbound gateway POSTs without ``event`` (text=STOP, from=…) still opt out.
"""
channel = "sms"
_log_webhook_request(request, channel=channel)
if not _webhook_authorized(
request, secret=settings.SMTP2GO_WEBHOOK_SECRET or ""
):
_log_webhook_auth_failed(request, channel=channel)
return HttpResponseForbidden("invalid webhook token")
payload = parse_webhook_payload(request)
@@ -724,9 +806,21 @@ def sms_webhook(request):
or ""
)
stopped = bool(phone) and record_sms_stop(str(phone))
logger.info(
"webhook_processed channel=sms event_type=inbound_stop "
"opt_out=%s phone=%s",
stopped,
phone,
)
return JsonResponse({"ok": True, "opt_out": stopped})
event = process_smtp2go_sms_webhook(payload)
try:
event = process_smtp2go_sms_webhook(payload)
except Exception as exc: # noqa: BLE001
logger.exception("SMTP2GO SMS webhook processing failed")
_log_webhook_result(channel=channel, error=str(exc))
return JsonResponse({"ok": False, "error": "processing_failed"}, status=200)
_log_webhook_result(channel=channel, event=event)
return JsonResponse(
{
"ok": True,
@@ -749,16 +843,21 @@ def email_webhook(request):
Email events: all delivery/engagement boxes
Email headers: X-Monica-Message-Id
"""
channel = "email"
_log_webhook_request(request, channel=channel)
if not _webhook_authorized(
request, secret=settings.SMTP2GO_WEBHOOK_SECRET or ""
):
_log_webhook_auth_failed(request, channel=channel)
return HttpResponseForbidden("invalid webhook token")
payload = parse_webhook_payload(request)
try:
event = process_smtp2go_email_webhook(payload)
except Exception: # noqa: BLE001 — never 500 SMTP2GO (they retry for 48h)
except Exception as exc: # noqa: BLE001 — never 500 SMTP2GO (they retry for 48h)
logger.exception("SMTP2GO email webhook processing failed")
_log_webhook_result(channel=channel, error=str(exc))
return JsonResponse({"ok": False, "error": "processing_failed"}, status=200)
_log_webhook_result(channel=channel, event=event)
return JsonResponse(
{
"ok": True,