[🛠] Reducir el conteo de bots en las estadísticas del blog
✨ Resumen de GPT-5.5
Registro de cómo reduje tráfico de crawlers cloud y accesos headless ligeros en el conteo del Cloudflare Worker, añadiendo filtros ASN/organización, señales de visible engagement y desbloqueando un ASN de ISP mal bloqueado.
Los números de analytics volvieron a verse raros.
Después de añadir la página pública de estadísticas, el trabajo local ya no ensuciaba el contador de producción. Pero apareció otro problema.
Algunas peticiones no parecían lectura humana.
Abrían varias rutas muy rápido, llevaban un User-Agent parecido a un navegador, pero no se comportaban como un lector quedándose en la página. En un blog estático público es normal recibir crawlers. El problema es que, si también llaman a /track, inflan las vistas y las visitas públicas.
Cuando añadí el contador escribí que bots y visitas duplicadas estaban razonablemente bloqueados. En operación, ese “razonablemente” necesitaba subir un poco.
User-Agent no bastaba
El Worker ya tenía controles básicos.
bot user-agent
Cloudflare verified bot
Cloudflare bot score
dedupe window
Eso atrapa bots obvios.
Pero no todos los crawlers escriben bot en el User-Agent. Algunas peticiones parecen Chrome normal. Solo con User-Agent no se distingue bien una persona de un navegador automatizado fino.
Los datos request.cf de Cloudflare incluyen ASN y organización ASN. Hice que /track también los revise.
TRACK_BLOCKED_ASNS
TRACK_BLOCKED_AS_ORGS
Las variables de entorno no reemplazan por completo los defaults. Se suman a los defaults del Worker. Así los ejes claramente bloqueados quedan en código, y lo descubierto en operación se añade por entorno.
La comparación exacta de organización era débil.
Huawei Cloud
Huawei-Cloud-HK
Huawei Cloud Singapore POP
Huawei Clouds Singapore
Los nombres varían. Por eso la comparación acepta coincidencia exacta y texto contenido. Un default puede cubrir varias variantes.
Añadir 3 segundos de visible engagement
El filtro ASN por sí solo es demasiado de red.
Un lector real puede estar detrás de un ISP o red corporativa. Un crawler también puede venir de una red común. Había que mirar señales del cliente.
Ahora el cliente solo envía /track después de que la página haya estado visible un rato.
track_delay_seconds = 3
Tres segundos no es mucho. Un lector real casi no lo nota. Pero filtra previews instantáneas, scraping en pestañas ocultas y cargas que desaparecen enseguida.
El cliente manda estos valores en el payload.
engagementMs
visibilityState
documentHidden
viewportWidth
viewportHeight
El Worker los valida otra vez.
No basta con confiar en que el JS esperó. Si engagementMs es menor al umbral, se ignora como client_visible_too_short. Si el viewport es cero o inválido, se ignora como client_viewport_invalid.
Las señales visibles no eran opcionales
La primera implementación seguía siendo floja.
Comprobaba visibilityState y documentHidden si existían, pero una petición sin esos campos aún podía pasar. Eso endurecía al cliente nuevo, pero dejaba vivas peticiones manuales a /track sin campos.
Así que la condición del Worker quedó estricta.
visibilityState === "visible"
documentHidden === false
Si ambas no coinciden exactamente, se ignora como client_signal_missing.
La idea es tratar hidden y missing como la misma familia. /track es el endpoint que aumenta contadores públicos. Una petición a ese endpoint debe parecer enviada por la página normal y el cliente normal. Si faltan señales, no merece pasar.
/analytics sigue siendo público, pero /track exige más.
Leer estadísticas
-> público
Aumentar vistas
-> production origin
-> visible engagement
-> viewport válido
-> pasa filtros bot/ASN/org
Los valores bloqueados hay que revisarlos
Este trabajo también dejó claro que una blocklist no mejora solo por crecer.
Al intentar atrapar crawlers cloud, es posible bloquear por accidente un ASN de ISP real. Eso también descarta lectores reales. Por eso saqué del bloqueo integrado el ASN añadido por error.
Los filtros fuertes se ven bien, pero los false positives también cuestan.
Contar bots infla el número.
Bloquear personas borra lectores reales.
Ambas cosas son malas.
El criterio quedó así.
Bloquear ejes claramente cloud/crawler en el Worker.
Exigir señales visibles del cliente.
No poner tráfico tipo ISP humano en bloqueos integrados.
Guardar valores sospechosos con ignored_reason para auditoría.
analytics_events conserva filas mínimas con ignored_reason incluso para peticiones ignoradas. Sin eso, después sería difícil explicar por qué bajó un número o qué se bloqueó.
Los números deben aparecer rápido, pero no demasiado fácil
Un contador de visitas vive en un equilibrio incómodo.
Si actualiza muy lento, parece roto. Por eso el trabajo del contador usaba baseline de GA más incrementos inmediatos en D1.
Pero si contar es demasiado fácil, los bots también cuentan.
Este cambio mueve el equilibrio un poco hacia la cautela. Los lectores normales siguen contando después de 3 segundos. Pero automatización instantánea, documentos ocultos, viewports inválidos y ejes cloud conocidos entran con más dificultad.
La estadística pública no es un libro contable.
Aun así, debe acercarse a una señal de que alguien leyó. Los números grandes no son el objetivo. Los números confiables permiten decidir mejor.
Lo que comprobé
Comprobé esto durante el trabajo.
node --check cloudflare/ga-stats-worker.js
node --check assets/js/custom/visitor-stats.js
git diff --check
bundle exec jekyll build
Cloudflare Worker deploy
GitHub Pages deploy
También hice smoke tests del Worker.
/analytics?range=today
-> trackDelaySeconds: 3
/track without visibilityState
-> ignored, client_signal_missing
/track with visibilityState="hidden"
-> ignored, client_signal_missing
/track with documentHidden=true
-> ignored, client_signal_missing
También confirmé que el HTML desplegado de la home contenía data-track-delay-seconds="3".
No fue un cambio vistoso.
Pero los números públicos necesitan este tipo de defensa. Las vistas son visibles, así que un conteo malo rompe la confianza rápido. Este trabajo no hizo el contador más grande; lo hizo más difícil de engañar.
Deja un comentario