--- name: extract_cmp_tcf kind: function lang: py domain: browser version: "1.3.0" purity: impure signature: "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" description: "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." tags: [cdp, browser, consent, cmp, tcf, iab, privacy, data-broker, python, navegator] uses_functions: [cdp_eval_py_browser, find_consent_controls_llm_py_browser] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: ["json", "os", "sys", "time"] params_schema: params: - name: url desc: "URL del sitio a escanear. Se navega la pestana activa del Chrome con remote debugging hacia esta URL." - name: port desc: "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: wait_load_s desc: "Segundos a esperar tras navegar para que la pagina cargue. Default 7.0." - name: settle_s desc: "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: timeout_s desc: "Timeout (segundos) para cada evaluacion CDP. Default 30.0." - name: accept_first desc: "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: settle_accept_s desc: "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: llm_fallback desc: "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)." output: "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:', 'text:' o 'no-button'); si se dispara el fallback LLM pasa a 'llm:' (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." tested: false tests: [] test_file_path: "" file_path: "python/functions/browser/extract_cmp_tcf.py" --- ## Ejemplo ```python 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: ```python # 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"): ```python # 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=` 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 `