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"OR("
f"{{GACampaignID}}='{campaign_id}',"
f"FIND('{campaign_id}',{{GoogleCampaignID}}),"
f"FIND(',{campaign_id},',',' & {{GoogleCampaignID}} & ','),"
f"FIND('gad_campaignid={campaign_id}',{{attr_referer}})"
f"),"
f"{{attr_utm_medium}}='paid_search',"
f"{{creado}}>='{mes_inicio}'"
f")"
)
@ -372,16 +371,16 @@ class AirtableClient:
for i in range(0, len(batch), 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.
updates: lista de (gcm_record_id, consejo_text, criticidad)
Actualiza en lote los campos 'Consejo', 'Criticidad' y 'Log' de GACampaignMes.
updates: lista de (gcm_record_id, consejo_text, criticidad, log_text)
criticidad: 'Crítico' | 'Peligro' | 'Mantener'
"""
for i in range(0, len(updates), 10):
batch = [
{"id": rid, "fields": {"Consejo": consejo, "Criticidad": criticidad}}
for rid, consejo, criticidad in updates[i:i+10]
{"id": rid, "fields": {"Consejo": consejo, "Criticidad": criticidad, "Log": log}}
for rid, consejo, criticidad, log in updates[i:i+10]
]
if batch:
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
if ratio_leads >= 1.0:
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"
elif ritmo < -0.15:
urgencia = "ACELERAR"

View File

@ -70,7 +70,7 @@ class GoogleAdsClient:
return result
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")
query = f"""
SELECT
@ -82,28 +82,43 @@ class GoogleAdsClient:
metrics.cost_micros,
metrics.conversions,
metrics.clicks,
metrics.impressions,
metrics.ctr
metrics.impressions
FROM campaign
WHERE campaign.id = {campaign_id}
AND segments.date DURING THIS_MONTH
"""
try:
response = ga_service.search(customer_id=self.customer_id, query=query)
cost = conversions = clicks = impressions = 0
meta = {}
for row in response:
m = row.metrics
return {
"campaign_id": campaign_id,
"name": row.campaign.name,
"status": row.campaign.status.name,
"budget_daily": row.campaign_budget.amount_micros / 1_000_000,
"budget_resource_name": row.campaign_budget.resource_name,
"cost": m.cost_micros / 1_000_000,
"conversions": m.conversions,
"clicks": m.clicks,
"impressions": m.impressions,
"ctr": round(m.ctr * 100, 2),
}
cost += m.cost_micros / 1_000_000
conversions += m.conversions
clicks += m.clicks
impressions += m.impressions
if not meta:
meta = {
"name": row.campaign.name,
"status": row.campaign.status.name,
"budget_daily": row.campaign_budget.amount_micros / 1_000_000,
"budget_resource_name": row.campaign_budget.resource_name,
}
if not meta:
return {}
ctr = round(clicks / impressions * 100, 2) if impressions > 0 else 0.0
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:
print(f" ❌ Error Google Ads para campaña {campaign_id}: {e}")
return {}

11
run.py
View File

@ -225,10 +225,21 @@ def run():
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((
campaign["airtable_id"],
decision.get("consejo", ""),
CRITICIDAD_MAP[p],
log_text,
))
resumen.append({