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]]:
|
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.
|
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)
|
1. GACampaignID / GoogleCampaignID (leads web normales)
|
||||||
2. gad_campaignid en attr_referer (UTM web)
|
2. gad_campaignid en attr_referer (UTM web)
|
||||||
3. attr_referer = campaign_name con UserAgent Google-Ads-Notifications (Lead Form)
|
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 por curso)
|
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()
|
now = datetime.now()
|
||||||
mes_inicio = f"{now.year}-{now.month:02d}-01"
|
mes_inicio = f"{now.year}-{now.month:02d}-01"
|
||||||
@ -353,8 +354,8 @@ class AirtableClient:
|
|||||||
if campaign_name else "FALSE()"
|
if campaign_name else "FALSE()"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extraer número de curso y tipo de fuente del nombre de campaña
|
|
||||||
curso_clause = "FALSE()"
|
curso_clause = "FALSE()"
|
||||||
|
leadform_cursoid_clause = "FALSE()"
|
||||||
m = re.search(r'fco_(?:search|pmx)_(\d+)', campaign_name, re.IGNORECASE)
|
m = re.search(r'fco_(?:search|pmx)_(\d+)', campaign_name, re.IGNORECASE)
|
||||||
if m:
|
if m:
|
||||||
course_num = m.group(1)
|
course_num = m.group(1)
|
||||||
@ -369,6 +370,10 @@ class AirtableClient:
|
|||||||
f"AND({{attr_cursoid}}='{course_num}',"
|
f"AND({{attr_cursoid}}='{course_num}',"
|
||||||
f"{{attr_utm_source}}='{utm_source}')"
|
f"{{attr_utm_source}}='{utm_source}')"
|
||||||
)
|
)
|
||||||
|
leadform_cursoid_clause = (
|
||||||
|
f"AND({{attr_cursoid}}='{course_num}',"
|
||||||
|
f"{{UserAgent del visitante}}='Google-Ads-Notifications')"
|
||||||
|
)
|
||||||
|
|
||||||
formula = (
|
formula = (
|
||||||
f"AND("
|
f"AND("
|
||||||
@ -377,7 +382,8 @@ class AirtableClient:
|
|||||||
f"FIND(',{campaign_id},',',' & {{GoogleCampaignID}} & ','),"
|
f"FIND(',{campaign_id},',',' & {{GoogleCampaignID}} & ','),"
|
||||||
f"FIND('gad_campaignid={campaign_id}',{{attr_referer}}),"
|
f"FIND('gad_campaignid={campaign_id}',{{attr_referer}}),"
|
||||||
f"{leadform_clause},"
|
f"{leadform_clause},"
|
||||||
f"{curso_clause}"
|
f"{curso_clause},"
|
||||||
|
f"{leadform_cursoid_clause}"
|
||||||
f"),"
|
f"),"
|
||||||
f"{{creado}}>='{mes_inicio}'"
|
f"{{creado}}>='{mes_inicio}'"
|
||||||
f")"
|
f")"
|
||||||
@ -386,18 +392,19 @@ class AirtableClient:
|
|||||||
ids = [r["id"] for r in records]
|
ids = [r["id"] for r in records]
|
||||||
return len(ids), ids
|
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.
|
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'
|
date_str: 'YYYY-MM-DD'
|
||||||
"""
|
"""
|
||||||
next_day = (datetime.strptime(date_str, "%Y-%m-%d") + timedelta(days=1)).strftime("%Y-%m-%d")
|
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}')"
|
formula = f"AND({{creado}}>='{date_str}',{{creado}}<'{next_day}')"
|
||||||
records = self.leads.all(
|
fields = ["GACampaignID", "GoogleCampaignID", "attr_referer"]
|
||||||
formula=formula,
|
if cursoid_to_campaign:
|
||||||
fields=["GACampaignID", "GoogleCampaignID", "attr_referer"],
|
fields += ["attr_cursoid", "UserAgent del visitante"]
|
||||||
)
|
records = self.leads.all(formula=formula, fields=fields)
|
||||||
counts: dict[str, int] = {}
|
counts: dict[str, int] = {}
|
||||||
for r in records:
|
for r in records:
|
||||||
f = r["fields"]
|
f = r["fields"]
|
||||||
@ -408,6 +415,10 @@ class AirtableClient:
|
|||||||
m = re.search(r"gad_campaignid=(\d+)", f.get("attr_referer", ""))
|
m = re.search(r"gad_campaignid=(\d+)", f.get("attr_referer", ""))
|
||||||
if m:
|
if m:
|
||||||
cid = m.group(1)
|
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:
|
if cid:
|
||||||
counts[cid] = counts.get(cid, 0) + 1
|
counts[cid] = counts.get(cid, 0) + 1
|
||||||
return counts
|
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()
|
monthly_metrics = gads.get_monthly_metrics_all()
|
||||||
today_metrics = gads.get_yesterday_metrics_all()
|
today_metrics = gads.get_yesterday_metrics_all()
|
||||||
_ayer_date = (datetime.now() - timedelta(days=1))
|
_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...")
|
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)
|
||||||
@ -165,6 +164,17 @@ def run():
|
|||||||
lf_conv = int(monthly_metrics.get(c["google_campaign_id"], {}).get("conversions", 0))
|
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
|
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 ===
|
# === PRIMERA PASADA: recopilar datos de todas las campañas ===
|
||||||
collected = []
|
collected = []
|
||||||
skipped = []
|
skipped = []
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user