Files
fn_registry/python/functions/browser/extract_cmp_tcf.md
T
egutierrez 763e06c127 feat(browser): auto-commit con 178 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-20 18:22:23 +02:00

12 KiB

name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, params_schema, tested, tests, test_file_path, file_path
name kind lang domain version purity signature description tags uses_functions uses_types returns returns_optional error_type imports params_schema tested tests test_file_path file_path
extract_cmp_tcf function py browser 1.3.0 impure def extract_cmp_tcf(url: str, *, port: int = 9222, wait_load_s: float = 7.0, settle_s: float = 5.0, timeout_s: float = 30.0, accept_first: bool = False, settle_accept_s: float = 4.0, llm_fallback: bool = False) -> dict Navega por CDP a un Chrome con remote debugging, detecta el CMP (Consent Management Platform: Didomi, OneTrust, Sourcepoint, Quantcast u otro TCF) de un sitio web y lee su objeto IAB TCF v2 para contar vendors (data brokers) y propositos declarados, mas detectar muro pago-o-consientes. Pensado para escanear masivamente periodicos espanoles y cruzar vendor IDs contra la GVL.
cdp
browser
consent
cmp
tcf
iab
privacy
data-broker
python
navegator
cdp_eval_py_browser
find_consent_controls_llm_py_browser
false error_go_core
json
os
sys
time
params output
name desc
url URL del sitio a escanear. Se navega la pestana activa del Chrome con remote debugging hacia esta URL.
name desc
port Puerto de remote debugging de Chrome. Default 9222. Usar 9333 para el Chrome aislado del MCP (NO 9222 si es el navegador personal del usuario).
name desc
wait_load_s Segundos a esperar tras navegar para que la pagina cargue. Default 7.0.
name desc
settle_s Segundos extra para que el CMP termine de inicializar antes de arrancar el volcado del TCF. Default 5.0. Subir (8-10) para CMPs lentos que inyectan __tcfapi de forma diferida.
name desc
timeout_s Timeout (segundos) para cada evaluacion CDP. Default 30.0.
name desc
accept_first Si True, antes de leer el TCData definitivo intenta ACEPTAR el banner de consentimiento (clic en 'aceptar todo': selectores conocidos de Didomi/OneTrust/Quantcast + fallback por texto del boton), espera settle_accept_s y re-ejecuta el volcado del TCF. Necesario para CMPs (Quantcast) que no exponen vendors pre-consent: devuelven consents/legitimateInterests vacios hasta que el usuario interactua. Default False (no toca el banner, comportamiento identico al historico).
name desc
settle_accept_s Segundos a esperar tras aceptar el banner para que el CMP re-emita el TCData poblado. Default 4.0. Solo aplica si accept_first=True.
name desc
llm_fallback Si True (y accept_first=True), SOLO cuando el intento normal de aceptar deja vendor_ids vacio tras leer el TCData recurre a find_consent_controls_llm (haiku, max_candidates=80) para localizar el control 'aceptar todo' que los selectores hardcodeados no encontraron, lo clica via cdp_eval, espera settle_accept_s y re-ejecuta el volcado del TCF. Default False (nunca llama al LLM, comportamiento identico). El LLM solo se invoca cuando hace falta de verdad: si el flujo de selectores/texto ya recupero vendors, NO gasta la llamada a ask_llm (ni siquiera cuando el clic salio 'no-button' pero habia vendors, p.ej. Didomi que expone getRequiredVendorIds sin consentir). Gotcha: cada sitio que dispare el fallback consume una llamada a ask_llm (rate limits).
dict plano. Caso ok: {status:'ok', url, final_url, title, cmp:'didomi'|'onetrust'|'sourcepoint'|'quantcast'|'otro_tcf'|'ninguno', cmp_id:int|None, tcf_policy:int|None, gdpr_applies:bool|None, n_vendors:int, n_vendors_total:int|None, n_vendors_required:int|None, n_purposes:int|None, tcstring_len:int, paywall_consent:bool, vendor_ids:[int]}. Cuando accept_first=True se anade ademas accept_method (str): lo que devolvio el JS de clic ('sel:<selector>', 'text:<texto>' o 'no-button'); si se dispara el fallback LLM pasa a 'llm:<selector>' (clic LLM exitoso) o 'llm:no-control' (el LLM no encontro control). Cuando se dispara el fallback LLM se anaden llm_used:True y llm_reason:str (la explicacion del locator); si llm_fallback=False o el flujo normal ya dio vendors, esos campos NO aparecen. vendor_ids es generico para cualquier CMP TCF v2: si Didomi expone los required ids se usan esos (lo que el sitio solicita); si no (Quantcast, Sourcepoint, otro_tcf) se usa la union de claves de tcData.vendor.consents + legitimateInterests. n_vendors = len(vendor_ids) cuando hay lista. Caso fallo: {status:'error', url, error:str}. Nunca lanza.
false
python/functions/browser/extract_cmp_tcf.py

