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:
parent
c025a5f828
commit
170f0a3207
@ -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
|
||||
|
||||
97
backfill_leadform_jun8_10.py
Normal file
97
backfill_leadform_jun8_10.py
Normal 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
12
run.py
@ -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 = []
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user