Experimentos XunL: edite páginas visualmente, asigne visitantes de manera consistente, confirme las consultas aceptadas en el servidor y obtenga informes con intervalos de confianza y un estado de decisión claro.
Elige cómo conectarte
Ambos utilizan la misma conexión al sitio, experimentos e informes; sólo difieren la instalación y la confirmación de la consulta.
| JS SDK | Complemento de WordPress | |
|---|---|---|
| Sitios web | Cualquier sitio web: Shopify, Webflow, Wix, Squarespace, administradores de etiquetas, sitios personalizados | WordPress 6.2 o posterior |
| Instalar | Agregue una línea de script al encabezado compartido | Cargue el complemento ZIP e ingrese los detalles de la conexión |
| Página, clics y objetivos de eventos personalizados | Soporte | Soporte |
| Meta de consulta aceptada | Firmado por su servidor (ejemplos de Node.js, Python y PHP a continuación) | Automático para Contact Form 7, Elementor Pro, WPForms y Gravity Forms |
Cualquier sitio web: instale JS SDK
- Inicie sesión en XunL, agregue el sitio web en Sitios, luego cree una conexión en Experimentos A/B → Sitios e instalación.
- Copie el script que muestra la consola y colóquelo en el encabezado compartido por cada página, lo antes posible. Reemplace SITE_PUBLIC_KEY en el ejemplo con la clave pública de su sitio.
- Conecte el consentimiento del visitante (a continuación), luego seleccione Verificar conexión en la consola.
<script defer src="https://www.xunl.net/js/xunl-experiments-sdk.js" data-xunl-key="SITE_PUBLIC_KEY" data-xunl-origin="https://www.xunl.net" data-xunl-consent="pending"></script>El script solo lee la configuración del experimento público; Nunca pongas el conector en secreto en una página web. En aplicaciones de una sola página, llame a XunLExperiments.refresh() después de cambiar las rutas con History.pushState.
Dónde agregarlo en cada plataforma
- Shopify
- Tienda en línea → Temas → Editar código → diseño/theme.liquid, péguelo justo antes de la etiqueta principal de cierre. El pago no se puede editar excepto en Shopify Plus, así que establezca objetivos en las páginas antes de realizar el pago.
- Flujo web
- Configuración del sitio → Código personalizado → Código principal, pegar y publicar. Requiere un plan de sitio pago.
- Wix
- Configuración → Avanzado → Código personalizado → Agregar código personalizado, elija Todas las páginas y Encabezado. Requiere un plan premium con un dominio conectado.
- Espacio cuadrado
- Configuración → Avanzado → Inyección de código → Encabezado. Requiere el plan Business o superior.
- Administrador de etiquetas de Google
- Cree una etiqueta HTML personalizada, pegue el script y actívelo en Todas las páginas o Inicialización – Todas las páginas. Cargar a través de un administrador de etiquetas es posterior a escribirlo en el encabezado, por lo que es más probable que los cambios visuales parpadeen.
- Sitios personalizados, React, Next.js
- Colóquelo en el encabezado de la plantilla HTML, como el diseño raíz o _document en Next.js, o index.html en proyectos Vue y React.
Conectar el consentimiento del visitante
De forma predeterminada, data-xunl-consent="pending": antes del consentimiento, el SDK no toca el almacenamiento del navegador, no crea una identificación de visitante ni envía eventos. Llame a uno de los siguientes desde la devolución de llamada de su banner de cookies.
// After the visitor accepts analytics cookies in your consent banner
XunLExperiments.consent(true);
// If they decline or withdraw consent
XunLExperiments.consent(false);
// Or, without touching the SDK directly (also works before it has started)
window.dispatchEvent(new CustomEvent('xunl:consent', { detail: { granted: true } }));Si su sitio no necesita consentimiento donde opera, configure data-xunl-consent como otorgado en el script. Si se requiere consentimiento depende de las reglas donde se encuentran su sitio y sus visitantes.
Establecer objetivos del experimento
Cada experimento tiene un objetivo principal, establecido antes del lanzamiento; el veredicto utiliza sólo el objetivo principal y los demás objetivos son como referencia.
| Objetivo | Cuenta cuando | Ejemplo |
|---|---|---|
| Páginas | El visitante accede a una URL, como una página de agradecimiento. | https://www.ssevening.com/thank-you/ |
| Haga clic | Un clic en un elemento que coincide con un selector de CSS | #quote-button |
| Evento personalizado | Reportado desde su código con seguimiento | XunLExperiments.track('quote_started') |
| Consulta aceptada | Su servidor confirma que la consulta se guardó o envió; ver la siguiente sección | — |
Conteo de consultas aceptadas: confirmación del servidor
En sitios que no son WordPress, cuente las consultas aceptadas en tres pasos. Envíe solo después de que la consulta realmente se haya guardado o enviado, nunca por clics, fallas de validación o spam.
Paso 1: dejar que los formularios lleven el contexto del experimento
Agregue data-xunl-forms="all" al script (o un selector de CSS para cubrir solo los formularios de consulta). Los formularios enviados incluyen un campo xunl_context oculto.
<script defer src="https://www.xunl.net/js/xunl-experiments-sdk.js" data-xunl-key="SITE_PUBLIC_KEY" data-xunl-origin="https://www.xunl.net" data-xunl-consent="pending" data-xunl-forms="all"></script>Para formularios enviados con fetch o AJAX, agregue usted mismo el contexto a la solicitud:
const body = new FormData(form);
const ctx = window.XunLExperiments && XunLExperiments.inquiryContext();
if (ctx) body.set('xunl_context', JSON.stringify(ctx));
await fetch('/api/inquiry', { method: 'POST', body });Paso 2: firme y envíe después de que su servidor guarde la consulta
El secreto del conector se muestra una vez cuando crea la conexión; manténgalo en la variable de entorno del servidor XUNL_CONNECTOR_SECRET. Utilice su propio número de consulta como ID de envío; Reenviar la misma consulta no se cuenta dos veces.
// Node 18+. Call after your server has saved or sent the inquiry.
const crypto = require('node:crypto');
const XUNL = 'https://www.xunl.net', KEY = 'SITE_PUBLIC_KEY', SECRET = process.env.XUNL_CONNECTOR_SECRET;
async function confirmInquiry(xunlContext, submissionId) {
let ctx; try { ctx = JSON.parse(xunlContext || 'null'); } catch { return false; }
if (!ctx || ctx.preview || !ctx.visitorId || !(ctx.assignments || []).length) return false;
const body = JSON.stringify({ eventId: crypto.randomUUID(), submissionId, visitorId: ctx.visitorId,
acceptedAt: new Date().toISOString(), assignments: ctx.assignments, status: 'accepted' });
const path = '/x/ab/v1/sites/' + KEY + '/conversions';
for (let attempt = 0; attempt < 3; attempt++) { // retries keep the body, sign again
const ts = String(Math.floor(Date.now() / 1000)), nonce = crypto.randomUUID();
const digest = crypto.createHash('sha256').update(body).digest('hex');
const sig = crypto.createHmac('sha256', SECRET).update(`v1\nPOST\n${path}\n${ts}\n${nonce}\n${digest}`).digest('hex');
const res = await fetch(XUNL + path, { method: 'POST', body, redirect: 'error', headers: { 'Content-Type': 'application/json',
'X-XunL-Timestamp': ts, 'X-XunL-Nonce': nonce, 'X-XunL-Signature': 'v1=' + sig } }).catch(() => null);
if (res && res.ok) return true;
if (res && res.status !== 409 && res.status < 500) return false;
await new Promise(r => setTimeout(r, 2000 * (attempt + 1)));
}
return false;
}
module.exports = { confirmInquiry };# Python 3.8+. Call after your server has saved or sent the inquiry.
import hashlib, hmac, json, os, time, uuid, urllib.error, urllib.request
from datetime import datetime, timezone
XUNL, KEY, SECRET = 'https://www.xunl.net', 'SITE_PUBLIC_KEY', os.environ['XUNL_CONNECTOR_SECRET']
def confirm_inquiry(xunl_context, submission_id):
try:
ctx = json.loads(xunl_context or 'null')
except ValueError:
return False
if not ctx or ctx.get('preview') or not ctx.get('visitorId') or not ctx.get('assignments'):
return False
body = json.dumps({'eventId': str(uuid.uuid4()), 'submissionId': submission_id, 'visitorId': ctx['visitorId'],
'acceptedAt': datetime.now(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z'),
'assignments': ctx['assignments'], 'status': 'accepted'}).encode()
path = f'/x/ab/v1/sites/{KEY}/conversions'
for attempt in range(3): # retries keep the body, sign again
ts, nonce = str(int(time.time())), str(uuid.uuid4())
canonical = f'v1\nPOST\n{path}\n{ts}\n{nonce}\n{hashlib.sha256(body).hexdigest()}'
sig = hmac.new(SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
req = urllib.request.Request(XUNL + path, data=body, method='POST', headers={'Content-Type': 'application/json',
'X-XunL-Timestamp': ts, 'X-XunL-Nonce': nonce, 'X-XunL-Signature': 'v1=' + sig})
try:
with urllib.request.urlopen(req, timeout=10):
return True
except urllib.error.HTTPError as e:
if e.code != 409 and e.code < 500:
return False
except OSError:
pass
time.sleep(2 * (attempt + 1))
return False<?php // PHP 7.4+ with cURL. Call after your server has saved or sent the inquiry.
function xunl_uuid(): string { $b = random_bytes(16); $b[6] = chr(ord($b[6]) & 0x0f | 0x40); $b[8] = chr(ord($b[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4)); }
function xunl_confirm_inquiry(string $xunlContext, string $submissionId): bool {
$xunl = 'https://www.xunl.net'; $key = 'SITE_PUBLIC_KEY'; $secret = getenv('XUNL_CONNECTOR_SECRET');
$ctx = json_decode($xunlContext, true);
if (!is_array($ctx) || !empty($ctx['preview']) || empty($ctx['visitorId']) || empty($ctx['assignments'])) return false;
$body = json_encode(['eventId' => xunl_uuid(), 'submissionId' => $submissionId, 'visitorId' => $ctx['visitorId'],
'acceptedAt' => (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s.v\Z'),
'assignments' => $ctx['assignments'], 'status' => 'accepted'], JSON_UNESCAPED_SLASHES);
$path = '/x/ab/v1/sites/' . $key . '/conversions';
for ($attempt = 0; $attempt < 3; $attempt++) { // retries keep the body, sign again
$ts = (string) time(); $nonce = xunl_uuid();
$sig = hash_hmac('sha256', "v1\nPOST\n$path\n$ts\n$nonce\n" . hash('sha256', $body), $secret);
$ch = curl_init($xunl . $path);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', "X-XunL-Timestamp: $ts", "X-XunL-Nonce: $nonce", "X-XunL-Signature: v1=$sig"]]);
curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
if ($code >= 200 && $code < 300) return true;
if ($code > 0 && $code !== 409 && $code < 500) return false;
sleep(2 * ($attempt + 1));
}
return false;
}Paso 3: el contrato API
- Dirección
- PUBLICAR https://www.xunl.net/x/ab/v1/sites/{SITE_PUBLIC_KEY}/conversions
- Encabezados
- Tipo de contenido: aplicación/json · X-XunL-Timestamp · X-XunL-Nonce · X-XunL-Signature: v1=<hex>
- Cadena para firmar
- v1\nPOST\n/x/ab/v1/sites/{key}/conversions\n{timestamp}\n{nonce}\n{sha256(body)}
- Algoritmo
- HMAC-SHA256(secreto del conector, cadena para firmar) → hexadecimal en minúsculas
- Cuerpo
- {Id. de evento, Id. de envío, Id. de visitante, aceptado en, asignaciones, estado: "aceptado"}
- Hora y nonce
- Segundos Unix dentro de los 300 s del tiempo del servidor; nonce es un UUID en minúscula, nuevo para cada solicitud.
- Respuestas
- 200 aceptado o duplicado; 401 mala firma o hora; 409 la asignación del navegador aún no ha llegado, así que vuelva a firmar el mismo cuerpo y vuelva a intentarlo más tarde; no vuelva a intentar con otros 4xx.
Complemento de WordPress
- Descargue el complemento ZIP en Experimentos A/B → Sitios e instalación, luego en WordPress vaya a Complementos → Agregar nuevo → Cargar complemento y actívelo.
- En WordPress → Configuración → Experimentos XunL, ingrese la dirección XunL, la clave pública del sitio y el secreto del conector.
- Asigne los formularios de consulta para que cuenten como tipo: ID de formulario, por ejemplo cf7:123. Los formularios que no figuran en la lista nunca se recopilan.
- Conecte el consentimiento del visitante y luego verifique la conexión en la consola. El complemento ya incluye el SDK, así que no vuelva a agregar el script.
Verificar la conexión
- Acepte cookies en su sitio, abra la consola de desarrollador del navegador y ejecute el siguiente comando; Se debe otorgar el estado de consentimiento.
- De vuelta en la consola, seleccione Verificar conexión y confirme que los eventos y las asignaciones están aumentando.
- Abra la página del experimento varias veces en ventanas privadas y confirme que ambas versiones se muestran y envían correctamente.
XunLExperiments.diagnostics()
// { version, consentStatus, visitorId, assignments, pendingEvents, lastError, ... }Preguntas frecuentes
¿Puedo usarlo en un sitio web que no sea WordPress?
Sí. Agregue el script JS SDK a su encabezado compartido para ejecutar experimentos visuales y de URL dividida y para medir objetivos de página, clics y eventos personalizados. Las consultas aceptadas son firmadas y enviadas por su propio servidor; el complemento de WordPress simplemente realiza ese paso por usted.
¿Por qué el servidor debe confirmar las consultas?
El navegador solo ve que se hizo clic en Enviar, lo que también incluye errores de validación, spam y duplicados. Cuando su servidor firma la consulta después de que realmente se haya guardado o enviado, el informe coincide con las ventas recibidas.
¿Ralentizará la página o provocará parpadeos?
El script se carga de forma asincrónica y mantiene la página original si la configuración falla o se agota el tiempo de espera. Los cambios visuales se aplican una vez que llega la configuración, por lo que colocar el script con anticipación y mantener los cambios pequeños reduce el parpadeo; para un rediseño completo, utilice la URL dividida para enviar visitantes a la nueva página.
¿Se registra algo antes de que los visitantes den su consentimiento?
No. De forma predeterminada, el SDK espera: hasta que se llama a XunLExperiments.consent(true) o se activa un evento xunl:consent, no toca el almacenamiento del navegador, no crea una identificación de visitante ni envía ningún evento de experimento. Retirar el consentimiento borra los registros locales.
¿Qué pasa con las aplicaciones de una sola página (React, Vue)?
La navegación hacia adelante y hacia atrás vuelve a coincidir automáticamente. Después de cambiar las rutas con History.pushState, llame a XunLExperiments.refresh() una vez para que el SDK vuelva a evaluar qué experimentos se aplican a la nueva URL.