Ejemplo

import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from browser.extract_cmp_tcf import extract_cmp_tcf

# Requiere un Chrome lanzado con --remote-debugging-port=9333 (el aislado del MCP).
res = extract_cmp_tcf("https://www.lavanguardia.com", port=9333)
print(res["status"], res["cmp"], res["n_vendors"], res["paywall_consent"])
# -> ok didomi 700 True   (recuentos reales varian por sitio/fecha)

Para CMPs que NO exponen vendors pre-consent (Quantcast), aceptar el banner primero:

# bolsamania.com / confilegal.com usan Quantcast: pre-consent dan 0 vendors.
res = extract_cmp_tcf("https://www.bolsamania.com", port=9335, accept_first=True)
print(res["cmp"], len(res["vendor_ids"]), res["accept_method"])
# -> quantcast 1613 sel:.qc-cmp2-summary-buttons button[mode=primary]

Para CMP con clases dinamicas / texto no estandar donde el clic por selectores sale no-button, activar el fallback LLM (haiku localiza el "aceptar todo"):

# Solo gasta ask_llm si el flujo de selectores fallo de verdad.
res = extract_cmp_tcf("https://www.periodistadigital.com", port=9335,
                      accept_first=True, llm_fallback=True)
print(res["accept_method"], res.get("llm_used"), len(res["vendor_ids"]))
# -> llm:[data-fnllm="3"] True 812   (si el LLM localizo el control)
# En sitios que ya dieron vendors por selector, llm_used NO aparece.

O directo por CLI: python3 python/functions/browser/extract_cmp_tcf.py "https://www.lavanguardia.com" 9333 (tercer arg 1/accept activa accept_first; cuarto arg 1/llm activa llm_fallback).

Cuando usarla

Cuando necesites auditar de forma masiva que data brokers (vendors IAB TCF) y propositos declara el banner de cookies de un sitio: escaneo de periodicos, paneles de prensa, o cualquier corpus de webs con muro de consentimiento. Devuelve un dict plano listo para volcar a una tabla (DuckDB / Excel) y cruzar vendor_ids contra la Global Vendor List. Usala como paso de captura dentro de un pipeline de escaneo; los vendor_ids enriquecidos con la GVL dan el nombre de cada data broker.

