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:
parent
d58448f698
commit
45e9515ae8
@ -1,5 +1,6 @@
|
|||||||
from pyairtable import Api
|
from pyairtable import Api
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import json
|
||||||
import config
|
import config
|
||||||
|
|
||||||
|
|
||||||
@ -307,6 +308,7 @@ class AirtableClient:
|
|||||||
"cpa_maximo": float(f.get("CPAMax") or 0),
|
"cpa_maximo": float(f.get("CPAMax") or 0),
|
||||||
"margen_objetivo": float(f.get("MargenObjetivo") or 0),
|
"margen_objetivo": float(f.get("MargenObjetivo") or 0),
|
||||||
"conv_leads_lake_mes": int(f.get("ConvLeadsLakeMes") or 0),
|
"conv_leads_lake_mes": int(f.get("ConvLeadsLakeMes") or 0),
|
||||||
|
"metricas_diarias": f.get("MetricasDiarias") or "{}",
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -377,6 +379,18 @@ 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_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:
|
def batch_update_gacampaignmes_advice(self, updates: list[tuple]) -> None:
|
||||||
"""
|
"""
|
||||||
Actualiza en lote los campos 'Consejo', 'Criticidad' y 'Log' de GACampaignMes.
|
Actualiza en lote los campos 'Consejo', 'Criticidad' y 'Log' de GACampaignMes.
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from google.ads.googleads.client import GoogleAdsClient as GAdsClient
|
from google.ads.googleads.client import GoogleAdsClient as GAdsClient
|
||||||
from google.ads.googleads.errors import GoogleAdsException
|
from google.ads.googleads.errors import GoogleAdsException
|
||||||
|
from datetime import datetime
|
||||||
import config
|
import config
|
||||||
|
|
||||||
|
|
||||||
@ -69,6 +70,36 @@ class GoogleAdsClient:
|
|||||||
print(f" ❌ Error obteniendo métricas mensuales: {e}")
|
print(f" ❌ Error obteniendo métricas mensuales: {e}")
|
||||||
return result
|
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:
|
def get_campaign_metrics(self, campaign_id: str) -> dict:
|
||||||
"""Métricas del mes en curso para una campaña concreta (acumulado mensual)."""
|
"""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")
|
||||||
|
|||||||
28
run.py
28
run.py
@ -2,6 +2,7 @@ import sys
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", line_buffering=True)
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", line_buffering=True)
|
||||||
|
|
||||||
from airtable_client import AirtableClient
|
from airtable_client import AirtableClient
|
||||||
@ -97,6 +98,7 @@ def run():
|
|||||||
print("→ Sincronizando campañas desde Google Ads...")
|
print("→ Sincronizando campañas desde Google Ads...")
|
||||||
google_campaigns = gads.get_all_campaigns()
|
google_campaigns = gads.get_all_campaigns()
|
||||||
monthly_metrics = gads.get_monthly_metrics_all()
|
monthly_metrics = gads.get_monthly_metrics_all()
|
||||||
|
today_metrics = gads.get_yesterday_metrics_all()
|
||||||
print(" Calculando PPL y CapTotalMes...")
|
print(" Calculando PPL y CapTotalMes...")
|
||||||
ppl_lookup, cap_lookup = at.build_campaign_lookups()
|
ppl_lookup, cap_lookup = at.build_campaign_lookups()
|
||||||
sync_result = at.sync_campaigns_from_google_ads(google_campaigns, monthly_metrics, ppl_lookup, cap_lookup)
|
sync_result = at.sync_campaigns_from_google_ads(google_campaigns, monthly_metrics, ppl_lookup, cap_lookup)
|
||||||
@ -194,6 +196,7 @@ def run():
|
|||||||
"metrics": metrics,
|
"metrics": metrics,
|
||||||
"analysis": analysis,
|
"analysis": analysis,
|
||||||
"decision": decision,
|
"decision": decision,
|
||||||
|
"today_metrics": today_metrics.get(cid, {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
# Actualizar status en GACampaignMes y Google Ads Campaigns
|
# Actualizar status en GACampaignMes y Google Ads Campaigns
|
||||||
@ -212,6 +215,9 @@ def run():
|
|||||||
# === SEGUNDA PASADA: imprimir en orden + aplicar decisiones ===
|
# === SEGUNDA PASADA: imprimir en orden + aplicar decisiones ===
|
||||||
resumen = []
|
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
|
last_priority = -1
|
||||||
|
|
||||||
for item in collected:
|
for item in collected:
|
||||||
@ -220,6 +226,7 @@ def run():
|
|||||||
metrics = item["metrics"]
|
metrics = item["metrics"]
|
||||||
analysis = item["analysis"]
|
analysis = item["analysis"]
|
||||||
decision = item["decision"]
|
decision = item["decision"]
|
||||||
|
today_m = item["today_metrics"]
|
||||||
cid = campaign["google_campaign_id"]
|
cid = campaign["google_campaign_id"]
|
||||||
|
|
||||||
p = _priority(item)
|
p = _priority(item)
|
||||||
@ -283,6 +290,21 @@ def run():
|
|||||||
elif course_num in courses_with_leadform:
|
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"
|
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((
|
advice_updates.append((
|
||||||
campaign["airtable_id"],
|
campaign["airtable_id"],
|
||||||
decision.get("consejo", ""),
|
decision.get("consejo", ""),
|
||||||
@ -310,6 +332,12 @@ def run():
|
|||||||
at.batch_update_gacampaignmes_advice(advice_updates)
|
at.batch_update_gacampaignmes_advice(advice_updates)
|
||||||
print(" ✓ Consejos y criticidad guardados.")
|
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
|
# Snapshot diario: ConvLeadsLakeMesFinal + ConvLeadsLakeMesGrupo
|
||||||
# PMX con companion Search → Grupo = conversiones Google (ya calculado en leads_grupo)
|
# PMX con companion Search → Grupo = conversiones Google (ya calculado en leads_grupo)
|
||||||
final_leads_data = [
|
final_leads_data = [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user