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 <noreply@anthropic.com>
This commit is contained in:
Jose Manuel 2026-04-30 10:44:53 +02:00
parent d58448f698
commit 45e9515ae8
3 changed files with 80 additions and 7 deletions

View File

@ -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.

View File

@ -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")

42
run.py
View File

@ -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 = [