Gotchas

  • Requiere un Chrome lanzado con --remote-debugging-port=<port> y al menos una pestana de tipo page. Sin remote debugging, la navegacion falla y devuelve {"status":"error", ...}. NO usar el puerto 9222 si es el navegador personal (tiene sesiones del usuario abiertas): usar 9333, el Chrome aislado del MCP.
  • Navega la pestana activa (location.href = url) — reusa el target que elija cdp_eval (primer page). No abre pestana nueva; si necesitas aislar, abre una pestana dedicada antes.
  • El CMP puede tardar en inicializar. Si n_vendors sale 0 o cmp sale otro_tcf cuando esperabas Didomi, sube wait_load_s / settle_s — algunos sitios cargan el SDK del CMP de forma diferida.
  • El stub __tcfapi('getTCData', 2, cb) encola el callback hasta que el CMP real carga; por eso hay dos pasadas (arrancar volcado, luego leer window.__tcdump). Si el usuario aun no acepto el banner, los recuentos de vendor.consents pueden ser 0 pero vendor.legitimateInterests y el recuento de Didomi suelen estar poblados.
  • Headless puede ser detectado por algunos CMP (cambian comportamiento o no cargan). Para resultados fiables usar un Chrome con UI (el del MCP, 9333).
  • vendor_ids se obtiene de forma generica para cualquier CMP TCF v2: con Didomi se usan los getRequiredVendorIds() (lo que el sitio realmente solicita); con cualquier otro CMP (Quantcast, Sourcepoint, otro_tcf) se usa la union de claves de tcData.vendor.consents + tcData.vendor.legitimateInterests (los IDs del universo GVL que el CMP tiene configurado). Antes de v1.1.0 solo Didomi rellenaba vendor_ids; los demas CMP TCF quedaban con la lista vacia y n_vendors=0.
  • n_vendors = len(vendor_ids) cuando hay lista resuelta; si no, cae a la mejor estimacion didomi_required > didomi_total_vendors > n_vendor_li.
  • Si un sitio TCF sigue devolviendo vendor_ids vacio, casi siempre es porque el CMP inyecta __tcfapi de forma muy diferida: sube settle_s a 8-10 en esa llamada.
  • Quantcast (cmp_id 10) pre-consent devuelve TCData vacio: mientras el banner solo esta mostrado (eventStatus:"cmpuishown", tcString vacio), vendor.consents, vendor.legitimateInterests y vendor.disclosedVendors estan TODOS a 0 — no hay forma de leer vendors sin que el usuario interactue con el banner. En cuanto se acepta (o se rechaza) el banner, el TCData se puebla y la funcion extrae cientos/miles de vendor_ids correctamente (verificado: bolsamania.com pasa de 0 a 1613 vendors tras cerrar el banner). Didomi NO sufre esto: expone getRequiredVendorIds() aunque no haya consentimiento. Para escaneo masivo de sitios Quantcast, pasar accept_first=True (desde v1.2.0): la funcion acepta el banner por selector/texto antes de leer el TCF.
  • accept_first=True clica desde el documento PRINCIPAL: los selectores conocidos (#didomi-notice-agree-button, #onetrust-accept-btn-handler, .qc-cmp2-summary-buttons button[mode=primary], button[aria-label*=Aceptar/Accept]) y el fallback por texto del boton funcionan para Didomi, OneTrust y Quantcast porque renderizan el banner en el DOM de la pagina. Sourcepoint mete el banner dentro de un <iframe> (sp_message_container_*): el clic desde el documento principal NO alcanza el boton dentro del iframe, asi que accept_method saldra no-button para Sourcepoint y los vendors seguiran sin poblarse. No esta resuelto (no hay sitios Sourcepoint en el set actual); resolverlo requeriria evaluar el JS dentro del frame del iframe (otro target CDP). El parametro nunca lanza por esto: simplemente reporta no-button.
  • llm_fallback=True gasta una llamada a ask_llm (haiku) por cada sitio que lo dispare (rate limits de la API Anthropic). El fallback solo se invoca cuando el flujo normal de selectores fallo de verdad (vendor_ids vacio tras leer el TCData): los sitios cuyo CMP estandar (Didomi/OneTrust/Quantcast por selector o texto) ya recupera vendors NO gastan la llamada. Caso clave: Didomi expone getRequiredVendorIds() sin necesidad de consentir, asi que aunque el clic salga no-button el vendor_ids ya viene poblado y el LLM no se dispara. Para un escaneo masivo esto acota el gasto a los CMP con clases dinamicas / texto no estandar. El fallback marca accept_method='llm:<selector>' (clic LLM exitoso), o 'llm:no-control' si el LLM no encontro un boton aceptable / el clic fallo, y siempre anade llm_used:True + llm_reason. NO resuelve banners dentro de iframes (Sourcepoint): el LLM recolecta controles del documento principal, igual que el flujo de selectores.
  • Nunca lanza: cualquier error de red, CDP o parseo JSON se reporta en error con status="error".

Capability growth log

  • v1.3.0 (2026-06-18) — llm_fallback: si el clic por selectores falla (no-button), usa find_consent_controls_llm (haiku) para localizar y clicar 'aceptar todo' antes de leer el TCF. Gotcha: el fallback gasta una llamada a ask_llm (rate limits) por sitio que lo necesite.
  • v1.2.0 (2026-06-18) — accept_first: acepta el banner (Didomi/OneTrust/Quantcast por selector + fallback por texto) antes de leer el TCF, para CMPs que no exponen vendors pre-consent (Quantcast). Gotcha: Sourcepoint mete el banner en un iframe, el clic desde el documento principal no lo alcanza (sale 'no-button').
  • v1.1.0 (2026-06-18) — vendor_ids genericos desde tcData.vendor.consents/legitimateInterests para CMPs no-Didomi (Quantcast, otro_tcf); +settle para CMPs lentos.