Cómo Beaconry sobrevive a LiteSpeed, WP Rocket y Cloudflare
Los plugins de caché y de optimización de JS son los asesinos silenciosos del tracking de conversiones. Delay-JS espera un clic que a menudo nunca llega, Combine reordena tu bloque de init, Rocket Loader reescribe tu etiqueta de script. Beaconry se excluye a sí mismo de todo optimizador de JS que pueda verificar contra la propia documentación del proveedor, y refresca su nonce REST en el navegador, para que una pestaña abierta durante horas, o una página servida desde el caché de página completa, siga enviando eventos válidos. Esto es exactamente lo que hace y los dos casos que aún tienes que resolver a mano.
Dos modos de fallo, y son distintos
La gente mete "el plugin de caché rompió mi tracking" en un solo cajón. En realidad son dos problemas sin relación entre sí, y Beaconry los resuelve con dos mecanismos sin relación entre sí.
El primero es la optimización de JavaScript: Delay JavaScript Execution, Combine JS, Minify y Cloudflare Rocket Loader. Estos reescriben o reprograman el script que dispara tus eventos. El segundo es el caché de HTML de página completa: el mismo documento HTML, incluido el nonce de seguridad incrustado, se sirve a miles de visitantes y al mismo visitante horas después. El nonce que contiene queda obsoleto. El caché de página no toca tu JavaScript en absoluto, así que un arreglo para uno no hace nada por el otro.
Si solo piensas en la optimización de JS, lanzas un plugin que igual se apaga en cualquier sitio con una pestaña de larga vida o un caché de página agresivo. Si solo piensas en el caché de página, lanzas uno cuyo bloque de init se aplaza hacia el olvido. Necesitas ambos.
Por qué Delay-JS es el que de verdad duele
La mayoría de los optimizadores de JS degradan el tracking. Delay JavaScript Execution lo destruye. La función está construida para retener todo script no crítico hasta la primera interacción del usuario (scroll, mousemove, touch, tecla), para que la página logre una gran puntuación de Lighthouse con cero JS en la ruta crítica. Maravilloso para las métricas de pintado. Fatal para un evento de pageview, porque una gran parte de las sesiones, rebotes, visitas móviles de un toque y fuera, páginas precargadas, nunca produce esa primera interacción. Sin interacción, sin script, sin evento. La conversión ocurrió y tus analíticas nunca se enteraron.
Combine JS y Rocket Loader son más sutiles. No bloquean la ejecución del todo, la reordenan. Beaconry encola dos cosas: el archivo externo nl-data.js (deferred, en el footer) y un pequeño bloque inline que llama a NLData.init() con la config de runtime y el nonce de seguridad de la página. Si un combinador fusiona el archivo externo en un solo bundle pero deja el init inline donde estaba, o Rocket Loader reescribe la etiqueta del archivo como async, la llamada inline puede ejecutarse antes de que exista el motor del que depende. El pageview lanza un error en lugar de dispararse.
Así que el objetivo es estrecho y específico: mantener los dos scripts de Beaconry fuera de todo optimizador de JS, sin desactivar esos optimizadores para el resto del sitio. El cliente conserva su puntuación Lighthouse de 95+ en todo lo demás. Solo se extraen dos handles de script.
El problema de las dos superficies
Aquí está el detalle con el que tropiezan la mayoría de los plugins de tracking. WordPress te da el filtro script_loader_tag para reescribir la etiqueta HTML de un script, que es como añades un atributo de exclusión. Pero ese filtro solo ve siempre la etiqueta del archivo externo, el <script src="...nl-data.js">. Nunca ve el bloque inline separado que emite wp_add_inline_script(), el que en realidad llama a NLData.init() y dispara el pageview. Ese bloque inline se renderiza como su propio elemento <script id="bcnr-nl-data-js-after">, y script_loader_tag no puede tocarlo.
Lo que significa que una exclusión basada en atributos protege el archivo del motor pero deja expuesta la línea que lo ejecuta. Tienes que proteger ambas superficies, y necesitan mecanismos distintos:
- El archivo externo se protege con atributos de etiqueta (para Cloudflare y WP Rocket Delay) más filtros de exclusión basados en handle (para LiteSpeed y SiteGround).
- El bloque de init inline se protege con filtros de exclusión por subcadena de contenido, porque no hay atributo de etiqueta que añadir. Beaconry registra la subcadena estable
NLData.initen los dos optimizadores que exponen un hook documentado de exclusión inline.
Los dos handles en cuestión son bcnr-nl-data y bcnr-nl-data-gate (el motor de tracking y el motor del banner de consentimiento). Todo lo de abajo opera exactamente sobre esos dos y nada más, así que es idempotente y sin efectos secundarios para el resto de tus scripts.
La matriz de exclusión verificada en la documentación
Esta es la parte en la que ir a la fuente original demuestra su valor: cada uno de los marcadores de abajo se confirmó palabra por palabra contra la propia documentación del plugin de caché, no contra un post de blog, no contra un hilo de foro. Esa comprobación descartó 30 de 41 marcadores candidatos porque eran folclore que nunca aparece en una documentación oficial. Esto es lo que pasó el corte.
Etiqueta de script externa
El filtro script_loader_tag de Beaconry antepone dos atributos a sus propias dos etiquetas, en el orden que exigen los proveedores:
<script data-cfasync="false" nowprocket
src="...assets/vendor/nl-data/nl-data.js" defer></script>data-cfasync="false"le dice a Cloudflare Rocket Loader que ignore el script. Cloudflare exige que este atributo aparezca antes desrc, razón por la cual Beaconry lo inyecta justo después del<scriptde apertura.nowprocketle dice a WP Rocket que nunca aplique Delay JavaScript Execution al archivo. Este es el marcador que más importa, porque Delay-JS es el optimizador que rompe el tracking por completo en lugar de solo reordenarlo.
Exclusiones basadas en handle (LiteSpeed y SiteGround)
LiteSpeed Cache y SiteGround Speed Optimizer no exponen un atributo de etiqueta. Exponen hooks de filtro que toman una lista de handles de script. Beaconry añade sus dos handles a cada uno:
- LiteSpeed, tres filtros que cubren cada eje de JS:
litespeed_optimize_js_excludes(Combine y Minify),litespeed_optm_js_defer_exc(Defer y Delay),litespeed_optm_gm_js_exc(Guest Mode JS, la variante optimizada que LiteSpeed sirve a los visitantes no logueados). - SiteGround, tres filtros:
sgo_js_async_exclude(async y defer),sgo_js_minify_exclude(minify),sgo_javascript_combine_exclude(combine).
Exclusiones de bloque inline (WP Rocket y SiteGround)
Para el bloque inline NLData.init(), dos optimizadores exponen una exclusión documentada por subcadena de contenido. Beaconry registra la subcadena NLData.init en ambos:
- WP Rocket:
rocket_defer_inline_exclusions, así que el bloque de init nunca se aplaza ni se retrasa. - SiteGround:
sgo_javascript_combine_excluded_inline_content, así que el bloque de init nunca se pliega en un bundle combinado.
Todo esto se registra automáticamente al cargar el plugin. No hay ningún ajuste que activar. Si usas uno de estos plugins, los filtros ya están adjuntos y no hacen nada de forma limpia si el plugin está ausente.
Las dos excepciones manuales
La honestidad importa más que una tabla de compatibilidad limpia. Dos optimizadores no pueden excluirse automáticamente, porque el proveedor no ofrece ningún hook a nivel de código para ellos. Si tienes activadas esas funciones específicas, añades una línea cada una a mano.
- WP Rocket, Combine JavaScript. No hay ningún filtro ni atributo documentado para excluir un archivo o un script inline de Combine, solo los campos de la UI de admin bajo File Optimization. Si activas Combine JS, añade
nl-data.jsa "Excluded JavaScript Files" y un snippetNLData.inita "Excluded Inline JavaScript". Vale la pena saberlo: Combine es legacy y está desactivado por defecto en HTTP/2 (que es casi todos los hosts ahora). Delay-JS es la función de WP Rocket que en realidad rompe el tracking, y esa SÍ se excluye automáticamente víanowprocket. Así que en la práctica la mayoría de los usuarios de WP Rocket no necesitan hacer nada. - Cloudflare, Auto Minify (legacy) o APO JS combine. Estos son toggles a nivel de zona en el dashboard, bajo Speed, Optimization, sin marcador de origen por script. Minify por sí solo no rompe el orden de ejecución, así que es inofensivo. Rocket Loader es el único optimizador de JS de Cloudflare que reordena la ejecución, y ese SÍ se gestiona automáticamente con
data-cfasync="false". Si usas APO con JS combine, excluye la ruta o deja JS combine desactivado.
Todo lo demás está cubierto automáticamente. WP Super Cache, W3 Total Cache, Autoptimize y WP Fastest Cache en modo de caché de página simple nunca alcanzan el JavaScript de Beaconry, así que no necesitan ninguna exclusión. Si usas uno de ellos con JS combine agresivo activado, aplica el mismo consejo genérico: excluye nl-data.js de la optimización de JS. El caché de página por sí solo ya es seguro, que es justo el sentido de la siguiente sección.
El caché de página y el nonce obsoleto
Ahora el segundo modo de fallo. El endpoint REST de Beaconry, /wp-json/beaconry/v1/event, está protegido por un nonce de WordPress. La config inline incrusta un wp_create_nonce('beaconry_event') fresco al renderizar, nl-data lo adjunta a cada POST de evento, y el servidor descarta cualquier evento cuyo nonce no se verifique. Ese nonce es parte de la capa antiabuso: sin él, cualquiera en la internet abierta podría hacer POST de conversiones falsas, quemar tu cuota de Meta CAPI o envenenar tus informes de GA4.
Los nonces de WordPress están limitados en el tiempo. Rotan aproximadamente cada 12 horas y siguen válidos durante unas 24. Eso está bien para una página recién renderizada. Es un problema en el momento en que entra en juego el caché de página completa, porque ahora el nonce queda congelado en un documento HTML cacheado:
- Un visitante abre una pestaña y la deja abierta durante 14 horas. El nonce incrustado en el momento de la carga ya ha rotado fuera de vigencia.
- Tu caché de página sirve un documento HTML generado hace 20 horas. Cada visitante que aterriza en él recibe un nonce que ya está pasada su ventana.
- Una sesión de invitado rota por debajo de una página de larga vida.
En todos los casos el nonce incrustado es ahora uno que el endpoint REST rechaza, así que cada POST de evento se descarta silenciosamente. El usuario sigue en tu sitio, sigue convirtiendo, y los eventos se evaporan en la puerta.
Refresco de nonce en el cliente
El arreglo vive en el motor del navegador y es completamente independiente del plugin de caché que uses, eso es lo que lo hace robusto. nl-data sella el timestamp de cuando recibió su nonce. Antes de transmitir un evento, comprueba la edad del nonce. Si la página ha estado abierta más tiempo que la ventana de refresco, obtiene un nonce fresco de un endpoint GET dedicado, lo intercambia y luego envía:
function ensureFreshNonce( cb ) {
if ( !_config || !_config.nonce ) { cb(); return; }
if ( ( Date.now() - _nonceTs ) < NONCE_REFRESH_MS ) { cb(); return; }
var url = _config.endpoint.replace( /\/event$/, '/nonce' );
fetch( url, {
method: 'GET',
credentials: 'same-origin',
headers: { 'Accept': 'application/json' },
} )
.then( function ( r ) { return r && r.ok ? r.json() : null; } )
.then( function ( j ) {
if ( j && j.nonce ) { _config.nonce = j.nonce; _nonceTs = Date.now(); }
} )
.catch( function () { /* keep existing nonce on failure */ } )
.then( function () { cb(); } );
}Los detalles que hacen esto seguro en lugar de ingenioso:
- La ventana de refresco es de 6 horas, cómodamente dentro de la ventana de validez de ~24 horas. Así un nonce nunca se acerca a caducar antes de ser reemplazado, y el fetch de refresco se dispara como mucho un puñado de veces a lo largo de una sesión de todo el día, no en cada evento.
credentials: 'same-origin'envía las cookies de sesión, así que el nonce recién acuñado queda ligado a la misma sesión de invitado que el visitante ya tiene. Un nonce obtenido sin eso se verificaría contra una sesión distinta y sería inútil.- El fallo no es fatal. Si el fetch da error, el
.catchconserva el nonce existente y el evento aún intenta enviarse. Un nonce obsoleto pero quizá válido le gana a no tener evento. - Las páginas frescas y los endpoints Worker hacen cortocircuito. Una página más joven de 6 horas invoca el callback de forma síncrona sin salto de red, así que el tráfico normal nunca se retrasa. Los sitios del framework de NETZLICHT basados en Worker no llevan ningún nonce, así que toda la ruta se omite para ellos.
Mantener el propio endpoint de nonce no cacheable
Hay una última trampa. El endpoint de refresco, /wp-json/beaconry/v1/nonce, es un request GET, y los cachés de página completa como LiteSpeed cachearán con gusto las respuestas GET a /wp-json/*. Si esa respuesta se cachea, el refresco entrega el mismo nonce congelado que se suponía que debía reemplazar, y vuelves al punto de partida. Así que el endpoint se marca a sí mismo como no cacheable en ambos ejes:
do_action( 'litespeed_control_set_nocache', 'beaconry nonce must always be fresh' );
$response = new WP_REST_Response( [ 'nonce' => wp_create_nonce( 'beaconry_event' ) ], 200 );
$response->header( 'Cache-Control', 'no-store, max-age=0' );El header estándar Cache-Control: no-store cubre la mayoría de los proxies y CDNs. LiteSpeed ignora el Cache-Control simple para sus propias decisiones de caché, así que el endpoint también dispara litespeed_control_set_nocache, la API de control documentada de LiteSpeed. El do_action es un no-op inofensivo cuando LiteSpeed no está instalado. Exponer el nonce en un GET público no añade superficie de ataque, el mismo nonce ya está a la vista en la config inline de cada página, y el verdadero candado es el rate-limit por IP más los chequeos de consentimiento y de nonce en el propio endpoint de eventos.
Cómo verificarlo en tu propio sitio
No tienes que confiar en la tabla. Confírmalo en el navegador:
- Ver el código fuente en una página cacheada. Encuentra la etiqueta
<script>denl-data.js. Debería llevardata-cfasync="false"ynowprocket. Si esos atributos están presentes, Rocket Loader y WP Rocket Delay quedan ambos excluidos. - Pestaña Network de DevTools, deja la pestaña abierta. Después de que la página haya estado abierta más de 6 horas (o baja temporalmente la constante en una copia de staging), al siguiente evento debería precederle un GET a
/wp-json/beaconry/v1/nonceque devuelve un valor fresco, y el POST de evento siguiente lo lleva. - Comprueba los headers de respuesta de ese GET. Debería devolver
Cache-Control: no-store. Golpéalo dos veces y confirma que obtienes dos valores de nonce distintos, prueba de que el caché no lo está congelando. - Observa el POST de evento. Un 204 con el nonce en el cuerpo del request significa que el servidor lo aceptó. Si alguna vez ves que los eventos se detienen después de que la página lleva tiempo abierta, la ruta del nonce es el primer sitio donde mirar.
Para llevar
El tracking del lado del servidor no sobrevive automáticamente a un stack de caché. Los eventos siguen originándose en un script de navegador, y ese script vive en el mismo entorno hostil que todos los demás de la página: optimizadores que lo retrasan, combinadores que lo reordenan, cachés de página que congelan sus credenciales. Beaconry trata esto como dos problemas de ingeniería separados. Para la optimización de JS excluye sus dos handles de todo marcador que pueda verificar contra la propia documentación de un proveedor, y te dice con claridad los dos casos (WP Rocket Combine, Cloudflare APO combine) en los que no existe ningún hook de código. Para el caché de página refresca el nonce REST en el navegador en un ciclo de 6 horas, ligado a la sesión del visitante, fallando de forma suave, con el propio endpoint de refresco mantenido como no cacheable. El resultado es un tracking que sigue funcionando en un sitio WordPress endurecido y fuertemente cacheado sin pedirte que debilites el caché.