Fix Slack block limit: send one message per campaign + error logging
- Refactor send_daily_report: summary in one message, each campaign in its own message — eliminates the 50-block Slack limit that was silently dropping campaigns - _post now raises RuntimeError on Slack API errors (was silently returning None) - send_slack_report.py accepts optional date argument (defaults to yesterday) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6ec3342661
commit
ac7a0a9738
@ -17,7 +17,8 @@ def _extract_vertical(name: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
run_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
import sys as _sys
|
||||||
|
run_date = _sys.argv[1] if len(_sys.argv) > 1 else (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||||
print(f"Reenviando informe para {run_date}...")
|
print(f"Reenviando informe para {run_date}...")
|
||||||
|
|
||||||
meta = MetaAdsClient()
|
meta = MetaAdsClient()
|
||||||
|
|||||||
@ -57,7 +57,10 @@ def _post(method: str, **payload) -> dict:
|
|||||||
json=payload,
|
json=payload,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
return resp.json()
|
data = resp.json()
|
||||||
|
if not data.get("ok"):
|
||||||
|
raise RuntimeError(f"Slack {method} error: {data.get('error')} — {data.get('response_metadata', {}).get('messages', '')}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def update_message(channel: str, ts: str, text: str):
|
def update_message(channel: str, ts: str, text: str):
|
||||||
@ -253,15 +256,23 @@ def send_daily_report(
|
|||||||
})
|
})
|
||||||
|
|
||||||
blocks.append({"type": "divider"})
|
blocks.append({"type": "divider"})
|
||||||
|
|
||||||
# ── Análisis por campaña: decisión + adsets + anuncios ────────────────────
|
|
||||||
if campaign_details:
|
|
||||||
blocks.append({
|
blocks.append({
|
||||||
"type": "section",
|
"type": "context",
|
||||||
"text": {"type": "mrkdwn",
|
"elements": [{"type": "mrkdwn",
|
||||||
"text": f"*Análisis por campaña ({campaigns_analyzed} activas ayer)*"},
|
"text": f"{campaigns_analyzed} campañas analizadas — detalle por campaña a continuación"}],
|
||||||
})
|
})
|
||||||
# Build lookup: campaign_name → action info
|
|
||||||
|
# ── Enviar resumen ────────────────────────────────────────────────────────
|
||||||
|
result = _post(
|
||||||
|
"chat.postMessage",
|
||||||
|
channel=config.SLACK_CHANNEL_ID,
|
||||||
|
blocks=blocks,
|
||||||
|
text=f"Meta Optimizer — {prefix} — {date_label}",
|
||||||
|
)
|
||||||
|
ts = result.get("ts")
|
||||||
|
|
||||||
|
# ── Un mensaje por campaña (sin límite de bloques) ────────────────────────
|
||||||
|
if campaign_details:
|
||||||
action_map = {a["campaign_name"]: a for a in actions}
|
action_map = {a["campaign_name"]: a for a in actions}
|
||||||
|
|
||||||
for cid, detail in campaign_details.items():
|
for cid, detail in campaign_details.items():
|
||||||
@ -279,11 +290,9 @@ def send_daily_report(
|
|||||||
budget = bid_cfg.get("daily_budget_eur")
|
budget = bid_cfg.get("daily_budget_eur")
|
||||||
budget_str = f"{budget:.0f}€/día" if budget else "—"
|
budget_str = f"{budget:.0f}€/día" if budget else "—"
|
||||||
|
|
||||||
# Icono y etiqueta de acción
|
|
||||||
atype = action["action_type"] if action else "MAINTAIN"
|
atype = action["action_type"] if action else "MAINTAIN"
|
||||||
emoji, alabel = _ACTION_DISPLAY.get(atype, ("⚪", atype))
|
emoji, alabel = _ACTION_DISPLAY.get(atype, ("⚪", atype))
|
||||||
|
|
||||||
# Campaign header
|
|
||||||
header_text = (
|
header_text = (
|
||||||
f"{emoji} *{name}*\n"
|
f"{emoji} *{name}*\n"
|
||||||
f"Vertical: _{vertical}_ | Margen: *{m_str}* | "
|
f"Vertical: _{vertical}_ | Margen: *{m_str}* | "
|
||||||
@ -291,25 +300,23 @@ def send_daily_report(
|
|||||||
f"*Decisión: {alabel}*"
|
f"*Decisión: {alabel}*"
|
||||||
)
|
)
|
||||||
if action:
|
if action:
|
||||||
justification = action.get("justification", "")[:500]
|
header_text += f"\n_{action.get('justification', '')[:500]}_"
|
||||||
header_text += f"\n_{justification}_"
|
|
||||||
if action.get("alert"):
|
if action.get("alert"):
|
||||||
header_text += f"\n:warning: {action['alert']}"
|
header_text += f"\n:warning: {action['alert']}"
|
||||||
|
|
||||||
blocks.append({
|
camp_blocks: list = [
|
||||||
"type": "section",
|
{"type": "section", "text": {"type": "mrkdwn", "text": header_text}},
|
||||||
"text": {"type": "mrkdwn", "text": header_text},
|
]
|
||||||
})
|
|
||||||
|
|
||||||
# Botones solo para acciones que ejecutan algo en Meta API
|
# Botones de acción a nivel campaña
|
||||||
if action and atype in _ACTIONABLE:
|
if action and atype in _ACTIONABLE:
|
||||||
effect = _effect_text(action, budget)
|
effect = _effect_text(action, budget)
|
||||||
if effect:
|
if effect:
|
||||||
blocks.append({
|
camp_blocks.append({
|
||||||
"type": "section",
|
"type": "section",
|
||||||
"text": {"type": "mrkdwn", "text": effect},
|
"text": {"type": "mrkdwn", "text": effect},
|
||||||
})
|
})
|
||||||
blocks.append({
|
camp_blocks.append({
|
||||||
"type": "actions",
|
"type": "actions",
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
@ -330,48 +337,26 @@ def send_daily_report(
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Tabla adsets + anuncios
|
# Tabla adsets + anuncios
|
||||||
detail_parts = []
|
|
||||||
adset_table = _adset_ad_table(adsets, "Conjuntos de anuncios", show_bid=True)
|
adset_table = _adset_ad_table(adsets, "Conjuntos de anuncios", show_bid=True)
|
||||||
ad_table = _adset_ad_table(ads, "Anuncios")
|
ad_table = _adset_ad_table(ads, "Anuncios")
|
||||||
if adset_table:
|
combined = "\n\n".join(filter(None, [adset_table, ad_table]))
|
||||||
detail_parts.append(adset_table)
|
|
||||||
if ad_table:
|
|
||||||
detail_parts.append(ad_table)
|
|
||||||
if detail_parts:
|
|
||||||
combined = "\n\n".join(detail_parts)
|
|
||||||
for chunk in [combined[i:i+2900] for i in range(0, len(combined), 2900)]:
|
for chunk in [combined[i:i+2900] for i in range(0, len(combined), 2900)]:
|
||||||
blocks.append({
|
camp_blocks.append({
|
||||||
"type": "section",
|
"type": "section",
|
||||||
"text": {"type": "mrkdwn", "text": chunk},
|
"text": {"type": "mrkdwn", "text": chunk},
|
||||||
})
|
})
|
||||||
|
|
||||||
# Botones de pausa por anuncio (solo los que Claude recomienda pausar)
|
# Botones de pausa por anuncio
|
||||||
blocks.extend(_ad_action_blocks(ads))
|
camp_blocks.extend(_ad_action_blocks(ads))
|
||||||
|
|
||||||
blocks.append({"type": "divider"})
|
_post(
|
||||||
|
|
||||||
if len(blocks) >= 45: # Safety margin before Slack's 50-block limit
|
|
||||||
blocks.append({
|
|
||||||
"type": "section",
|
|
||||||
"text": {"type": "mrkdwn",
|
|
||||||
"text": "_... más campañas disponibles en Baserow._"},
|
|
||||||
})
|
|
||||||
break
|
|
||||||
|
|
||||||
# ── Pie ───────────────────────────────────────────────────────────────────
|
|
||||||
blocks.append({
|
|
||||||
"type": "context",
|
|
||||||
"elements": [{"type": "mrkdwn",
|
|
||||||
"text": f"{campaigns_analyzed} campañas analizadas ayer"}],
|
|
||||||
})
|
|
||||||
|
|
||||||
result = _post(
|
|
||||||
"chat.postMessage",
|
"chat.postMessage",
|
||||||
channel=config.SLACK_CHANNEL_ID,
|
channel=config.SLACK_CHANNEL_ID,
|
||||||
blocks=blocks,
|
blocks=camp_blocks,
|
||||||
text=f"Meta Optimizer — {prefix} — {date_label}",
|
text=name,
|
||||||
)
|
)
|
||||||
return result.get("ts") if result.get("ok") else None
|
|
||||||
|
return ts
|
||||||
|
|
||||||
|
|
||||||
def send_execution_summary(log: dict):
|
def send_execution_summary(log: dict):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user