Resumen rápido
| Canal |
North star |
Fuente |
Script |
Método |
| Google Ads | CPSQL | Supermetrics AW · 3453900141 | pull-google-ads.py | Auto |
| LinkedIn Ads | CPSQL | Supermetrics LIA · 509238676 | pull-linkedin-ads.py | Auto |
| SEO / Orgánico | Clicks | GSC API + GAWA GA4 PRO | pull-seo.py | Semi |
| LI Orgánico | Eng. rate | Supermetrics LIP | pull-social.py | Auto |
| Instagram | Reach | Supermetrics IGI | pull-social.py | Auto |
| YouTube | Avg view % | Supermetrics YT2 | pull-social.py | Auto |
| Web Analytics | generate_lead (solo SQL) | GA4 Data API · SA calero-marketing-analytics + HubSpot | pull-web-analytics.py | Auto |
| Eventos | Asistentes | HubSpot marketing/v3 + forecast costes | pull-events.py | Semi |
| Outbound | Aprobadas | TheirStack + IA classifier (4 CSVs) | build-outbound.py | Semi |
| Brand | Branded clicks | GSC (SA clawdbot) + GA4 Direct (SA mkt) | pull-brand.py | Auto |
| AEO | Citation SoV | Tool propia (export .md) · legacy: Bing WMT + HubSpot AEO beta | import-aeo-tool.py | Manual |
| Influshakers | Empleados posteando | xlsx equipo Social Selling | import-social-selling.py | Manual |
| PR | Clippings | 4 xlsx de agencia (PT·ES·IT·FR) | import-pr.py | Manual |
Conector
Supermetrics — Google Ads (AW)
account: 3453900141
Script de pull
deploy/pull-google-ads.py --month YYYY-MM
Script de validación
deploy/validate-google-ads.py — gate con 6 métricas + tolerancias
Output
data/google-ads/monthly/YYYY-MM.json
Credenciales requeridas
| Variable | Descripción | Obligatoriedad |
SUPERMETRICS_API_KEY | API key de Supermetrics (cuenta Shakers) | OBLIGATORIO |
CLOUDFLARE_API_TOKEN | Solo para el deploy final | OBLIGATORIO (deploy) |
Proceso mensual
- Ejecutar
bash deploy/build-and-deploy.sh el día 1 del mes.
- El orquestador llama a
pull-google-ads.py con el mes anterior.
- El script lanza dos queries Supermetrics AW: métricas de campaña (QA/QC) y AllConversions por ConversionTypeName (QG). QG alimenta
daily_conversion_breakdown, capi_conversions, prev_capi_conversions y prev_micro_conversions. Los filtros de string de Supermetrics están rotos — todo el filtrado por prefijo/acción se hace en el script Python.
validate-google-ads.py verifica 6 métricas (spend, clicks, sql, cpsql, mql, impr). Si falla, el pipeline se detiene.
- Orquestador actualiza
manifest.json y hub-summary.json. Este archivo tiene estructura mensual: months.{YYYY-MM}.channels.{canal}. No hay una clave plana channels en la raíz.
- Deploy automático a Cloudflare Pages.
Solo cuenta AW 3453900141. No usar otras cuentas. CPSQL no va en el JSON: se calcula client-side como spend/sql. Brand/NB split 100% client-side (filtros de string Supermetrics están rotos).
Contrato de datos (schema v1.0)
meta { account, period, window, currency, tz, north_star, source, sql_conversion_action, refreshed_at, schema_version }
hero { spend, clicks, sql, mql, mql_status }
trend [ { label, cpsql, n } ]
brand_split [ { type, spend, clicks, mql, sql } ]
country [ { flag, name, spend, sql } ]
device [ { device, spend, clicks, sql } ]
campaigns [ { name, type, spend, sql, is, is_lost_budget, is_lost_rank, status, dot } ]
terms [ { q, type, spend, clicks, sql, qs, action } ]
ads [ { ad, campaign, type, type_class, impr, clicks, ctr } ]
health [ { scope, signal, value, status } ]
daily_conversion_breakdown [ { date, action, conversions } ] ← micro-conv. secundarias del mes (Landings IT, Landings soluciones, Form Contrata Talento)
capi_conversions [ { action, conversions } ] ← HubSpot-*-2026 del mes, excluye HubSpot-sql-2026 (conversiones CAPI)
prev_capi_conversions [ { action, conversions } ] ← ídem mes anterior
prev_micro_conversions [ { action, conversions } ] ← micro-conv. mes anterior, excluye HubSpot-sql-2026
supply { spend, clicks, impr, ctr, cpc, registrations, registrations_from_supply_campaigns,
registrations_from_demand_campaigns, mql_form, mql_form_by_campaign,
campaigns:[ { name, spend, clicks, impr, ctr, registrations, mql_form } ],
registrations_by_campaign:[ { campaign, registrations, supply_campaign } ], actions }
prev_supply { label, spend, clicks, registrations, mql_form } ← MoM de la pestaña Supply
Pestaña 0 · Inversión y objetivos (desde agosto 2026)
Primer panel de la ficha: reparte todo el gasto del canal entre los dos lados y cuelga de cada uno sus objetivos con la misma lente (CRM origen). Demand: SQL y MQL. Supply: registros y MQL form. Los dos bloques suman exactamente la inversión total del mes.
La campaña de Brand sirve a ambos, así que se prorratea su inversión y se atribuyen sus resultados a cada lado. Base del reparto: sesiones por intención de la landing en GA4 (SUPPLY_PATH_TOKENS: /register/talent, /freelance, /unirme-a-shakers, /empieza-a-trabajar…), con las sesiones sin landing identificable repartidas pro-rata. En julio 2026 salió 87% demand / 13% supply, es decir €1.552 y €231 de los €1.783 de Brand.
Parte del volumen no tiene campaña asignable. En julio 2026 llegaban a HubSpot sin nombre de campaña (
Auto-tagged PPC) 38 de 88 registros y 11 de 17 SQL; tras la recuperación por gclid quedan
19 y 3. Diagnóstico completo, evidencia y plan de reparación en
Reparación de tracking.
El reparto va por tráfico, nunca por conversiones. El gasto se consume en clics; prorratear por resultado haría circular el coste unitario que se quiere medir (la campaña que más produce de algo se llevaría más coste de ese algo, igualando los CPL artificialmente). La simetría inversa se declara pero no se reparte: en julio, PMax_Supply originó 3 SQL de demand (2 directos y 1 recuperado por gclid).
budget_objectives { spend_total, lens,
demand: { spend_own, spend_from_shared, spend_total, objectives:[{label,value,from_shared}] },
supply: { … }, shared_split: { campaign, sessions_supply, sessions_demand,
sessions_unclassified, supply_share, basis }, supply_to_demand_sql, unresolved }
hs_sql_by_campaign [ { campaign, sql } ] ← SQL CRM del mes por campaña de primer contacto
Recuperación de campaña por gclid (desde agosto 2026)
Cuando el visitante acepta el banner de cookies después de navegar, HubSpot arranca su seguimiento en una URL que ya no lleva hstk_campaign y escribe Auto-tagged PPC en lugar del nombre de campaña. El clic sí queda guardado en hs_google_click_id, así que el pull traduce gclid → campaña contra el recurso click_view de la Google Ads API.
Dos pasadas: primero el día de la primera visita que registra HubSpot y, para los que no dan match, un barrido del resto de la ventana (el clic puede ser bastante anterior al consentimiento). Se aplica solo a la lente first-touch, que es la que usa la campaña, y solo al mes del informe: el mes anterior se baja para el MoM pero ahí la campaña no se usa. La campaña recuperada entra en hs_sql_by_campaign, en el origen supply y en el panel de inversión.
Ventana de 90 días y un solo día por consulta. click_view obliga a filtrar por una fecha concreta y solo guarda 90 días. Julio 2026 caduca el 2026-10-28. Si un mes se cierra tarde, esta recuperación deja de estar disponible y el volumen se queda sin campaña de forma definitiva.
El gclid no se persiste nunca. Se usa en memoria durante el pull; el JSON solo lleva agregados por campaña y contadores. Los nombres de campaña se canonizan contra los de la cuenta (canon_campaign) porque HubSpot los guarda en minúsculas y la API devuelve el caso real: sin eso, la misma campaña generaba dos claves y las lentes que indexan por nombre perdían filas.
gclid_recovery { source, lens, mode, api_enabled, api_disabled_reason,
offline_map_entries, window_days, expires:{first_day,last_day},
sql: { total, unresolved, unresolved_with_gclid, unresolved_without_gclid,
recovered, still_unresolved, out_of_window, by_campaign:[{campaign,count}] },
supply: { … } }
# credenciales (fuera del repo, por variable de entorno)
GOOGLE_ADS_DEVELOPER_TOKEN obligatoria
GOOGLE_ADS_OAUTH_FILE ruta al JSON de OAuth de usuario
GOOGLE_ADS_LOGIN_CUSTOMER_ID opcional (MCC)
GOOGLE_ADS_API_VERSION opcional; por defecto prueba v21 → v20 → v19
# flags
--skip-gclid omite la recuperación
--gclid-map FILE mapa {gclid: campaña} precalculado; se usa antes de llamar a la API
Sin credenciales el pull no falla: desactiva la recuperación, lo deja escrito en api_disabled_reason y publica los contadores con lo que haya. La ficha lo refleja en el panel de Salud Operativa.
Pestaña 2 · Supply (desde agosto 2026)
Supply salió de la pestaña de Demand y tiene ficha propia con dos señales que no se suman entre sí porque no valen lo mismo:
- Registro de talento (mayor valor): alta real de usuario, CAPI
HubSpot-supply-registro-2026, atribución last-click. KPI principal de la pestaña.
- MQL form supply (menor valor): conversión nativa Google Ads/GTM (
Supply MQL [ES]), solo interés, sin registro detrás.
Los costes por conversión se calculan solo sobre campañas supply (token SUPPLY en el nombre), no sobre el total del canal: la mayoría de los registros los captura Brand por último clic y ese volumen no tiene coste supply asignable. La ficha muestra el reparto explícito en el aviso de atribución.
Tres lentes de atribución, cada una con un uso y ninguna intercambiable:
- CRM origen (HubSpot first-touch +
lead_status_supply): base de gestión y de los CPL. Cuenta contactos reales y separa User (registro) de MQL (form).
- GA4 (
registro_talent / supply_form por campaña de sesión y de primer contacto, más sesiones por campaña): comportamiento real en la web.
- CAPI Google (last-click 30d con modelado): solo para las pujas del algoritmo. En julio 2026 reportó 292,9 registros contra 88 del CRM (3,3x).
Coste asignable: solo el gasto de campañas SUPPLY. Brand y las campañas de demand también originan registros de talento, pero su gasto sirve a los dos lados y no se reparte — se listan en demand_capture marcados como coste compartido. HubSpot etiqueta como Paid Search también Microsoft Ads, y deja parte del tráfico auto-tagged sin nombre de campaña: ambos se clasifican aparte (other_platform, unresolved) y no se dan por buenos.
La ficha añade una tabla de calidad de tráfico (clics facturados vs sesiones GA4). Sirve para detectar campañas que pagan clics que nunca llegan a la web: en julio 2026, la Demand Gen de supply facturó 8.543 clics y generó 416 sesiones (4,9%), así que su CPC real por sesión es €1,08 y no €0,05.
Requiere GA4_SA_FILE (SA calero-marketing-analytics, property 304414381) además de las credenciales habituales. Se puede omitir con --skip-attrib.
El gasto supply queda fuera del hero de Demand y por tanto del CPL/CPSQL de la pestaña 1. build_prev_period() aplica el mismo filtro, así que el MoM de Demand es demand contra demand (corregido 2026-08-03; antes comparaba contra un mes anterior con supply dentro e inflaba CTR y clics).
Panel Conversiones secundarias · en observación: se renderiza dinámicamente por mes a partir de estos 4 campos. Se oculta si ambos grupos (CAPI + micro) están vacíos. Los deltas usan prevLabel (calculado desde pp.label del periodo anterior, no hardcoded).
Conector
Supermetrics — LinkedIn Ads (LIA)
account: 509238676
Script de pull
deploy/pull-linkedin-ads.py --month YYYY-MM
Output
data/linkedin-ads/monthly/YYYY-MM.json
Pipeline HubSpot
Lee deals en HubSpot. closedwon = Rechazado (no ganado). Ganado = dealstage Active (1221750) o posterior.
Credenciales requeridas
| Variable | Descripción | Obligatoriedad |
SUPERMETRICS_API_KEY | API key de Supermetrics | OBLIGATORIO |
Proceso mensual
- Incluido en
build-and-deploy.sh. Se ejecuta automáticamente.
- El script extrae spend, impresiones, clics, CTR y conversiones SQL/MQL de la cuenta LIA.
- El split Supply/Brand se determina client-side en el HTML por tokens en el nombre de campaña (
SUPPLY, TALENT, CG-PEOPLE).
- Sin validador dedicado. Si los valores son nulos el HTML muestra
—.
Filtros de string en Supermetrics LIA están rotos. Devuelven vacío sin error. Todo el split Brand/Supply es client-side. Nunca filtrar por nombre de campaña en el query de Supermetrics.
deal amount = GMV, no revenue. Revenue ≈ 20% del GMV.
Supply: registro no es formulario (desde agosto 2026)
LinkedIn tiene dos conversiones distintas en las campañas de captación de talento y hasta el 2026-08-03 el script las sumaba en un solo campo (talent_registrations). Esa métrica inflaba el volumen y hundía el coste por registro. Ahora se separan con la misma semántica que lead_status_supply en HubSpot y que el report de Google Ads:
| Bucket | Regla sobre el nombre de la conversión | Qué es |
registros | contiene REGISTRATION o REGISTRO | Alta real de talento en la plataforma. Equivale a lead_status_supply = User. |
forms | contiene MQL o FORM | Formulario de interés sin registro detrás. Equivale a lead_status_supply = MQL. |
otros | cualquier otro nombre | No se descarta: se publica en conversions_by_name y sube una fila de salud, para que un nombre nuevo no desaparezca en silencio. |
Ene-Jun 2026 están reexpresados (2026-08-03). Cada uno de esos meses lleva un bloque restatement con lo publicado y lo reexpresado, y una fila de salud. El demand no cambió en ningún mes. Junio pasó de 143 conversiones a €17,10 a 19 registros a €128,67 más 127 formularios a €19,25.
LinkedIn reatribuye meses cerrados. Repullear Feb y Abr hoy devuelve menos conversiones supply que el pull original del 2026-06-23 (169 vs 299 y 347 vs 524). Donde pasa, el mes lleva restatement.vintage_note y una fila de salud de Vintage. No repullear meses cerrados sin necesidad: borra los parches manuales.
Cross-check CRM (manual)
El bloque crm_crosscheck contrasta lo que declara LinkedIn contra lead_status_supply en HubSpot. Es manual y cada pull lo deja vacío. No está automatizado porque HubSpot no recibe la campaña de LinkedIn: hs_analytics_source_data_1 vale literalmente "LinkedIn" en todos los contactos de PAID_SOCIAL, así que una lente CRM por campaña no es posible hoy. En julio 2026 el contraste dio 320 registros en plataforma contra 64 en CRM (105 sumando SOCIAL_MEDIA), y 408 formularios contra 837. Discrepancia abierta.
Consulta de referencia: contactos con createdate en el mes, lead_status_supply presente y hs_analytics_source = PAID_SOCIAL, agrupados por estado. Control de sanidad: PAID_SEARCH debe cuadrar con los registros del report de Google Ads del mismo mes.
Message Ads no reportan impresiones
Las campañas *_MSG_v1 devuelven impressions = 0 y clicks = 0 con gasto positivo. En julio fueron €3.591,45. El campo supply.spend_sin_impresiones lo declara y sube una fila de salud, porque si no el CTR y la frecuencia se leen como si cubrieran todo el presupuesto.
Contrato de datos (schema v1.1)
meta { account, period, window, sql_conversion_action, supply_exclude_tokens,
supply_buckets, demand_status, north_star, source, refreshed_at, schema_version }
hero { spend, impressions, reach, frequency, clicks, ctr, leads, mql, mql_status, sql, cpl, cpsql }
_notes [ { type, text } ] portada del Overview
funnel [ { stage, spend, impressions, clicks, ctr, leads, sql } ]
pipeline { status:"manual", influenced_eur, opportunities, companies_touched, cpo_influenced }
crm_crosscheck { status:"manual", platform:{...}, crm:{...}, verdict, note }
campaigns [ { name, objective, status_raw, dot, spend, impressions, reach, clicks, ctr,
leads, mql, sql, cpsql, cpl } ]
residual_conversions [ { campaign, conversion, value } ] convierten sin entrega en el mes
supply { spend, impressions, clicks, lgf_leads, registros, forms, otros,
cost_per_registro, cost_per_form, spend_sin_impresiones,
conversions_by_name:[ { name, bucket, value } ],
campaigns:[ { name, objective, status_raw, spend, impressions, clicks,
lgf_leads, registros, forms, cost_per_registro, no_impressions } ] }
prev_period { label, window, spend, sql, mql, leads, cpl, cpsql }
prev_supply { label, spend, registros, forms, cost_per_registro, cost_per_form }
restatement { date, reason, published, restated, vintage_note } solo Ene-Jun 2026
audiences { job_function, seniority, company_size, industry }
formats [ { type, name, campaign, impressions, clicks, ctr, leads } ]
health [ { scope, signal, value, status } ]
CPSQL no va en JSON. Se calcula en HTML como spend/sql. El coste por registro y por formulario sí van precalculados porque el denominador no es evidente.
meta.demand_status = "paused" cuando las campañas cliente no tuvieron entrega en el mes: el hero sale a cero legítimamente y sube una nota de portada. Julio 2026 es el primer mes así.
Conexiones
Google Search Console (OAuth / SA)
Supermetrics GAWA → GA4 PRO 304414381
Script de pull
deploy/pull-seo.py --month YYYY-MM
Setup inicial
Ver deploy/README-gsc-setup.md para configurar OAuth GSC
Output
data/seo/monthly/YYYY-MM.json
Manifest usa "id" (no "slug") — intencional
Credenciales requeridas
| Variable / Archivo | Descripción | Obligatoriedad |
SUPERMETRICS_API_KEY | Para pull GA4 vía GAWA | OBLIGATORIO |
GSC_CREDENTIALS_FILE | Path al JSON OAuth de GSC (default: deploy/gsc-credentials.json) | OBLIGATORIO |
deploy/clawdbot-gsc-reader-sa.json | SA file (gitignored, nunca committear) | OBLIGATORIO |
Proceso mensual
- Verificar que
deploy/gsc-credentials.json existe (o variable GSC_CREDENTIALS_FILE apunta al SA file).
- Ejecutar
bash deploy/build-and-deploy.sh. Si OAuth no está configurado, el panel muestra banner "Pendiente primer pull" sin romper el resto.
- El script combina datos de GSC (clicks, impresiones, posición, keywords) con GA4 (sesiones de origen orgánico).
- Split NB/Brand:
keyword.includes('shakers') client-side en el HTML.
SA file (clawdbot-gsc-reader-sa.json) está gitignored. Nunca committear. Si se pierde, regenerar desde GCP Console y actualizar la variable de entorno.
Lente CRM (desde agosto 2026)
Hasta el 2026-08-03 el canal publicaba hero.sql = null con sql_status = PENDING_HUBSPOT. Ya no: pull_hubspot_organic() consulta HubSpot con la misma definición de SQL que los canales de pago (createdate en el mes, lifecyclestage IN salesqualifiedlead/opportunity/customer, contactos de test QA Shakers excluidos), cambiando solo la fuente de origen.
Fuente hs_analytics_source | Qué es | Dónde sale |
ORGANIC_SEARCH | Búsqueda orgánica clásica | hero.sql, hero.registros_talento y pestaña Negocio · CRM |
AI_REFERRALS | Tráfico derivado de asistentes de IA | aeo.crm · no se suma al orgánico |
Backfill Ene-Jun 2026 (2026-08-03), sin repullear GSC. Los seis meses publicados llevan meta.crm_backfilled_at y se verificó por assert que ninguna métrica de Search Console del hero cambiara. La serie es homogénea desde enero pero no se publicó mes a mes.
El SQL de HubSpot mezcla demand y talento en el mismo lifecyclestage (fallo 2 del diagnóstico de tracking, sin resolver), así que crm.sources.*.sql_de_talento declara cuántos son de talento en vez de esconderlo. Flag --skip-crm para saltarse HubSpot.
Alcance de la propiedad y subdominios
La propiedad es sc-domain:shakersworks.com, así que el hero incluye www + blog + know + careers. El bloque subdomains los desglosa.
Dos correcciones del 2026-08-03.
1. meta.know_excluded: true era engañoso: la exclusión de know.shakersworks.com aplica solo a la tabla de top páginas, no al hero, donde sí suma. Ahora son dos flags: know_excluded_del_hero: false y know_excluded_de_top_pages: true.
2. El cluster blog no tenía filtro de sección y se publicaba con 0 clics teniendo tráfico real (julio: 231 clics y 67.881 impresiones). Aparecía por el efecto colateral de clusters.setdefault(cluster, {}) al recorrer las keywords tracked. Ahora filtra por blog.shakersworks.com, que es justo lo que el mapa de etiquetas de la ficha ya asumía. Backfilleado en Ene-Jun de forma aditiva. Un cluster sin filtro se marca con no_path: true en vez de publicarse como cero.
No calcular porcentajes de reparto sumando subdominios. En Search Console las cifras a nivel de página y los totales de la propiedad se cuentan distinto: una SERP que muestra dos páginas del sitio cuenta una impresión por página y solo una en el total. En julio los clics por host suman 3.588 contra 3.528 del total, y en impresiones la desviación es mucho mayor.
Contrato de datos (schema v1.1)
meta { site, period, window, prev_window, north_star:"Clicks", source,
know_excluded_del_hero, know_excluded_de_top_pages, hosts_incluidos,
crm_backfilled_at, hosts_backfilled_at, refreshed_at, schema_version }
hero { clicks, impressions, ctr, avg_position, nb_clicks, nb_impressions,
brand_clicks, sql, sql_status, registros_talento }
prev_period { label, window, clicks, nb_clicks, impressions, avg_position }
crm { queried_at, lens, sql_definition,
sources: { ORGANIC_SEARCH | AI_REFERRALS:
{ sql, sql_qa_excluidos, sql_de_talento, registros, forms } } }
subdomains [ { host, pages, clicks, impressions, ctr, avg_position } ]
brand_split [ { type:"Brand"|"Non-brand", clicks, impressions, ctr, avg_position } ]
top_pages [ { url, clicks, impressions, ctr, avg_position } ] sin know
clusters [ { id, path_prefix, no_path, clicks, impressions, ctr, avg_position,
terms:[ { cluster, type, q, volume, cpc_eur, intent,
position, position_prev, delta, clicks, impressions, ctr } ] } ]
quick_wins [ … mismo shape que terms … ] pos 4-10, impr >50, CTR <2%
device_split [ { device, clicks, impressions, impr_pct, ctr, avg_position } ]
aeo { featured_snippets, rich_results, note, source,
crm: { sql, registros, forms, note } }
health [ { scope, signal, value, status } ]
La posición media del Non-brand en brand_split es parcial: se calcula sobre las 5.000 queries visibles del API, no sobre el total. El hero sí usa el agregado real de la propiedad.
El north star es Clicks y la segunda métrica Posición media, que son las dos que muestra la tarjeta del hub. Hasta agosto 2026 meta.north_star decía "NB clicks" aunque el hero de la ficha ya medía clics totales. El SQL orgánico es KPI de negocio, no north star.
Conexiones
Supermetrics LIP (LinkedIn Pages)
Supermetrics IGI (Instagram Insights)
Supermetrics YT2 (YouTube Analytics)
Script de pull
deploy/pull-social.py --month YYYY-MM
Un solo script, tres plataformas. JSON compartido.
Output
data/social/monthly/YYYY-MM.json
Leído por LI Orgánico, Instagram, YouTube y Social (legacy)
Canales que leen este JSON
channels/linkedin-organic/
channels/instagram/
channels/youtube/
channels/social/ (legacy)
Credenciales requeridas
| Variable | Descripción | Obligatoriedad |
SUPERMETRICS_API_KEY | Acceso a LIP, IGI y YT2 | OBLIGATORIO |
Proceso mensual
- Incluido en
build-and-deploy.sh. Ejecutar el día 1 del mes siguiente.
- Pull de los tres conectores en secuencia. Si uno falla, los otros siguen y el JSON lleva el campo en null.
new_followers de Instagram solo disponible para los últimos 30 días vía IGI. Si el pull es muy tardío, ese campo llega a null (esperado).
avg_view_pct YouTube = métrica a nivel canal, no por vídeo individual.
Instagram new_followers: si el pull se ejecuta después del día 30 del mes siguiente, la API IGI devuelve error 500. Campo queda null. Planificar pull antes del día 28.
Contrato de datos Social v1.0
meta { schema, period, month, window, accounts, north_stars, source, refreshed_at, status }
linkedin
hero { impressions, clicks, likes, comments, shares, engagements, engagement_rate,
new_followers, new_followers_org, total_followers, total_followers_org }
posts [ { title, url, media_type, impressions, engagements, engagement_rate, clicks, likes, shares } ]
health [ { scope, signal, value, status } ]
instagram
hero { reach, profile_views, interactions, total_followers, new_followers }
posts [ { type, product_type, permalink, caption, reach, like_count, comments_count, saves, shares, interactions } ]
reels { count, avg_watch_time_min, total_views, total_interactions, avg_skip_rate }
health [ { scope, signal, value, status } ]
youtube
hero { views, minutes_watched, avg_view_pct, new_subscribers, lost_subscribers,
net_subscribers, total_subscribers, likes, shares }
videos [ { title, url, published, views, minutes_watched, avg_view_pct, likes, shares } ]
traffic_sources [ { source, views, minutes_watched } ]
health [ { scope, signal, value, status } ]
Conexión
GA4 Data API directa (SA calero-marketing-analytics)
property: 304414381
+ HubSpot para el contraste de SQL
Script de pull
deploy/pull-web-analytics.py --month YYYY-MM
Creado el 2026-08-03. Ya no hay pull manual.
Scope de datos
www.shakersworks.com
blog.shakersworks.com
know.shakersworks.com
Excluye: app y academy
Output
data/web-analytics/monthly/YYYY-MM.json
Credenciales requeridas
| Variable | Descripción | Obligatoriedad |
GA4_SA_FILE | SA con Viewer en la property PRO (default ~/.config/gcloud/shakers-mkt-sa.json) | OBLIGATORIO |
HUBSPOT_PRIVATE_APP_TOKEN | Contraste de SQL de demand. Sin él, crm_contrast queda a null | Recomendado |
Automatización (2026-08-03)
El canal se rellenaba a mano desde mayo 2026. Ahora deploy/pull-web-analytics.py hace todo el pull contra la GA4 Data API. Validado contra junio publicado al dígito: sesiones, form views, landings, navegación, generate_lead, los dos ratios de conversión, pageviews, nuevos usuarios y bounce rate coinciden exactamente, y los 12 canales también. La serie sigue siendo comparable.
Las sesiones se publican como SUMA POR HOSTNAME, que es la agregación con la que se publicaron mayo y junio. Una sesión que toca www y blog cuenta en los dos hosts, así que esa suma es mayor que la consulta sin dimensión: en junio 11.720 sumando hosts contra 11.431 sin dimensión (2,5%). El campo hero.sessions_dedup trae la segunda para que la diferencia sea auditable. Los ratios de conversión usan la suma, para no romper la serie.
Ya no aplica el bug de filtros AND+OR de Supermetrics GAWA: el script va directo a la Data API y los filtros compuestos funcionan (andGroup con inListFilter).
Tres guardas automáticas nuevas
| Guarda | Qué detecta | Por qué |
| Reparto entre hosts | El MoM de los hosts incluidos no va en la misma dirección que el de TODA la property, o difieren más de 15 pp | Publicar solo www+blog+know deja un agujero: si el tráfico se mueve de app a www, el canal enseña crecimiento sin que llegue nadie nuevo. Pasó en julio 2026. |
| Contraste CRM | generate_lead contra el SQL de demand de HubSpot, con MoM de los dos | generate_lead es un evento de GA4 y depende del consentimiento y de GTM. En julio 2026 cubre el 35,8% del SQL que registra el CRM. |
| Form landings concentrados | Un solo canal con más del 50% de las llegadas directas al formulario | Mismo patrón que la anomalía de Display de mayo 2026 (733 sesiones), dada por corregida en junio (96) y reaparecida en julio (397, el 72%). |
Pendiente: registrar type_form como event-scoped custom dimension en GA4 PRO (304414381). Hasta que se registre, el split SQL/MQL no es consultable vía API. Acción: Gabi (GA4 admin).
Lo único que sigue a mano: sql_form_submissions y sql_form_completion_rate_pct vienen de HubSpot Forms y su API exige el scope form-analytics-api-access, no concedido. Se pasan con --sql-submissions y --sql-completion-rate; si no se pasan quedan a null con fila de salud en warn. Su base tampoco es la de generate_lead (submission de HubSpot contra evento de GA4), así que no se comparan en el mismo ratio.
Contrato de datos (schema v1.1)
meta { period, window, source, hostname_filter, north_star, note, form_pages,
sessions_note, automated_since, automation_note, forms_note,
refreshed_at, schema_version }
hero { sessions (suma por host), sessions_dedup, form_views,
form_landing_sessions, form_nav_views, generate_lead,
conv_rate_sessions_pct, conv_rate_form_pct, pageviews, new_users,
bounce_rate_pct, sql_form_submissions, sql_form_completion_rate_pct }
prev_period { label, window, sessions, form_views, generate_lead, pageviews,
new_users, bounce_rate_pct }
crm_contrast { sql_demand, sql_talento, sql_qa_excluidos, definition,
by_source:[ { source, sql } ], prev:{ label, sql_demand } }
channels [ { channel, sessions, generate_lead, conv_rate_pct } ]
sessions_by_host [ { host, sessions } ] hosts incluidos
excluded_hosts [ { host, sessions, generate_lead } ] app, academy y demás
form_landings [ { page, channel, sessions } ]
form_views_by_page [ { page, views } ]
funnel [ { step, sub, value, pct_of_sessions } ]
health [ { scope, signal, value, status } ]
Conexión
Ninguna. JSON editado manualmente.
Output
data/events/monthly/YYYY-MM.json
Proceso mensual
- Recopilar datos de eventos del mes: asistentes, tipo (online/presencial), leads generados.
- Editar o crear
data/events/monthly/YYYY-MM.json con el schema correspondiente.
- Actualizar
data/events/monthly/manifest.json.
- Actualizar
data/hub-summary.json: clave months.{YYYY-MM}.channels.events = total asistentes (estructura mensual).
- Deploy manual:
wrangler pages deploy .
Canal 100% manual sin dependencias externas. No hay credenciales que gestionar.
Fuente
TheirStack (job signals) + clasificador IA (Python)
4 archivos CSV por ciclo mensual
Mercados
midmarket · enterprise · international
Países: ES · PT · IT · UK
Script de pull
deploy/build-outbound.py con los 4 CSVs como argumentos. Metodología: mes natural + aprobadas fechadas por proxy discovered_at (los exports no traen fecha de aprobación).
Output
data/outbound/monthly/YYYY-MM.json
Los 4 archivos CSV
| Archivo | Qué contiene | Columnas clave |
TheirStack Leads.csv |
Todas las señales de hiring detectadas por TheirStack (raw payload JSON en cada fila) |
id_internal, source, raw_payload |
TheirStack Aproved.csv |
Señales que pasan el filtro IA sobre empresas ya existentes en CRM. La IA decide el canal y prioridad. Se rutean al SDR correspondiente. |
job_id, canal_target, job_title, job_url, company_name, country_code, decision_ia, motivo_ia, hs_status |
TheirStack Aproved New.csv |
Señales que pasan el filtro IA sobre empresas nuevas (no existen en CRM). Se crean empresa + contacto en HubSpot. |
Job_ID, Job_Title, Empresa, HS_Company_ID, HS_Contact_ID, SDR_Asignado, Cluster, URL_Oferta, Canal_Slack |
TheirStack Discard.csv |
Señales que no pasan el filtro IA con el motivo de descarte. |
job_id, empresa, puesto, motivo_descarte, fecha, link |
Proceso mensual
- Descargar los 4 CSVs del mes desde la plataforma TheirStack / carpeta compartida.
- Abrir una sesión de Claude Code en el directorio del repo (
/Users/alfonsocalero/02_reporting/shakers-reporting).
- Adjuntar los 4 CSVs al contexto de la conversación con
@path/to/file.csv.
- Indicar el mes del reporte (ej.
mayo 2026) y el mes MoM (ej. abril 2026).
- Claude extrae métricas con Python (dedup por
job_id, cross-tabs mercado/país, motivos de descarte, distribución CRM).
- Claude genera
data/outbound/monthly/YYYY-MM.json con el schema v1.0.
- Claude actualiza
manifest.json, hub-summary.json y hace deploy.
Deduplicación: los CSVs Aproved pueden contener duplicados (mismo job_id, diferente motivo_ia). Siempre deduplicar por job_id antes de contar. En Mayo 2026: 248 filas → 234 job_ids únicos.
Clasificación de mercados: viene del campo canal_target del CSV Aproved (midmarket-outbound-team, enterprise-outbound-team, international-outbound-team). Las empresas nuevas (Aproved New) se clasifican por el campo Cluster (UK, ITALIA, PORTUGAL) — todas se cuentan como international.
MoM: en la primera edición (Mayo 2026) prev_period.* = null. A partir de Junio 2026 el JSON del mes anterior ya existe y Claude puede calcular deltas automáticamente.
Motivos de descarte conocidos
| Código | Descripción |
crm_prospección_reciente | La empresa fue contactada recientemente — evita saturar |
competidor_recruiter | La empresa es una agencia de recruiting / competidor directo |
no_tech | El rol no es técnico (Product Marketing, Operations, etc.) |
crm_descartada_reciente | La empresa fue descartada del CRM recientemente |
hibrido_sin_freelance | La oferta pide híbrido/presencial sin modalidad freelance |
outside_ir35 | UK: contrato outside IR35 (incompatible con modelo Shakers) |
junior_intern | Perfil junior o becario |
otros | Otros motivos |
Contrato de datos (schema v1.0)
meta { period, window, source, markets, countries, note }
hero { signals_total, ai_reviewed, approved_total, approved_existing,
approved_new, discarded, approval_rate_pct, ai_review_rate_pct }
prev_period { label, window, signals_total?, approved_total?,
approved_existing?, approved_new?, discarded?, note? }
funnel [ { step, sub, value, pct_of_signals, color } ]
by_market [ { market, approved_total, approved_existing, approved_new,
pct_of_approved, countries } ]
by_country [ { country, flag, approved_total, approved_existing,
approved_new, pct_of_approved } ]
crm_status [ { status, count, note } ]
discard_reasons [ { reason, count, pct, label } ]
top_discarded_companies [ { company, count, note } ]
health [ { scope, signal, value, status } ]
Fuentes
GSC (sc-domain:shakersworks.com, SA clawdbot) · queries con tokens shaker/shacker/sakers
GA4 PRO canal Direct (SA calero-marketing-analytics), hostnames www+blog+know
Script
python3 deploy/pull-brand.py --month YYYY-MM
Cero pasos manuales. Env: GOOGLE_APPLICATION_CREDENTIALS (GSC) + GA4_SA_FILE + SUPERMETRICS_API_KEY.
Output
data/brand/monthly/YYYY-MM.json
Nota
La definición brand es superset de la del canal SEO (incluye misspellings): los números difieren un poco a propósito.
Brand de pago (desde agosto 2026)
El canal medía solo el brand orgánico, así que una caída era ilegible: no se podía distinguir entre la marca se busca menos y el anuncio de marca se está llevando el clic que antes llegaba gratis. Ahora pull_paid_brand() trae clics, impresiones y gasto de las campañas de Google Ads con brand en el nombre (cuenta 3453900141, vía Supermetrics AW) y el JSON publica paid_brand, hero.total_brand_clicks y hero.organic_share_of_brand_pct.
Serie Ene-Jul 2026: 5 de 6 cambios mensuales van en sentido inverso entre pagado y orgánico, la correlación de niveles es -0,67 y el brand total apenas se mueve (media 4.571 clics, variación 12,5%). En abril→mayo el intercambio es casi 1:1 (orgánico -405, pagado +411). Es el patrón que se espera de canibalización, pero con 7 meses no alcanza significación estadística: es correlación, no causalidad. Para decidirlo hay que pausar el brand de pago un periodo o montar un holdout geográfico. La fila de salud lo levanta automáticamente cada mes que los dos se muevan en sentido inverso.
El brand total es un constructo. GSC y Google Ads son sistemas de medición distintos: la suma orienta pero no es exacta y no debe presentarse como un total auditado.
Se consulta aquí y no se lee del JSON de Google Ads porque ese canal no guarda clics por campaña (solo publica lo necesario para SQL y CPSQL, por decisión de diseño).
La tarjeta del hub: los KPI de siempre siguen ahí
Añadir el brand de pago no cambió ninguna métrica que ya existía. branded_clicks y direct_sessions se calculan con la misma consulta a GSC y a GA4 que antes, con las mismas definiciones, y la ficha los mantiene como los dos primeros KPI. Lo nuevo va en una fila aparte que se oculta si el mes no trae paid_brand.
Lo que sí se rompió, y está corregido el 2026-08-03: la tarjeta del hub había pasado a mostrar Branded clicks + % orgánico del brand, dejando fuera el tráfico directo que el equipo venía siguiendo. Y peor: el MoM de una cuota se estaba imprimiendo como porcentaje relativo, así que 50,4% → 40,5% aparecía como -20% en vez de -9,9 pp. Dos números distintos (-22% de clics y -20% de cuota) que parecían medir lo mismo. Arreglado en index.html: cualquier KPI con fmt:"pct" muestra el delta en puntos porcentuales salvo que traiga delta_pp:false. Afectaba también a Bounce rate, Tasa de aprobación de outbound, Share of voice y Eng. rate. La tarjeta de brand vuelve a llevar los tres: Branded clicks, Tráfico directo y % orgánico del brand.
Direct está contaminado
direct_sessions se usa como proxy de notoriedad, pero el fallo de atribución documentado en
docs/tracking.html hace que parte del tráfico de campaña aterrice como Direct al perderse los parámetros antes del consentimiento. En julio 2026 Direct marca máximo de la serie (5.047 sesiones, +13,7%)
y eso no se puede leer como más notoriedad de marca. La fila de salud está en
warn a propósito.
Contrato de datos (schema v1.1)
meta { channel, period, window, north_star, source, brand_definition,
direct_definition, paid_brand_definition, refreshed_at, schema_version }
hero { branded_clicks, branded_impressions, branded_ctr, branded_position,
brand_share_clicks_pct, direct_sessions, direct_share_pct,
paid_brand_clicks, paid_brand_spend, total_brand_clicks,
organic_share_of_brand_pct }
prev_period { label, window, branded_*, direct_*, paid_brand_clicks,
paid_brand_spend, total_brand_clicks }
paid_brand { clicks, impressions, spend, ctr, cpc,
campaigns:[ { name, spend, clicks, impressions } ] }
top_queries [ { q, clicks, impressions, ctr, position } ] top 15
health [ { scope, signal, value, status } ]
Los meses sin paid_brand (antes de agosto 2026, si se repullean sin la clave de Supermetrics) esconden los KPI y el panel de pagado en la ficha, y suben una fila de salud en watch. La serie completa Ene-Jul se rellenó el 2026-08-03.
DOS SERIES QUE NO SE COMPARAN. El canal cambió de metodología en julio 2026 y cada mes declara a cuál pertenece en meta.series.
legacy (mayo-junio 2026, congelada): citas de IA de Bing Webmaster Tools por export manual + panel HubSpot AEO beta.
tool (desde julio 2026): herramienta propia sobre una base de prompts ponderada y actualizada semanalmente, con control de crecimiento. Mide google_aio y LLMs.
Encadenarlas publicaría un -67% en SoV y un +166% en visibilidad de marca que no han ocurrido (junio 15,0% contra julio 5,0%; junio 9,95% contra julio 26,5%). Son universos distintos con los mismos nombres de métrica. La ficha pinta solo los bloques de la serie del período, cambia el banner y el contrato, y la tarjeta del hub lleva prev: null en julio a propósito.
Serie tool (desde julio 2026)
Fuente
Tool propia de AEO. Resumen mensual en markdown (aeo-monthly-YYYY-MM.md) con indicadores, wins, losses, acciones y URLs propias citadas.
Script
deploy/import-aeo-tool.py --month YYYY-MM --export ~/Downloads/aeo-monthly-YYYY-MM.md --total-citations N --universe-citations N
Output
data/aeo/monthly/YYYY-MM.json · schema v2.0
North star
Citation SoV. Es un ratio, así que aguanta el crecimiento de la base de prompts.
Total citations son SOLO citas propias (volumen de Shakers, no del mercado), igual que en la serie legacy, donde Bing WMT reportaba citas de páginas nuestras. El total del mercado va aparte en citations_universe: es el denominador del SoV y sirve para auditarlo, pero no es un KPI porque nadie mueve las citas de la competencia. Aviso: las citas propias también crecen de forma mecánica si crece la base, así que se leen contra la metadata de la base y no en absoluto.
Dos cosas van a mano porque el export no las trae.
1. --total-citations (propias, el KPI) y --universe-citations (todas las marcas, el denominador): el .md solo publica el Citation SoV en porcentaje. El importer valida la coherencia de los tres y sube una fila de salud en warn si la desviación pasa de 0,15 pp, o en watch si falta el universo y el ratio no se puede verificar. En julio: 254 propias / 5.055 del universo = 5,02% contra el 5,0% del export, desviación 0,02 pp.
2. Metadata de la base de prompts: --prompts, --weights-version, --engines y --base-updated. Sin ella no se puede separar una mejora real del crecimiento de la base, que es justo lo que el control de crecimiento de la tool resuelve. Si falta, el JSON lo declara y sube fila de salud en warn. En julio 2026 falta.
Cobertura del detalle de URLs. Las URLs propias citadas del export llegan todas con motor google_aio. Los indicadores de LLM (menciones 55,9%, citas 30,5% en julio) no traen URL, así que la tabla de contenido citable no cubre todo lo que mide la tool. La ficha lo avisa.
Indicadores nuevos que aparezcan en el export y no estén mapeados en INDICATOR_KEYS no se descartan: van a indicators_extra con su etiqueta original y salen en la tabla. Mismo criterio que el bucket otros de supply en LinkedIn.
Serie legacy (mayo-junio 2026, congelada)
Fuentes
Citations = Bing WMT: 3 CSVs mensuales (AIPerformanceOverviewStats, AISearchQueriesReport, AIPageStatsReport), ventana móvil.
HubSpot AEO beta: SoV, visibilidad y sentimiento (lectura manual). Sus citas nunca se usaron como KPI.
Script
deploy/import-aeo.py · congelado, no se ejecuta más. Se conserva para poder regenerar mayo y junio si hiciera falta.
Contrato de datos (schema v2.0 · serie tool)
meta { channel, series:"tool", period, window, mes_cerrado, north_star:"Citation SoV",
source, methodology, prompt_base:{ prompts, weights_version, engines,
base_updated, note }, comparable_with_legacy:false, legacy_note,
engines_en_urls_citadas, export_file, refreshed_at, schema_version:"2.0" }
hero { citation_sov_pct, total_citations (SOLO propias · KPI),
citations_universe (todas las marcas · denominador, no KPI),
brand_visibility_pct }
prev_period null hasta agosto 2026 (julio es el primer mes de la serie)
indicators [ { key, label, value, help } ] los 8 del export, con su definición
indicators_extra [ { label, value, unit } ] indicadores nuevos sin mapear
wins / losses [ … ] vacíos si el export dice "sin wins/pérdidas ≥0,5 pp"
actions [ … ] acciones sugeridas por la tool
cited_urls [ { url, engine, prompt_id, cluster, prompt_family } ]
cited_by_cluster [ { cluster, urls } ] derivado de la URL
cited_by_prompt_family [ { family, urls } ] h2 = rol, ge = genérico, br = marca
health [ { scope, signal, value, status } ]
JULIO 2026 NO ESTÁ PUBLICADO, a propósito. El scraper de menciones no puede medir el north star del canal y sin él el mes no se cierra. Decisión de Alfonso (2026-08-03): esperar al tracking del programa en vez de publicar un mes cojo.
Qué cubre el scraper y qué no
| Métrica | ¿Está en el xlsx? | Julio 2026 |
| Menciones del mes | Sí, hoja Menciones, una fila por post | 73 · 60 autores únicos · 51 perfiles y 22 empresas · 11 reposts |
| Engagement de las menciones | Sí | 2.517 reacciones · 316 comentarios · 103 compartidos |
| Comentarios sobre las menciones | Sí, hoja Comentarios | 232 de 204 personas distintas (solo recuento, ver datos personales) |
| Empleados posteando (north star) | NO de forma fiable | hay que pedirlo al equipo |
| Posts de empleados | NO de forma fiable | hay que pedirlo |
| Impresiones estimadas | NO existe el campo | junio publicó 311.363 con la Matriz de Impresiones |
| Programa InfluShakers: activos y puntos | NO | junio: 53 activos de 108 |
Por qué la hoja "Post equipo" no vale como north star. Tiene 397 filas desde 2017 y 65 autores, pero cruzada contra las 102 URLs de LinkedIn del roster de la hoja Equipo solo engancha 2 personas en julio 2026, con 4 posts que además son los cuatro reposts. En junio engancha 6 personas y 20 posts, contra las 15 personas y 41 posts que reportó el tracking del programa. Y está contaminada: 10 de sus 14 filas de julio son de fuera del roster, incluida la cuenta de empresa y terceros sin relación. Es un scrape parcial, no el tracking.
El cruce con el roster está comprobado y funciona, pero no se ha integrado: decisión de Alfonso de quedarse solo con menciones agregadas, sin distinguir empleado de tercero.
RUPTURA DE FUENTE. Las menciones de mayo y junio 2026 salen del xlsx manual del equipo (105 en junio) y desde julio del scraper (73). Misma métrica, fuentes distintas: el importer lo declara en meta.source_break_note y sube una fila de salud. No cruzar el MoM sin decirlo, que es el problema que hubo que separar en AEO.
Datos personales
La hoja Comentarios trae nombre, cargo y URL de perfil de 204 personas externas. El importer publica solo recuentos de esa hoja: ni nombres, ni cargos, ni URLs de perfil. Verificado en la salida.
De las menciones sí se publica el autor y la URL del post, que es contenido público que menciona a la marca, pero nunca la URL de perfil de una persona. Ojo igualmente: el 69,9% de las menciones de julio son de perfiles individuales, así que la tabla de top menciones enseña nombres de personas externas. Si eso no encaja, la alternativa es publicar solo el tipo de autor y el engagement.
Cómo cerrar el mes cuando llegue el dato
python3 deploy/import-social-selling-scraper.py \
--xlsx "~/Downloads/menciones_shakers_linkedin.xlsx" --month 2026-07 \
--employees-posting N --posts N --impressions-est N \
--influshakers-active N --influshakers-roster N
Sin esos flags el importer marca el mes como PARCIAL y se niega a escribir: solo lo deja inspeccionar con --stdout. Es la guarda que impide publicar un mes sin north star.
El importer viejo (deploy/import-social-selling.py) sigue siendo el de la fuente manual y no se toca: lee otras hojas y otro formato.
Motivos de descarte: 33 etiquetas para 8 categorías
El clasificador de IA escribe unas veces un slug y otras prosa libre, así que el mismo motivo llegaba partido. En julio 2026: crm_prospección_reciente (124) y crm_prospeccion_reciente sin tilde (17) se contaban por separado, y "Rol no técnico" venía en cuatro variantes.
Ahora canon_reason() agrupa por palabras clave en orden y conserva el texto original en raw_labels, que la ficha muestra debajo de cada motivo. Efecto en el ranking de julio: CRM en prospección reciente pasa de 124 (48,6%) a 141 (55,3%) y Rol no técnico de 21 en cuarto puesto a 53 (20,8%) en segundo. Arreglar el prompt del clasificador para que emita solo slugs y esto deja de hacer falta.
Tres guardas nuevas
| Guarda | Qué detecta | Julio 2026 |
| Hueco de revisión | Señales que entran y no reciben ni aprobación ni descarte. Salta si pasa del 25% | 422 de 935 (45,1%) |
| Doble decisión | Mismo job_id presente en el export de aprobadas Y en el de descartes. Es una contradicción del origen, no un cálculo: cuenta en los dos lados del embudo | 6 ofertas |
| Enrutado de alertas | Alertas que hay que reasignar porque el SDR asignado no es el owner activo. Salta por encima del 10% | 96 de 562 (17,1%) y 104 (18,5%) con owner desconocido |
Los dos CSV nuevos
TheirStack Activity SDR.csv entra como bloque ACUMULADO, no del mes. No trae fecha ni job_id, así que no hay forma de recortarlo al período. Se publica con sdr_activity.scope = "acumulado", un aviso en la ficha y una fila de salud que lo repite. No entra en ningún KPI del mes. Flag --activity.
Para poder atribuirlo a un mes hace falta que el export incluya la fecha de la alerta.
TheirStack BD's.csv NO se ha integrado. Comprobado: de sus 364 job_id solo 5 aparecen en Leads y 3 o 4 en aprobadas o descartes, así que no pertenece al embudo que mide este canal (TheirStack → clasificador IA → aprobar o descartar). Es otra vía y meterlo en el mismo embudo daría un total falso. Pendiente de saber qué representa antes de decidir si es un canal aparte o una fase nueva.
Fechado de las aprobadas
Los exports de aprobadas no traen fecha: se fechan por proxy con el discovered_at del job en Leads (join por job_id). Validado: el script reproduce junio 2026 publicado al dígito (841 señales, 289 + 75 aprobadas, 230 descartes) partiendo de los exports acumulados de agosto.
Alternativa comprobada por si el discovered_at falla algún mes: el job_id de TheirStack crece con la fecha de proceso y los cortes entre meses son limpios (julio 2026 = 746.148.801 a 785.346.278). Clasificar por umbral de job_id acierta 655 de 655 contra las fechas reales de los descartes.
Un fallo de fuente se publicaba como CEROS. _sm_call() devolvía lista vacía al fallar y las sumas daban 0, con meta.status fijo en "OK". En julio 2026 las seis consultas de Instagram fallaron con QUERY_AUTH_NOT_FOUND y el pull escribió reach 0, profile views 0, interactions 0: la ficha habría publicado un -100% que no ha ocurrido.
Cómo degrada ahora
| Situación | Qué hace |
| Todas las consultas de una plataforma fallan | Sus KPI pasan a null (no a 0), listas a vacío, bloque.status = "SIN_DATO" y una fila de salud en error con el motivo y la consulta que falló |
| Alguna plataforma cae | meta.status = "PARCIAL" + meta.platforms_sin_dato + meta.status_note |
| Consolidado del mes | Excluye las plataformas sin dato de by_platform, recalcula impresiones, interacciones y engagement rate, y añade platforms_excluidas más un aviso en formula_note: no es comparable con un mes completo |
| Las cuatro fichas que leen el JSON | Banner amarillo arriba (renderNoData()) explicando que los guiones son falta de dato y no una caída. En la ficha de la plataforma caída da el motivo concreto; en las demás, la nota del mes |
Julio 2026 está PARCIAL. Instagram sin dato. Para arreglarlo hay que volver a autorizar la fuente IGI en Supermetrics (Settings → Data sources → Instagram Insights). Ojo: el histórico no se recupera solo, hay que repullear julio en cuanto la autorización esté de vuelta.
El label del período en el manifest y en la tarjeta del hub llevan el sufijo · sin Instagram para que no haya que abrir la ficha para saberlo.
Fuente
xlsx del equipo ("REPORT SOCIALSELLING <Q>.xlsx"): hoja de menciones por mes + puntuaciones InfluShakers.
Los KPIs hero (empleados posteando, nº posts) NO salen del xlsx: los reporta el equipo desde el tracking del programa y van por CLI.
Script
deploy/import-social-selling.py --xlsx ... --month YYYY-MM --sheet "Menciones LinkedIn <Mes>" --prev-sheet ... --employees-posting N --posts N --prev-employees N --prev-posts N
Output
data/social-selling/monthly/YYYY-MM.json
Mejora pendiente
Pedir al equipo que el xlsx incluya empleados/posts por mes para eliminar los parámetros manuales.
Fuentes (4 xlsx, formatos distintos)
The Square (PT: fecha, medio, AAV €, reach) · Wildcom (ES: mes en texto, solo estado "Publicado") · Shared report (IT: lancio desde feb) · Roadmap (FR: retombées desde jun, audience + equiv pub).
En proceso de unificación en un sheet único consensuado con las agencias.
Script
deploy/import-pr.py --month YYYY-MM --square ... --wildcom ... --italy ... --france ...
Normaliza fechas (corrige typos de año), AAV con separadores raros ("7 201 €") y meses en texto.
Regla de publicación
Un mes solo se publica si TODOS los trackers activos están al día. Actividad: PT+ES desde enero · IT desde febrero · FR desde junio. Si falta uno, el mes no existe en el canal.
Output
data/pr/monthly/YYYY-MM.json
No ingiere datos ni produce JSON. Es la capa de síntesis narrativa encima de los KPIs que el hub ya publica: cada historia es un HTML scrollytelling autocontenido en stories/YYYY-MM.html que cuenta el cierre del mes con el sistema de marca Shakers. Se incluye aquí porque su fuente de datos sí es un proceso de ingesta (el Executive Marketing report de Notion), aunque el output no sea una ficha de canal.
Fuente
Executive Marketing report de Notion (solo lectura) + los JSON del propio hub. Cifras verbatim, nada inventado.
Output
stories/YYYY-MM.html
Autocontenido: tipografía, logo e imágenes embebidas en base64, sin depender de shared/.
Enganche desde el home
CTA animada ("Cuéntame la historia de <mes>") gated por el array STORY_MONTHS en index.html; la flecha se alinea bajo el chip del mes.
Sistema
skill shakers-theme + agente brand-reviewer. IBM Plex Sans, charts a mano en CSS/SVG.
Proceso por mes
- Extraer los KPIs del mes del Executive Marketing report de Notion. Cifras exactas, sin redondeos ni datos inventados.
- Generar
stories/YYYY-MM.html con la skill shakers-theme y pasar el brand-reviewer antes de dar por buena la pieza.
- Añadir el slug del mes al array
STORY_MONTHS en index.html para que aparezca la CTA en el chip correspondiente.
- Commit +
npx wrangler pages deploy . --project-name shakers-reporting --branch main + push.
Reglas de datos. El GMV por canal no es aditivo: cada canal atribuye el mismo deal por su propia lente, así que no se suman. Las imágenes generadas con Nim son solo conceptuales, sin cifras (los modelos de imagen distorsionan el texto), y todo meme con números exactos se hace en SVG a mano.
Primera historia: stories/2026-07.html (cierre de julio 2026). Detalle de la capa en
README.md y en
Roadmap.
deploy/import-social-selling-scraper.py· evaluado el 2026-08-03Menciones, una fila por postComentariosEquiposolo engancha 2 personas en julio 2026, con 4 posts que además son los cuatro reposts. En junio engancha 6 personas y 20 posts, contra las 15 personas y 41 posts que reportó el tracking del programa. Y está contaminada: 10 de sus 14 filas de julio son de fuera del roster, incluida la cuenta de empresa y terceros sin relación. Es un scrape parcial, no el tracking.El cruce con el roster está comprobado y funciona, pero no se ha integrado: decisión de Alfonso de quedarse solo con menciones agregadas, sin distinguir empleado de tercero.
meta.source_break_notey sube una fila de salud. No cruzar el MoM sin decirlo, que es el problema que hubo que separar en AEO.Comentariostrae nombre, cargo y URL de perfil de 204 personas externas. El importer publica solo recuentos de esa hoja: ni nombres, ni cargos, ni URLs de perfil. Verificado en la salida.De las menciones sí se publica el autor y la URL del post, que es contenido público que menciona a la marca, pero nunca la URL de perfil de una persona. Ojo igualmente: el 69,9% de las menciones de julio son de perfiles individuales, así que la tabla de top menciones enseña nombres de personas externas. Si eso no encaja, la alternativa es publicar solo el tipo de autor y el engagement.
PARCIALy se niega a escribir: solo lo deja inspeccionar con--stdout. Es la guarda que impide publicar un mes sin north star.El importer viejo (
deploy/import-social-selling.py) sigue siendo el de la fuente manual y no se toca: lee otras hojas y otro formato.