From 45e9515ae80f1a72a2ff52e11be3a3c4403e04f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20G=C3=B3mez?= Date: Thu, 30 Apr 2026 10:44:53 +0200 Subject: [PATCH] Add daily metrics snapshot (MetricasDiarias) to GACampaignMes Each optimizer run writes yesterday's cost, revenue (conversions x PPL) and margin per campaign as a JSON entry keyed by day number into the MetricasDiarias field. Yesterday is used because Google Ads data for the current day is not yet available. Co-Authored-By: Claude Sonnet 4.6 --- airtable_client.py | 14 ++++++++++++++ google_ads_client.py | 31 +++++++++++++++++++++++++++++++ run.py | 42 +++++++++++++++++++++++++++++++++++------- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/airtable_client.py b/airtable_client.py index 0f7fe19..a8d26fc 100644 --- a/airtable_client.py +++ b/airtable_client.py @@ -1,5 +1,6 @@ from pyairtable import Api from datetime import datetime +import json import config @@ -307,6 +308,7 @@ class AirtableClient: "cpa_maximo": float(f.get("CPAMax") or 0), "margen_objetivo": float(f.get("MargenObjetivo") or 0), "conv_leads_lake_mes": int(f.get("ConvLeadsLakeMes") or 0), + "metricas_diarias": f.get("MetricasDiarias") or "{}", }) return result @@ -377,6 +379,18 @@ class AirtableClient: for i in range(0, len(batch), 10): self.gacampaignmes.batch_update(batch[i:i+10]) + def batch_update_metricas_diarias(self, updates: list[dict]) -> None: + """ + Actualiza MetricasDiarias en GACampaignMes. + updates: lista de {airtable_id, metricas_json (str)} + """ + batch = [ + {"id": u["airtable_id"], "fields": {"MetricasDiarias": u["metricas_json"]}} + for u in updates + ] + 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]) -> None: """ Actualiza en lote los campos 'Consejo', 'Criticidad' y 'Log' de GACampaignMes. diff --git a/google_ads_client.py b/google_ads_client.py index a10e1f4..165ac1a 100644 --- a/google_ads_client.py +++ b/google_ads_client.py @@ -1,5 +1,6 @@ from google.ads.googleads.client import GoogleAdsClient as GAdsClient from google.ads.googleads.errors import GoogleAdsException +from datetime import datetime import config @@ -69,6 +70,36 @@ class GoogleAdsClient: print(f" ❌ Error obteniendo métricas mensuales: {e}") return result + def get_yesterday_metrics_all(self) -> dict: + """ + Devuelve métricas del día anterior para TODAS las campañas en una sola query. + Retorna dict {campaign_id: {conversions, cost}}. + """ + from datetime import timedelta + yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") + ga_service = self.client.get_service("GoogleAdsService") + query = f""" + SELECT + campaign.id, + metrics.conversions, + metrics.cost_micros + FROM campaign + WHERE campaign.status != 'REMOVED' + AND segments.date = '{yesterday}' + """ + result = {} + try: + response = ga_service.search(customer_id=self.customer_id, query=query) + for row in response: + cid = str(row.campaign.id) + result[cid] = { + "conversions": row.metrics.conversions, + "cost": row.metrics.cost_micros / 1_000_000, + } + except GoogleAdsException as e: + print(f" ❌ Error obteniendo métricas de hoy: {e}") + return result + def get_campaign_metrics(self, campaign_id: str) -> dict: """Métricas del mes en curso para una campaña concreta (acumulado mensual).""" ga_service = self.client.get_service("GoogleAdsService") diff --git a/run.py b/run.py index 10eb51f..10feebb 100644 --- a/run.py +++ b/run.py @@ -2,6 +2,7 @@ import sys import io import os import re +import json sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", line_buffering=True) from airtable_client import AirtableClient @@ -97,6 +98,7 @@ def run(): print("→ Sincronizando campañas desde Google Ads...") google_campaigns = gads.get_all_campaigns() monthly_metrics = gads.get_monthly_metrics_all() + today_metrics = gads.get_yesterday_metrics_all() print(" Calculando PPL y CapTotalMes...") ppl_lookup, cap_lookup = at.build_campaign_lookups() sync_result = at.sync_campaigns_from_google_ads(google_campaigns, monthly_metrics, ppl_lookup, cap_lookup) @@ -194,6 +196,7 @@ def run(): "metrics": metrics, "analysis": analysis, "decision": decision, + "today_metrics": today_metrics.get(cid, {}), }) # Actualizar status en GACampaignMes y Google Ads Campaigns @@ -211,16 +214,20 @@ def run(): # === SEGUNDA PASADA: imprimir en orden + aplicar decisiones === resumen = [] - advice_updates = [] # (gcm_record_id, consejo, criticidad) para batch update final + advice_updates = [] # (gcm_record_id, consejo, criticidad) para batch update final + metricas_updates = [] # {airtable_id, metricas_json} para MetricasDiarias + from datetime import timedelta + dia_hoy = (datetime.now() - timedelta(days=1)).strftime("%d") last_priority = -1 for item in collected: - campaign = item["campaign"] - leads = item["leads"] - metrics = item["metrics"] - analysis = item["analysis"] - decision = item["decision"] - cid = campaign["google_campaign_id"] + campaign = item["campaign"] + leads = item["leads"] + metrics = item["metrics"] + analysis = item["analysis"] + decision = item["decision"] + today_m = item["today_metrics"] + cid = campaign["google_campaign_id"] p = _priority(item) if p != last_priority: @@ -283,6 +290,21 @@ def run(): elif course_num in courses_with_leadform: log_text += " | ⚠️ LEADFORM COMPANION: existe una campaña _leadform activa para este curso — parte de las conversiones de Google pueden provenir de leads capturados directamente en Google" + # Métricas diarias: coste hoy, ingreso (conversiones × PPL) y margen + coste_hoy = round(today_m.get("cost", 0), 2) + conv_hoy = today_m.get("conversions", 0) + ingreso_hoy = round(conv_hoy * campaign["ppl"], 2) + margen_hoy = round(ingreso_hoy - coste_hoy, 2) + try: + md = json.loads(campaign["metricas_diarias"]) + except (json.JSONDecodeError, TypeError): + md = {} + md[dia_hoy] = {"coste": coste_hoy, "ingreso": ingreso_hoy, "margen": margen_hoy} + metricas_updates.append({ + "airtable_id": campaign["airtable_id"], + "metricas_json": json.dumps(md, ensure_ascii=False), + }) + advice_updates.append(( campaign["airtable_id"], decision.get("consejo", ""), @@ -310,6 +332,12 @@ def run(): at.batch_update_gacampaignmes_advice(advice_updates) print(" ✓ Consejos y criticidad guardados.") + # Guardar métricas diarias en MetricasDiarias + if metricas_updates: + print(f"→ Actualizando MetricasDiarias ({len(metricas_updates)} registros)...") + at.batch_update_metricas_diarias(metricas_updates) + print(" ✓ MetricasDiarias actualizado.") + # Snapshot diario: ConvLeadsLakeMesFinal + ConvLeadsLakeMesGrupo # PMX con companion Search → Grupo = conversiones Google (ya calculado en leads_grupo) final_leads_data = [