XunL Experiments: éditer visuellement les pages, affecter les visiteurs de manière cohérente, confirmer les demandes acceptées sur le serveur et obtenir des rapports avec des intervalles de confiance et un état de décision clair.
Choisissez comment vous connecter
Les deux utilisent la même connexion de site, des expériences et des rapports; seule l'installation et la confirmation de l'enquête diffèrent.
| JS SDK | plugin WordPress | |
|---|---|---|
| Sites Web | Tout site web: Shopify, Webflow, Wix, Squarespace, gestionnaires d'étiquettes, sites personnalisés | WordPress 6.2 ou supérieur |
| Installer | Ajouter une ligne de script à la tête partagée | Téléverser le plugin ZIP et entrer les détails de connexion |
| Page, cliquez et des objectifs d'événement personnalisés | Appui | Appui |
| Objectif d'enquête accepté | Signé par votre serveur (exemples Node.js, Python et PHP ci-dessous) | Automatique pour le formulaire de contact 7, Elementor Pro, WPForms et Gravity Forms |
Tout site web : installez le SDK JS
- Connectez-vous à XunL, ajoutez le site sous Sites, puis créez une connexion sous Expériences A/B → Sites et installation.
- Copiez le script que la console affiche et placez-le dans la tête partagée par chaque page, le plus tôt possible. Remplacer SITE_PUBLIC_KEY dans l'exemple par la clé publique de votre site.
- Connectez le consentement du visiteur (ci-dessous), puis sélectionnez Vérifier la connexion dans la console.
<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>Le script ne lit que la configuration de l'expérience publique; ne jamais mettre le secret de connecteur dans une page Web. Dans les applications à une page, appelez XunLexperiments.refresh() après avoir modifié les itinéraires avec history.pushState.
Où l'ajouter sur chaque plateforme
- Magasiner
- Boutique en ligne → Thèmes → Modifier le code → layout/theme.liquide, coller juste avant l'étiquette de la tête de fermeture. La checkout ne peut pas être édité sauf sur Shopify Plus, donc mettre des objectifs sur les pages avant la checkout.
- Flux Web
- Paramètres du site → Code personnalisé → Code de tête, coller et publier. Nécessite un plan de site payant.
- Wix
- Paramètres → Avancé → Code personnalisé → Ajouter un code personnalisé, choisissez Toutes les pages et Tête. Nécessite un plan premium avec un domaine connecté.
- Espace carré
- Paramètres → Avancé → Injection de code → En-tête. Nécessite le plan d'affaires ou plus.
- Gestionnaire d'étiquettes Google
- Créer une balise personnalisée HTML, coller le script et le déclencher sur toutes les pages ou l'initialisation – toutes les pages. Chargement à travers un gestionnaire de tags est plus tard que de l'écrire dans la tête, de sorte que les changements visuels sont plus susceptibles de clignoter.
- Sites personnalisés, Réagir, Next.js
- Mettez-le dans la tête de gabarit HTML, comme la mise en page racine ou _document dans Next.js, ou index.html dans les projets Vue et React.
Connecter le consentement du visiteur
Par défaut data-xunl-consentement="pendant": avant consentement le SDK ne touche pas le stockage du navigateur, créer un identifiant de visiteur ou envoyer des événements. Appelez l'un des suivants à partir du callback de votre bannière 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 votre site n'a pas besoin d'un consentement là où il fonctionne, définissez data-xunl-consent à accorder dans le script. La question de savoir si le consentement est requis dépend des règles où se trouvent votre site et les visiteurs.
Définir des objectifs d'expérience
Chaque expérience a un but principal, fixé avant le lancement; le verdict n'utilise que le but principal et d'autres buts sont à titre de référence.
| Objectif | Compte quand | Exemple |
|---|---|---|
| Pages | Le visiteur atteint un URL, comme une page de remerciement | https://www.ssevening.com/thank-you/ |
| Cliquez sur | Un clic sur un élément assorti d'un sélecteur CSS | #quote-button |
| Événement personnalisé | Signalé à partir de votre code avec la piste | XunLExperiments.track('quote_started') |
| Enquête acceptée | Votre serveur confirme que l'enquête a été enregistrée ou envoyée; voir la section suivante | — |
Nombre de demandes acceptées : confirmation du serveur
Sur les sites qui ne sont pas WordPress, compter les demandes acceptées en trois étapes. Envoyer seulement après que l'enquête a vraiment été enregistrée ou envoyée, jamais pour les clics, les échecs de validation ou le spam.
Étape 1: laissez les formes porter le contexte d'expérience
Ajouter data-xunl-forms="all" au script (ou un sélecteur CSS pour ne couvrir que les formulaires de demande). Les formulaires soumis incluent alors un champ caché xunl_context.
<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>Pour les formulaires envoyés avec fetch ou AJAX, ajoutez le contexte à la demande vous-même :
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 });Étape 2: signez et envoyez après que votre serveur enregistre l'enquête
Le secret du connecteur est affiché une fois lorsque vous créez la connexion; conservez-le dans la variable d'environnement du serveur XUNL_CONNECTOR_SECRET. Utilisez votre propre numéro d'enquête comme soumissionId; la reprise de la même enquête n'est pas comptée deux fois.
// 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;
}Étape 3: le contrat API
- Adresse
- POST https://www.xunl.net/x/ab/v1/sites/{SITE_PUBLIC_KEY}/conversions
- En-têtes
- Type de contenu: application/json · X-XunL-Timestamp · X-XunL-Nonce · X-XunL-Signature: v1=<hex>
- Chaîne à signer
- v1\nPOST\n/x/ab/v1/sites/{key}/conversions\n{timestamp}\n{nonce}\n{sha256(body)}
- Algorithme de signature
- HMAC-SHA256(secret de connexion, chaîne à signer) → hexagone inférieur
- Corps
- {eventId, submissionId, visitorId, acceptedAt, assignments, status: "accepted"}
- Heure et nonce
- Unix secondes dans les 300 s du temps du serveur; nonce est un UUID minuscule, nouveau pour chaque requête.
- Réponses
- 200 accepté ou duplicata; 401 mauvaise signature ou heure; 409 l'affectation du navigateur n'est pas encore arrivée, donc resigner le même corps et réessayer plus tard; ne pas réessayer autre 4xx.
plugin WordPress
- Télécharger le plugin ZIP sous A/B expérimentations → Sites et installation, puis dans WordPress aller à Plugins → Ajouter Nouveau → Upload Plugin et l'activer.
- Dans WordPress → Paramètres → XunL Experiments, entrez l'adresse XunL, la clé publique du site et le secret du connecteur.
- Cartez les formulaires d'enquête pour compter comme type:form ID, par exemple cf7:123. Les formulaires non répertoriés ne sont jamais recueillis.
- Connectez le consentement du visiteur, puis vérifiez la connexion dans la console. Le plugin inclut déjà le SDK, alors n'ajoutez pas le script à nouveau.
Vérifier la connexion
- Accepter les cookies sur votre site, ouvrir la console du développeur de navigateur et exécuter la commande ci-dessous; consentementStatut doit être accordé.
- Retour dans la console, sélectionnez Vérifier la connexion et confirmer que les événements et les affectations augmentent.
- Ouvrir la page d'expérience plusieurs fois dans des fenêtres privées et confirmer les deux versions afficher et soumettre correctement.
XunLExperiments.diagnostics()
// { version, consentStatus, visitorId, assignments, pendingEvents, lastError, ... }Foire aux questions
Puis-je l'utiliser sur un site web qui n'est pas WordPress ?
Oui. Ajoutez le script SDK JS à votre tête partagée pour exécuter des expériences visuelles et split-URL et pour mesurer les objectifs de page, de clic et d'événement personnalisé. Les demandes de renseignements acceptées sont signées et envoyées par votre propre serveur; le plugin WordPress fait simplement cette étape pour vous.
Pourquoi les demandes doivent-elles être confirmées par le serveur ?
Le navigateur voit seulement que Submit a été cliqué, ce qui inclut également les échecs de validation, spam et duplicata. Lorsque votre serveur signe l'enquête après qu'elle a vraiment été enregistrée ou envoyée, le rapport correspond à ce que les ventes reçoivent.
Est-ce que ça ralentira la page ou provoquera le scintillement ?
Le script charge asynchronement et conserve la page d'origine si la configuration échoue ou s'éteint. Les modifications visuelles s'appliquent une fois la configuration arrivée, si bien que placer le script tôt et garder les modifications petites réduit le scintillement; pour une refonte complète, utilisez split URL pour envoyer les visiteurs à la nouvelle page.
Est-ce que quelque chose est enregistré avant le consentement des visiteurs?
C'est pas vrai. Par défaut, le SDK attend : jusqu'à ce que XunLexperiments.consent(true) soit appelé ou qu'un événement xunl:consent brûle, il ne touche pas le stockage du navigateur, ne crée pas d'identifiant de visiteur ou n'envoie aucun événement expérimental. Le retrait du consentement efface les dossiers locaux.
Qu'en est-il des applications à une seule page (Réaction, Vue)?
La navigation vers l'arrière et vers l'avant est recoupée automatiquement. Après avoir changé d'itinéraires avec history.pushState, appelez XunLexperiments.refresh() une fois pour réévaluer le SDK quelles expériences s'appliquent au nouveau URL.