Multiple fixes: lead matching, metrics aggregation, Log field, SPRINT with capping=0

- Fix GoogleCampaignID matching with comma delimiters (exact word boundary)
- Remove attr_utm_medium filter (campaign ID filter is specific enough)
- Fix get_campaign_metrics: accumulate all daily rows for monthly totals
- Fix SPRINT urgency: only trigger when capping > 0
- Add Log field to GACampaignMes with Airtable vs Google Ads discrepancy info
- batch_update_gacampaignmes_advice now also updates Log field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Manuel 2026-04-25 11:05:06 +02:00
parent 5639bd6e76
commit a1aef8f58e
4 changed files with 48 additions and 23 deletions

View File

@ -344,10 +344,9 @@ class AirtableClient:
f"AND(" f"AND("
f"OR(" f"OR("
f"{{GACampaignID}}='{campaign_id}'," f"{{GACampaignID}}='{campaign_id}',"
f"FIND('{campaign_id}',{{GoogleCampaignID}})," f"FIND(',{campaign_id},',',' & {{GoogleCampaignID}} & ','),"
f"FIND('gad_campaignid={campaign_id}',{{attr_referer}})" f"FIND('gad_campaignid={campaign_id}',{{attr_referer}})"
f")," f"),"
f"{{attr_utm_medium}}='paid_search',"
f"{{creado}}>='{mes_inicio}'" f"{{creado}}>='{mes_inicio}'"
f")" f")"
) )
@ -372,16 +371,16 @@ class AirtableClient:
for i in range(0, len(batch), 10): for i in range(0, len(batch), 10):
self.gacampaignmes.batch_update(batch[i:i+10]) self.gacampaignmes.batch_update(batch[i:i+10])
def batch_update_gacampaignmes_advice(self, updates: list[tuple[str, str, str]]) -> None: def batch_update_gacampaignmes_advice(self, updates: list[tuple]) -> None:
""" """
Actualiza en lote los campos 'Consejo' y 'Criticidad' de GACampaignMes. Actualiza en lote los campos 'Consejo', 'Criticidad' y 'Log' de GACampaignMes.
updates: lista de (gcm_record_id, consejo_text, criticidad) updates: lista de (gcm_record_id, consejo_text, criticidad, log_text)
criticidad: 'Crítico' | 'Peligro' | 'Mantener' criticidad: 'Crítico' | 'Peligro' | 'Mantener'
""" """
for i in range(0, len(updates), 10): for i in range(0, len(updates), 10):
batch = [ batch = [
{"id": rid, "fields": {"Consejo": consejo, "Criticidad": criticidad}} {"id": rid, "fields": {"Consejo": consejo, "Criticidad": criticidad, "Log": log}}
for rid, consejo, criticidad in updates[i:i+10] for rid, consejo, criticidad, log in updates[i:i+10]
] ]
if batch: if batch:
self.gacampaignmes.batch_update(batch, typecast=True) self.gacampaignmes.batch_update(batch, typecast=True)

View File

@ -26,7 +26,7 @@ def analyze(campaign_config: dict, leads_entregados: int, ads_metrics: dict) ->
# Urgencia # Urgencia
if ratio_leads >= 1.0: if ratio_leads >= 1.0:
urgencia = "PAUSAR" urgencia = "PAUSAR"
elif ratio_leads < ratio_mes - 0.15 and dias_restantes <= 5: elif capping > 0 and ratio_leads < ratio_mes - 0.15 and dias_restantes <= 5:
urgencia = "SPRINT" urgencia = "SPRINT"
elif ritmo < -0.15: elif ritmo < -0.15:
urgencia = "ACELERAR" urgencia = "ACELERAR"

View File

@ -70,7 +70,7 @@ class GoogleAdsClient:
return result return result
def get_campaign_metrics(self, campaign_id: str) -> dict: def get_campaign_metrics(self, campaign_id: str) -> dict:
"""Métricas del mes en curso para una campaña concreta.""" """Métricas del mes en curso para una campaña concreta (acumulado mensual)."""
ga_service = self.client.get_service("GoogleAdsService") ga_service = self.client.get_service("GoogleAdsService")
query = f""" query = f"""
SELECT SELECT
@ -82,27 +82,42 @@ class GoogleAdsClient:
metrics.cost_micros, metrics.cost_micros,
metrics.conversions, metrics.conversions,
metrics.clicks, metrics.clicks,
metrics.impressions, metrics.impressions
metrics.ctr
FROM campaign FROM campaign
WHERE campaign.id = {campaign_id} WHERE campaign.id = {campaign_id}
AND segments.date DURING THIS_MONTH AND segments.date DURING THIS_MONTH
""" """
try: try:
response = ga_service.search(customer_id=self.customer_id, query=query) response = ga_service.search(customer_id=self.customer_id, query=query)
cost = conversions = clicks = impressions = 0
meta = {}
for row in response: for row in response:
m = row.metrics m = row.metrics
return { cost += m.cost_micros / 1_000_000
"campaign_id": campaign_id, conversions += m.conversions
clicks += m.clicks
impressions += m.impressions
if not meta:
meta = {
"name": row.campaign.name, "name": row.campaign.name,
"status": row.campaign.status.name, "status": row.campaign.status.name,
"budget_daily": row.campaign_budget.amount_micros / 1_000_000, "budget_daily": row.campaign_budget.amount_micros / 1_000_000,
"budget_resource_name": row.campaign_budget.resource_name, "budget_resource_name": row.campaign_budget.resource_name,
"cost": m.cost_micros / 1_000_000, }
"conversions": m.conversions, if not meta:
"clicks": m.clicks, return {}
"impressions": m.impressions, ctr = round(clicks / impressions * 100, 2) if impressions > 0 else 0.0
"ctr": round(m.ctr * 100, 2), return {
"campaign_id": campaign_id,
"name": meta["name"],
"status": meta["status"],
"budget_daily": meta["budget_daily"],
"budget_resource_name": meta["budget_resource_name"],
"cost": round(cost, 2),
"conversions": conversions,
"clicks": clicks,
"impressions": impressions,
"ctr": ctr,
} }
except GoogleAdsException as e: except GoogleAdsException as e:
print(f" ❌ Error Google Ads para campaña {campaign_id}: {e}") print(f" ❌ Error Google Ads para campaña {campaign_id}: {e}")

11
run.py
View File

@ -225,10 +225,21 @@ def run():
apply_decision(campaign, decision, metrics, gads) apply_decision(campaign, decision, metrics, gads)
conv_google = int(analysis["conversiones_google"])
discrepancia_fresh = abs(conv_google - leads)
if discrepancia_fresh > 10:
log_text = (
f"⚠️ DISCREPANCIA: {leads} leads Airtable vs {conv_google} conv Google Ads "
f"{discrepancia_fresh})"
)
else:
log_text = f"OK: {leads} leads Airtable | {conv_google} conv Google Ads"
advice_updates.append(( advice_updates.append((
campaign["airtable_id"], campaign["airtable_id"],
decision.get("consejo", ""), decision.get("consejo", ""),
CRITICIDAD_MAP[p], CRITICIDAD_MAP[p],
log_text,
)) ))
resumen.append({ resumen.append({