Deux temps : le traqueur pour la navigation, un proxy serveur pour les données sensibles du tunnel. Les valeurs (endpoint, clé publique) sont pré-remplies dans l'onglet Intégration du site, dans CONTROL.
Version complète — App Router et Pages Router, rattachement de session, webhooks de paiement, alias de champs, codes d'erreur et dépannage : INTEGRATION-NEXTJS.md.
INTEGRATION-NEXTJS.md
app/layout.jsx — la balise vaut pour tout le site, navigation SPA comprise : le traqueur intercepte history.pushState et compte chaque changement de route comme une page vue.
app/layout.jsx
history.pushState
export default function RootLayout({ children }) { return ( <html lang="fr"> <body> {children} <script src="https://messagerie.inovertix.com/inotrack.js" data-site="INOTRK_PUB_votre_cle_publique" data-endpoint="https://messagerie.inovertix.com/api/inotrack" defer /> </body> </html> ); }
Suivre un clic : n'importe quel élément portant data-inotrack.
data-inotrack
<button data-inotrack="Ajouter au panier" onClick={addToCart}>Ajouter au panier</button>
Envoyer une étape de tunnel depuis le navigateur :
window.inotrack?.("funnel", "add_to_cart", { product_id: 12, product_name: "Montre X", total: 25000 });
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 le serveur Next.js, qui détient la clé secrète.
app/api/inotrack/[...path]/route.js :
app/api/inotrack/[...path]/route.js
export async function POST(request, { params }) { const path = (await params).path.join("/"); // Liste blanche : ce relais ne doit pouvoir appeler QUE ces deux routes. if (!["funnel", "order-status"].includes(path)) { return Response.json({ error: "not_found" }, { status: 404 }); } const body = await request.text(); await fetch(`https://messagerie.inovertix.com/api/inotrack/${path}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.INOTRACK_SECRET_KEY}`, }, body, }); // Réponse immédiate et muette : la commande du client n'attend pas la mesure. return Response.json({ ok: true }); }
.env.local :
.env.local
INOTRACK_SECRET_KEY=INOTRK_SEC_votre_cle_secrete
Côté page, on appelle le proxy avec l'identifiant de session lu dans le localStorage — c'est lui qui raccroche l'étape au parcours de navigation :
localStorage
await fetch("/api/inotrack/funnel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_uid: localStorage.getItem("_inotrk_sid"), step_key: "form_validated", data: { phone, city, email, payment_method, amount_total: total, amount_deposit: deposit }, }), });
Sans session_uid, l'étape est enregistrée dans une session isolée : le tunnel reste juste, mais le parcours n'est plus reconstituable.
session_uid
Depuis votre backend (route API, webhook de paiement, tâche planifiée) :
await fetch("https://messagerie.inovertix.com/api/inotrack/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" }), });
Lots acceptés (200 maximum) : { "orders": [{ "order_ref": "…", "status": "…" }, …] }.
{ "orders": [{ "order_ref": "…", "status": "…" }, …] }
curl -sS "https://messagerie.inovertix.com/api/inotrack/ping?k=INOTRK_PUB_votre_cle_publique"
Puis la pastille verte sur /inotrack dans CONTROL, après quelques minutes de trafic.
/inotrack