Add 5th lead attribution path: cursoid + Google-Ads-Notifications UserAgent

Leadform leads in Airtable now counted for PMX campaigns via
attr_cursoid + UserAgent='Google-Ads-Notifications', both for
monthly totals and daily MetricasDiarias.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Manuel 2026-06-11 18:35:55 +02:00
parent c025a5f828
commit 170f0a3207
3 changed files with 130 additions and 12 deletions

View File

@ -339,11 +339,12 @@ class AirtableClient:
def get_leads_this_month_gads(self, campaign_id: str, campaign_name: str = "") -> tuple[int, list[str]]:
"""
Leads del mes actual para una campaña de Google Ads.
Cubre cuatro vías de atribución:
Cubre cinco vías de atribución:
1. GACampaignID / GoogleCampaignID (leads web normales)
2. gad_campaignid en attr_referer (UTM web)
3. attr_referer = campaign_name con UserAgent Google-Ads-Notifications (Lead Form)
4. attr_cursoid = course_num AND attr_utm_source = 'pmx'|'google' (atribución por curso)
3. attr_referer = campaign_name con UserAgent Google-Ads-Notifications (Lead Form por nombre)
4. attr_cursoid = course_num AND attr_utm_source = 'pmx'|'google' (atribución web por curso)
5. attr_cursoid = course_num AND UserAgent = 'Google-Ads-Notifications' (Lead Form por cursoid)
"""
now = datetime.now()
mes_inicio = f"{now.year}-{now.month:02d}-01"
@ -353,8 +354,8 @@ class AirtableClient:
if campaign_name else "FALSE()"
)
# Extraer número de curso y tipo de fuente del nombre de campaña
curso_clause = "FALSE()"
leadform_cursoid_clause = "FALSE()"
m = re.search(r'fco_(?:search|pmx)_(\d+)', campaign_name, re.IGNORECASE)
if m:
course_num = m.group(1)
@ -369,6 +370,10 @@ class AirtableClient:
f"AND({{attr_cursoid}}='{course_num}',"
f"{{attr_utm_source}}='{utm_source}')"
)
leadform_cursoid_clause = (
f"AND({{attr_cursoid}}='{course_num}',"
f"{{UserAgent del visitante}}='Google-Ads-Notifications')"
)
formula = (
f"AND("
@ -377,7 +382,8 @@ class AirtableClient:
f"FIND(',{campaign_id},',',' & {{GoogleCampaignID}} & ','),"
f"FIND('gad_campaignid={campaign_id}',{{attr_referer}}),"
f"{leadform_clause},"
f"{curso_clause}"
f"{curso_clause},"
f"{leadform_cursoid_clause}"
f"),"
f"{{creado}}>='{mes_inicio}'"
f")"
@ -386,18 +392,19 @@ class AirtableClient:
ids = [r["id"] for r in records]
return len(ids), ids
def get_leads_by_campaign_on_date(self, date_str: str) -> dict:
def get_leads_by_campaign_on_date(self, date_str: str, cursoid_to_campaign: dict = None) -> dict:
"""
Devuelve {google_campaign_id: count} para todos los leads de un día concreto.
Una sola llamada bulk para todos los campañas más eficiente que una por campaña.
Una sola llamada bulk para todas las campañas.
cursoid_to_campaign: {course_num: google_campaign_id} para atribuir leadforms por cursoid.
date_str: 'YYYY-MM-DD'
"""
next_day = (datetime.strptime(date_str, "%Y-%m-%d") + timedelta(days=1)).strftime("%Y-%m-%d")
formula = f"AND({{creado}}>='{date_str}',{{creado}}<'{next_day}')"
records = self.leads.all(
formula=formula,
fields=["GACampaignID", "GoogleCampaignID", "attr_referer"],
)
fields = ["GACampaignID", "GoogleCampaignID", "attr_referer"]
if cursoid_to_campaign:
fields += ["attr_cursoid", "UserAgent del visitante"]
records = self.leads.all(formula=formula, fields=fields)
counts: dict[str, int] = {}
for r in records:
f = r["fields"]
@ -408,6 +415,10 @@ class AirtableClient:
m = re.search(r"gad_campaignid=(\d+)", f.get("attr_referer", ""))
if m:
cid = m.group(1)
if not cid and cursoid_to_campaign:
if f.get("UserAgent del visitante") == "Google-Ads-Notifications":
cursoid = str(f.get("attr_cursoid") or "").strip()
cid = cursoid_to_campaign.get(cursoid, "")
if cid:
counts[cid] = counts.get(cid, 0) + 1
return counts

View File

@ -0,0 +1,97 @@
"""
Recalcula leads_lake en MetricasDiarias para días 8-10 de junio 2026
añadiendo la atribución de leadforms via cursoid + UserAgent.
También actualiza ConvLeadsLakeMes, Leads Lake y ConvLeadsLakeMesFinal.
"""
import json
import re
from datetime import datetime
from airtable_client import AirtableClient
DAYS = ["2026-06-08", "2026-06-09", "2026-06-10"]
def _course_num(name: str) -> str | None:
m = re.search(r'fco_(?:search|pmx)_(\d+)', name, re.IGNORECASE)
return m.group(1) if m else None
def run():
at = AirtableClient()
campaigns = at.get_active_gacampaignmes()
print(f"{len(campaigns)} campañas activas este mes\n")
# Mapping cursoid → PMX campaign_id para leadforms diarios
cursoid_to_campaign: dict[str, str] = {}
for c in campaigns:
num = _course_num(c["curso"])
if num and "pmx" in c["curso"].lower() and "_leadform" not in c["curso"].lower():
cursoid_to_campaign[num] = c["google_campaign_id"]
print(f"→ Mapping cursoid→campaign: {len(cursoid_to_campaign)} entradas")
# Obtener leads por campaña para cada día (una llamada bulk por día)
daily_counts: dict[str, dict[str, int]] = {}
for date_str in DAYS:
daily_counts[date_str] = at.get_leads_by_campaign_on_date(date_str, cursoid_to_campaign)
total = sum(daily_counts[date_str].values())
print(f" {date_str}: {total} leads totales encontrados")
print()
# Actualizar MetricasDiarias para cada campaña
metricas_updates = []
for campaign in campaigns:
cid = campaign["google_campaign_id"]
try:
md = json.loads(campaign["metricas_diarias"])
except (json.JSONDecodeError, TypeError):
md = {}
changed = False
for date_str in DAYS:
day_key = date_str[8:10]
if day_key not in md:
continue
new_count = daily_counts[date_str].get(cid, 0)
old_count = md[day_key].get("leads_lake", 0)
if old_count != new_count:
print(f" {campaign['curso'][:45]} día {day_key}: leads_lake {old_count}{new_count}")
md[day_key]["leads_lake"] = new_count
# Recalcular ingreso_lxp para el día (leads_lake × PPL)
md[day_key]["ingreso"] = round(new_count * campaign["ppl"], 2)
changed = True
if changed:
metricas_updates.append({
"airtable_id": campaign["airtable_id"],
"metricas_json": json.dumps(md, ensure_ascii=False),
})
if metricas_updates:
print(f"\n→ Actualizando MetricasDiarias ({len(metricas_updates)} registros)...")
at.batch_update_metricas_diarias(metricas_updates)
print(" ✓ MetricasDiarias actualizado.")
else:
print(" ✓ MetricasDiarias sin cambios.")
# Recalcular totales mensuales con la nueva atribución
print("\n→ Recalculando totales mensuales (leads_lake acumulado del mes)...")
final_leads_data = []
for campaign in campaigns:
cid = campaign["google_campaign_id"]
leads, lead_ids = at.get_leads_this_month_gads(cid, campaign["curso"])
at.update_gacampaignmes_leads_lake(campaign["airtable_id"], lead_ids)
final_leads_data.append({
"airtable_id": campaign["airtable_id"],
"conv_leads_lake_mes": leads,
"conv_leads_lake_mes_grupo": leads,
})
if leads > 0:
print(f" {campaign['curso'][:45]}: {leads} leads")
at.batch_update_gacampaignmes_final_leads(final_leads_data)
print(" ✓ ConvLeadsLakeMesFinal actualizado.")
if __name__ == "__main__":
run()

12
run.py
View File

@ -101,7 +101,6 @@ def run():
monthly_metrics = gads.get_monthly_metrics_all()
today_metrics = gads.get_yesterday_metrics_all()
_ayer_date = (datetime.now() - timedelta(days=1))
leads_yesterday = at.get_leads_by_campaign_on_date(_ayer_date.strftime("%Y-%m-%d"))
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)
@ -165,6 +164,17 @@ def run():
lf_conv = int(monthly_metrics.get(c["google_campaign_id"], {}).get("conversions", 0))
leadform_conv_by_course[num] = leadform_conv_by_course.get(num, 0) + lf_conv
# Mapping cursoid → PMX campaign_id para atribuir leadforms diarios por cursoid
cursoid_to_campaign: dict[str, str] = {}
for c in campaigns:
num = _course_num(c["curso"])
if num and "pmx" in c["curso"].lower() and "_leadform" not in c["curso"].lower():
cursoid_to_campaign[num] = c["google_campaign_id"]
leads_yesterday = at.get_leads_by_campaign_on_date(
_ayer_date.strftime("%Y-%m-%d"), cursoid_to_campaign
)
# === PRIMERA PASADA: recopilar datos de todas las campañas ===
collected = []
skipped = []