From a1aef8f58ed1e8df06802fae7e768c4dfcb2ef9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20G=C3=B3mez?= Date: Sat, 25 Apr 2026 11:05:06 +0200 Subject: [PATCH] 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 --- airtable_client.py | 13 ++++++------- analyzer.py | 2 +- google_ads_client.py | 45 +++++++++++++++++++++++++++++--------------- run.py | 11 +++++++++++ 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/airtable_client.py b/airtable_client.py index 5943ff5..debd9e8 100644 --- a/airtable_client.py +++ b/airtable_client.py @@ -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) diff --git a/analyzer.py b/analyzer.py index e5f74d3..e77b328 100644 --- a/analyzer.py +++ b/analyzer.py @@ -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" diff --git a/google_ads_client.py b/google_ads_client.py index b479b6f..a10e1f4 100644 --- a/google_ads_client.py +++ b/google_ads_client.py @@ -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 {} diff --git a/run.py b/run.py index 73d619f..f3fd062 100644 --- a/run.py +++ b/run.py @@ -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({