Guide exhaustif pour brancher un site Next.js sur INOTRACK. Tout y est vérifié contre le code réellement déployé (traqueur, API d'ingestion, extraction des champs).
Pour la version courte (2 étapes, 5 minutes), voir nextjs.md.
nextjs.md
session_uid
Dans CONTROL → INOTRACK → Sites & clés → votre site → onglet Intégration :
https://messagerie.inovertix.com/inotrack.js
<script>
https://messagerie.inovertix.com/api/inotrack
INOTRK_PUB_…
INOTRK_SEC_…
Vérifiez aussi, onglet Réglages, que vos domaines sont autorisés. Une clé publique n'écrit que depuis un domaine déclaré, et https://exemple.com et https://www.exemple.com comptent pour deux domaines distincts.
https://exemple.com
https://www.exemple.com
.env.local :
.env.local
NEXT_PUBLIC_INOTRACK_ENDPOINT=https://messagerie.inovertix.com/api/inotrack NEXT_PUBLIC_INOTRACK_KEY=INOTRK_PUB_votre_cle_publique # Serveur uniquement — PAS de préfixe NEXT_PUBLIC_, sans quoi Next.js # l'inclurait dans le bundle envoyé au navigateur. INOTRACK_SECRET_KEY=INOTRK_SEC_votre_cle_secrete
app/layout.jsx
import Script from "next/script"; export default function RootLayout({ children }) { return ( <html lang="fr"> <body> {children} {/* File d'attente : un clic survenu AVANT le chargement du traqueur n'est pas perdu, il est rejoué à l'initialisation. */} <Script id="inotrack-queue" strategy="beforeInteractive"> {`window.inotrack = window.inotrack || function () { (window.inotrack.q = window.inotrack.q || []).push(arguments); };`} </Script> <Script id="inotrack" src="https://messagerie.inovertix.com/inotrack.js" data-site={process.env.NEXT_PUBLIC_INOTRACK_KEY} data-endpoint={process.env.NEXT_PUBLIC_INOTRACK_ENDPOINT} strategy="afterInteractive" /> </body> </html> ); }
pages/_document.jsx
import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { return ( <Html lang="fr"> <Head /> <body> <Main /> <NextScript /> <script src="https://messagerie.inovertix.com/inotrack.js" data-site={process.env.NEXT_PUBLIC_INOTRACK_KEY} data-endpoint={process.env.NEXT_PUBLIC_INOTRACK_ENDPOINT} defer /> </body> </Html> ); }
Ce que le traqueur fait tout seul, sans une ligne de plus :
page vue à l'initialisation et à chaque navigation Next.js (il intercepte history.pushState, que le routeur App comme Pages utilisent) ;
history.pushState
durée passée sur chaque page et événement de sortie (via visibilitychange + sendBeacon — le seul signal fiable sur mobile) ;
visibilitychange
sendBeacon
Ce qu'il ne fait PAS : deviner vos étapes de tunnel. Elles doivent être envoyées explicitement — c'est l'objet des sections 4 et 5.
N'importe quel élément portant data-inotrack est suivi, avec sa valeur comme libellé :
data-inotrack
<button data-inotrack="Ajouter au panier">Ajouter au panier</button> <a href="https://wa.me/22890000000" data-inotrack="Contact WhatsApp">Nous écrire</a> <button data-inotrack="Filtre parfum homme" data-inotrack-value="homme">Homme</button>
L'attribut est cherché en remontant jusqu'à 6 niveaux de parents : un clic sur l'icône à l'intérieur du bouton trouve bien le bouton. Ces clics remontent dans INOANALYTIC → Clics tracés, pas dans le tunnel.
Signature : window.inotrack("funnel", "<clé technique>", { …données })
window.inotrack("funnel", "<clé technique>", { …données })
La clé doit correspondre exactement à la colonne « Clé technique » de l'onglet Étapes du tunnel. Sinon l'API répond 422 unknown_step et n'enregistre rien — volontairement : une erreur visible pendant l'intégration vaut mieux qu'un tunnel vide découvert une semaine plus tard.
422 unknown_step
"use client"; // Ajout au panier function ajouterAuPanier(produit) { // … votre logique … window.inotrack?.("funnel", "add_to_cart", { product_id: produit.id, product_name: produit.nom, // alimente le « Top produits » total: produit.prix, }); } // Formulaire de commande validé — c'est CETTE étape qui alimente la liste de relance function validerFormulaire(form) { window.inotrack?.("funnel", "form_validated", { telephone: form.telephone, ville: form.ville, email: form.email, moyen_paiement: form.moyenPaiement, total: form.total, acompte: form.acompte, }); } // Départ vers le paiement (ou choix « paiement à la livraison ») function lancerPaiement(commande) { window.inotrack?.("funnel", "payment_initiated", { moyen_paiement: commande.moyenPaiement, total: commande.total, }); }
Le ?. n'est pas une coquetterie : si le traqueur est bloqué par une extension, votre tunnel de commande ne doit pas planter pour autant.
?.
Pourquoi form_validated est l'étape la plus importante. C'est elle qui porte le téléphone et la ville. Sans elle, la liste de relance — les clients qui ont laissé leurs coordonnées sans finaliser — reste vide, quel que soit le reste.
form_validated
Le formulaire de commande transporte un téléphone, une ville, un montant. Ces données n'ont rien à faire dans une requête émise par le navigateur : elles sont visibles dans l'onglet Réseau, et la clé publique qui les accompagne est lisible par tous.
On les fait donc transiter par votre serveur Next.js, qui détient la clé secrète.
app/api/inotrack/[...path]/route.js
const ENDPOINT = process.env.NEXT_PUBLIC_INOTRACK_ENDPOINT; const ROUTES_AUTORISEES = ["funnel", "order-status"]; export async function POST(request, { params }) { const path = (await params).path.join("/"); // Liste blanche : ce relais ne doit pouvoir appeler QUE ces deux routes. // Sans elle, il deviendrait un proxy ouvert signé avec votre clé secrète. if (!ROUTES_AUTORISEES.includes(path)) { return Response.json({ error: "not_found" }, { status: 404 }); } const body = await request.text(); try { await fetch(`${ENDPOINT}/${path}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.INOTRACK_SECRET_KEY}`, }, body, // La mesure ne doit jamais faire attendre un client. signal: AbortSignal.timeout(3000), }); } catch { // Mesure perdue, commande intacte. C'est le bon arbitrage. } return Response.json({ ok: true }); }
pages/api/inotrack/[...path].js
export default async function handler(req, res) { if (req.method !== "POST") return res.status(405).end(); const path = [].concat(req.query.path).join("/"); if (!["funnel", "order-status"].includes(path)) return res.status(404).end(); try { await fetch(`${process.env.NEXT_PUBLIC_INOTRACK_ENDPOINT}/${path}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.INOTRACK_SECRET_KEY}`, }, body: JSON.stringify(req.body), signal: AbortSignal.timeout(3000), }); } catch { /* sans conséquence pour la commande */ } res.json({ ok: true }); }
await fetch("/api/inotrack/funnel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_uid: localStorage.getItem("_inotrk_sid"), // ← indispensable, cf. §6 step_key: "form_validated", data: { telephone: form.telephone, ville: form.ville, email: form.email, moyen_paiement: form.moyenPaiement, total: form.total, acompte: form.acompte, }, }), });
Quand une étape part de votre serveur, INOTRACK n'a aucun moyen de savoir de quel visiteur il s'agit : la requête vient de votre machine, pas de son navigateur. Sans session_uid, chaque envoi retombe sur une session artificielle.
Symptôme dans CONTROL : « 11 commandes pour 2 sessions », un tunnel dont les premières étapes sont à zéro alors que les ventes existent, et des parcours de session impossibles à reconstituer. Le bandeau de diagnostic de la page Conversion le signale explicitement.
Le traqueur stocke l'identifiant dans le localStorage, clé _inotrk_sid :
localStorage
_inotrk_sid
// Côté navigateur, avant tout appel serveur : const sessionUid = typeof window !== "undefined" ? window.localStorage.getItem("_inotrk_sid") : null;
Trois cas à couvrir :
Pour le webhook, l'astuce est de faire voyager le session_uid avec la commande :
// 1. À la validation du formulaire, on le persiste avec la commande await db.commandes.create({ ...donnees, inotrack_session: localStorage.getItem("_inotrk_sid") }); // 2. Dans le webhook FedaPay, on le relit await fetch(`${process.env.NEXT_PUBLIC_INOTRACK_ENDPOINT}/funnel`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.INOTRACK_SECRET_KEY}`, }, body: JSON.stringify({ session_uid: commande.inotrack_session, step_key: "payment_completed", data: { order_ref: commande.reference, // ← ouvre le suivi de livraison total: commande.total, acompte: commande.acompte, telephone: commande.telephone, ville: commande.ville, moyen_paiement: "fedapay", status: "processing", // statut initial de la commande (facultatif) }, }), });
Une session dure 30 minutes d'inactivité. Si votre parcours de commande peut durer plus longtemps (attente d'un SMS de confirmation, par exemple), lisez _inotrk_sid au début du parcours et conservez-le : il ne changera pas en cours de route.
C'est ce canal — et lui seul — qui distingue une vente d'une livraison. Sans lui, le taux de livraison reste à 0 % et toutes vos commandes apparaissent « sans statut ».
// À chaque changement d'état : expédiée, livrée, annulée, remboursée… await fetch(`${process.env.NEXT_PUBLIC_INOTRACK_ENDPOINT}/order-status`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.INOTRACK_SECRET_KEY}`, }, body: JSON.stringify({ order_ref: "CMD-1042", status: "completed" }), });
Rattrapage par lots (200 commandes maximum par requête) :
await fetch(`${process.env.NEXT_PUBLIC_INOTRACK_ENDPOINT}/order-status`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.INOTRACK_SECRET_KEY}`, }, body: JSON.stringify({ orders: commandes.map((c) => ({ order_ref: c.reference, status: c.statut })), }), });
Classement des statuts. Le préfixe WooCommerce wc- est retiré automatiquement (wc-completed et completed sont le même statut). Le classement standard :
wc-
wc-completed
completed
processing
on-hold
pending
cancelled
refunded
failed
Vos statuts maison (livree, remise_au_client…) se déclarent dans CONTROL → onglet Réglages du site, champ « Statuts considérés comme livré ». Les modifier reclasse tout l'historique, le taux de livraison reste donc cohérent.
livree
remise_au_client
order_ref doit être la même référence que celle envoyée à l'étape de conversion — c'est la clé de rapprochement. Si vous affichez « CMD-1042 » au client, envoyez « CMD-1042 » des deux côtés, pas l'identifiant interne de votre base.
order_ref
Les champs suivants sont extraits dans des colonnes indexées (filtres, exports, liste de relance). Tout le reste est conservé dans le payload et reste visible dans le parcours de session. Vous n'avez pas à renommer vos champs : les alias sont reconnus.
phone
telephone
tel
phone_number
billing_phone
whatsapp
email
mail
billing_email
e_mail
city
ville
billing_city
town
order_number
order_id
commande
numero_commande
reference
payment_method
payment
moyen_paiement
mode_paiement
provider
gateway
amount_total
total
montant
montant_total
order_total
amount
amount_deposit
deposit
acompte
avance
montant_acompte
Détails utiles :
La comparaison ignore la casse et les séparateurs : orderRef, Order-Ref et order_ref fonctionnent tous.
orderRef
Order-Ref
data
meta
fields
Les montants tolèrent les formats humains : "25 000", "25000,50" et "25,000.50" donnent tous le bon nombre. Un montant négatif est refusé.
"25 000"
"25000,50"
"25,000.50"
Le nom du produit est lu dans product_name, product, name, title, produit ou product_id (le premier trouvé) pour alimenter le « Top produits ».
product_name
product
name
title
produit
product_id
Trois emplacements possibles pour la clé, dans cet ordre de priorité :
Authorization: Bearer INOTRK_…
X-Inotrack-Key
?k=
Une clé secrète passée dans l'URL est refusée (401 secret_key_in_url) : elle finirait dans les journaux d'accès, les référents et l'historique du navigateur.
401 secret_key_in_url
GET /ping
200
{site, kind}
POST /collect
202
POST /funnel
POST /order-status
{updated, ignored}
Les limites sont par site, pas par clé.
Contraintes de charge utile : 50 événements maximum par lot /collect, corps de 32 Ko maximum, imbrication JSON de 6 niveaux maximum, URLs tronquées à 2048 caractères, 200 commandes maximum par lot /order-status.
/collect
/order-status
Codes d'erreur que vous rencontrerez pendant l'intégration :
401
missing_api_key
invalid_api_key
secret_key_in_url
403
origin_not_allowed
secret_key_required
site_inactive
422
unknown_step
step_key
validation_error
429
rate_limit_exceeded
1. La clé et le site répondent (aucune écriture) :
curl -sS "https://messagerie.inovertix.com/api/inotrack/ping?k=INOTRK_PUB_votre_cle" # → {"ok":true,"site":{"name":"…","slug":"…"},"kind":"public"}
2. Le traqueur est chargé — console du navigateur, sur votre site :
typeof window.inotrack // "function" localStorage.getItem("_inotrk_sid") // une chaîne — la session est ouverte
3. Une étape part bien — onglet Réseau, filtrez sur inotrack. Vous devez voir un POST vers /funnel répondant 202. Un 422 vous donne la raison en clair.
inotrack
POST
/funnel
4. Les données arrivent dans CONTROL :
/inotrack
/inotrack/analytics → les pages vues apparaissent (agrégat du jour rafraîchi toutes les heures, mais le jour courant est recalculé à la volée à l'affichage) ;
/inotrack/analytics
/inotrack/funnel → les étapes se remplissent, et le bandeau de diagnostic vous dit précisément ce qui manque encore ;
/inotrack/funnel
ouvrez un parcours de session : vous devez y voir pages vues et étapes du tunnel mêlées dans l'ordre chronologique. Si les étapes n'y sont pas, c'est le session_uid qui manque (§6).
Rien n'arrive du tout. Vérifiez dans cet ordre : (1) le domaine est-il dans les domaines autorisés — www. compte à part ; (2) la console montre-t-elle une erreur CORS ; (3) GET /ping répond-il 200 ; (4) le site est-il actif dans CONTROL ; (5) une extension de blocage n'intercepte-t-elle pas la requête.
www.
La navigation remonte, pas le tunnel. Normal si vous n'avez pas encore ajouté les appels de la §4 : INOTRACK ne devine aucune étape. Vérifiez ensuite que vos step_key correspondent au caractère près à l'onglet Étapes du tunnel.
Les commandes arrivent, pas les étapes amont. Vos conversions partent du serveur et les étapes navigateur ne sont pas branchées — c'est exactement le cas décrit au §6.
« N commandes pour 2 sessions ». Le session_uid n'est pas transmis (§6).
Le taux de livraison reste à 0 %. Aucun statut n'a été poussé sur /order-status (§7). Le compteur « sans statut (à synchroniser) » de la page Conversion vous donne le nombre exact de commandes concernées.
Les taux affichent « — ». Le dénominateur est à zéro : aucune session n'est entrée dans le tunnel sur la période. Un taux calculé sur zéro n'existe pas — c'est pour cela qu'il n'affiche pas « 0 % ».
Les pages vues sont dupliquées. Vous avez sans doute deux balises de traqueur (une dans le layout, une dans une page). Une seule suffit : la navigation Next.js est déjà gérée automatiquement.
Aucun cookie. Le traqueur utilise le localStorage (_inotrk_sid, _inotrk_ts, _inotrk_entry), jamais de cookie tiers.
_inotrk_ts
_inotrk_entry
Do Not Track respecté : si navigator.doNotTrack === "1", rien n'est collecté. L'API JavaScript reste exposée pour que vos appels ne lèvent pas d'exception.
navigator.doNotTrack === "1"
Rétention : 90 jours pour la navigation, 365 jours pour le tunnel, puis purge automatique. Les agrégats quotidiens, eux, sont conservés sans limite.
Les données que vous envoyez dans le tunnel (téléphone, email, ville) sont des données personnelles : ne transmettez que ce dont vous avez besoin, et pensez au consentement recueilli sur votre site avant d'exploiter la liste de relance.