# Documentation Elvn
Source: https://docs.elvn.live/
## Par où commencer
- **[Installer le tracking en 5 minutes](/demarrage/installation/)** — le snippet, où le poser, comment vérifier.
- **[Référence du tracker](/tracker/reference/)** — tous les attributs `data-*`, l'API `window.elvn`, l'opt-out.
- **[Proxy first-party](/integration/proxy-first-party/)** — passer sous les adblockers (nginx, Next.js, Worker, Caddy).
- **[Événements & goals](/integration/evenements-goals/)** — instrumenter des conversions.
- **[Webhooks](/integration/webhooks/)** — alertes signées HMAC vers Discord, Slack, ntfy.
- **[Page vie privée](/vie-privee/page-type/)** — le modèle à coller sur votre site (exemption CNIL).
## Vous êtes une IA ?
Si on vous a demandé d'« installer Elvn sur ce projet », suivez le
[guide d'installation pour agents IA](/ai/) : détection du framework, insertion
du snippet, checklist de vérification. L'intégralité de cette documentation est
aussi disponible en Markdown brut : [/llms.txt](/llms.txt) (index) et
[/llms-full.txt](/llms-full.txt) (contenu complet).
---
# Installer le tracking en 5 minutes
Source: https://docs.elvn.live/demarrage/installation/
## 1. Récupérer le snippet
Dans le dashboard Elvn, créez le site (menu **Sites → New site**) avec son
**domaine réel** (ex. `blog.exemple.fr`), puis ouvrez **Settings → Tracking** :
le snippet y est généré tout prêt avec votre identifiant public.
Le snippet standard :
```html
```
- `SITE_PUBLIC_ID` est l'identifiant public du site, affiché à sa création et
dans **Settings → Tracking**. Il n'est pas secret, mais il est propre à
chaque site.
- Le script pèse ~1,5 Ko min+gzip, sans dépendance, entièrement sous
`try/catch` silencieux : il ne peut pas casser votre page.
## 2. Le poser au bon endroit
Collez-le **avant ``, sur toutes les pages** du site. Selon votre
stack : le layout global (Next.js, Astro, SvelteKit…), le template de base
(`base.html`, `app.blade.php`…), ou le `
` de chaque fichier HTML. Le
[guide pour agents IA](/ai/) donne l'emplacement exact framework par framework.
Deux points d'attention :
- **Une seule occurrence** du script par page.
- **SPA** : rien à configurer — les navigations `pushState`/`replaceState` sont
suivies automatiquement.
Si votre site applique une **CSP**, autorisez uniquement :
- `script-src` : `https://elvn.live` ;
- `connect-src` : `https://elvn.live` (endpoint `POST /api/e`).
## 3. Vérifier
Déployez, puis visitez une page du site (depuis le domaine réel, pas
localhost). Sous ~30 à 60 secondes :
- l'encart « en attente du premier événement » de **Settings → Tracking**
passe au vert ;
- votre visite apparaît dans la vue **Realtime** du site.
Vérification côté réseau (onglet Network du navigateur) : une requête
`POST https://elvn.live/api/e` doit partir au chargement et recevoir un
**202** avec un corps vide.
En ligne de commande :
```sh
# le script doit être servi (200, text/javascript, ETag)
curl -sI https://elvn.live/js/t.js | head -5
# l'endpoint répond TOUJOURS 202, même à un payload invalide (voir ci-dessous)
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://elvn.live/api/e --data '{}'
```
## 4. Erreurs courantes
### Le 202 qui ne compte rien (domaine non déclaré)
`POST /api/e` répond **toujours 202 avec un corps vide**, même quand
l'événement est rejeté : c'est volontaire (aucun oracle pour les scanners).
Un 202 ne prouve donc **pas** que l'événement a été compté. Il est rejeté en
silence quand :
- le `data-site` est inconnu ou le site désactivé ;
- **le domaine visité ne correspond pas au domaine déclaré** du site dans le
dashboard (les sous-domaines du domaine déclaré sont acceptés). C'est
l'erreur nº 1 : vérifiez que le domaine du site Elvn est exactement celui
servi en production ;
- le navigateur envoie le signal **GPC** (`Sec-GPC: 1`) et que le site le
respecte (option activée par défaut) ;
- la requête vient d'un bot ou dépasse le rate limit (100 evt / 10 s par
site + IP).
La seule vérité : l'événement visible dans **Realtime**.
### Rien ne part depuis localhost
C'est normal : le tracker reste **muet sur `localhost` / `127.x` / `[::1]`**,
sauf si vous ajoutez l'attribut `data-dev` au script. Testez depuis le domaine
réel, ou ajoutez `data-dev` temporairement (voir la
[référence du tracker](/tracker/reference/)).
### Adblockers
Les listes type EasyPrivacy bloquent les domaines d'analytics connus : une
partie de vos visiteurs (public tech surtout) sera invisible avec le snippet
standard. La parade : servir le script et l'endpoint depuis **votre propre
domaine** — voir [Proxy first-party](/integration/proxy-first-party/).
### Autres cas où le tracker se tait volontairement
Automatisation (`navigator.webdriver`), page dans une iframe, visiteur ayant
activé l'opt-out (`localStorage.elvn_ignore === "true"`). Détail complet :
[référence du tracker](/tracker/reference/).
## Étape suivante
- Instrumenter des conversions : [Événements & goals](/integration/evenements-goals/).
- Informer vos visiteurs : [la page vie privée à coller](/vie-privee/page-type/).
---
# Référence du tracker t.js
Source: https://docs.elvn.live/tracker/reference/
Le tracker est servi par le Worker de collecte sur `GET /js/t.js`
(`Cache-Control: public, max-age=86400, stale-while-revalidate=604800` + ETag).
~1,5 Ko min+gzip (budget CI : 3 Ko). Zéro dépendance, IIFE, tout le code sous
`try/catch` silencieux : il ne lève jamais d'erreur dans la page hôte.
## Snippet
```html
```
## Attributs `data-*`
| Attribut | Requis | Effet |
| --- | --- | --- |
| `data-site` | oui | Identifiant public du site. Sans lui, le tracker ne fait rien. |
| `data-api` | non | Endpoint de collecte custom (ex. `/stats/e` en [proxy first-party](/integration/proxy-first-party/)). Défaut : `/api/e`, ou `/api/e` si l'origine est introuvable. |
| `data-spa="off"` | non | Désactive le suivi des navigations SPA. |
| `data-outbound="off"` | non | Désactive les événements `outbound` / `download`. |
| `data-engage="off"` | non | Désactive l'événement `engage` (temps actif + scroll). |
| `data-dev` | non | Autorise le tracking sur `localhost` / `127.x` / `[::1]` (sinon le tracker reste muet en local). Attribut de présence, sans valeur. |
Les trois attributs `spa` / `outbound` / `engage` ne se désactivent qu'avec la
valeur exacte `"off"`.
## Ce qui est suivi automatiquement
- **Pageview** au chargement, puis à chaque navigation SPA :
`history.pushState` / `history.replaceState` patchés + `popstate`,
**dédoublonnés si le `pathname` ne change pas**.
- **Liens sortants** : clic sur un `` dont le hostname diffère de la
page → événement `outbound` avec `{url}` en props.
- **Téléchargements** : clic sur un lien interne dont le path se termine par
une extension de fichier (pdf, zip, dmg, exe, pkg, msi, deb, rpm, apk, iso,
rar, 7z, gz, tgz, tar, csv, doc(x), xls(x), ppt(x), txt, mp3/mp4, mov, avi,
wmv, wav) → événement `download` avec `{url}`.
- **Engagement** : au premier passage de l'onglet en arrière-plan
(`visibilitychange → hidden`) de chaque pageview, un événement `engage`
avec `{t: temps actif cumulé en ms, d: profondeur de scroll max en %}`.
Le temps actif compte au plus **30 s après la dernière interaction** (clic,
touche, scroll). Réarmé à chaque navigation SPA. Sert au calcul réel du
bounce et de la durée de visite.
## Payload envoyé
JSON ≤ 4 096 octets sur `POST /api/e` :
```json
{ "s": "SITE_PUBLIC_ID", "u": "/chemin?utm_source=…", "r": "https://referrer…", "w": 1440, "l": "fr-FR", "e": "nom-evenement", "p": { "plan": "pro" } }
```
| Champ | Contenu |
| --- | --- |
| `s` | Identifiant public du site (`data-site`). |
| `u` | Path + query **filtrée côté client** : seuls les paramètres `utm_*`, `ref` et `source` sont conservés. |
| `r` | `document.referrer`. |
| `w` | `screen.width`. |
| `l` | `navigator.language`. |
| `e` | Nom d'événement (absent pour un pageview). |
| `p` | Props de l'événement (absent si aucune). |
**Transport** : `navigator.sendBeacon` (corps texte → pas de préflight CORS),
fallback `fetch(…, { keepalive: true })`. Aucun retry client : un événement
perdu est perdu. Les envois déclenchés avant la fin du parsing du document
sont mis en file et rejoués à `DOMContentLoaded`.
Le Worker re-valide tout côté serveur et répond **toujours 202, corps vide**
(événement accepté ou rejeté — aucun oracle).
## Événements custom — API `window.elvn`
### Stub de file d'attente (optionnel)
Pour appeler `window.elvn()` avant que `t.js` soit chargé, ajoutez ce stub
**avant** le snippet — les appels sont rejoués à l'initialisation :
```html
```
### Appels
```js
window.elvn("signup", { plan: "pro" });
window.elvn("newsletter-open");
```
### Contraintes (validées côté client ET re-validées par le Worker)
- **Nom** : minuscules `a-z`, chiffres, `_`, `-`, 1 à 64 caractères
(`/^[a-z0-9_-]{1,64}$/`). Le tracker met en minuscules automatiquement ; un
nom invalide est **abandonné en silence**.
- **Noms réservés** — ne pas les utiliser pour des événements custom :
`pageview`, `engage`, `outbound`, `download`.
- **Props** : objet **plat** uniquement. Valeurs scalaires : chaînes
(tronquées à 500 caractères), nombres finis, booléens. Objets imbriqués,
tableaux, `null`, `NaN`/`Infinity` → ignorés silencieusement. Clés de 1 à
64 caractères. Le JSON des props doit tenir en 2 048 octets (payload total
≤ 4 096 octets), sinon le Worker rejette l'événement.
- **Jamais de données personnelles dans les props** (email, nom,
identifiant…) — c'est la responsabilité de l'intégrateur et une condition
de l'exemption CNIL (voir [Vie privée](/vie-privee/page-type/)).
### Attribut déclaratif `data-elvn-event`
Délégation de clic — l'ancêtre le plus proche portant l'attribut gagne, y
compris quand le clic touche un élément imbriqué :
```html
Essayer
```
Les goals de type « event » du dashboard matchent ces noms d'événements.
## SPA
Rien à configurer : `pushState`/`replaceState` sont patchés et `popstate`
écouté. Un pageview est envoyé à chaque changement de `pathname` (même path →
dédoublonné), et l'événement `engage` est réarmé. Pour un routeur exotique qui
ne passe pas par l'History API : il n'existe **pas** d'API de pageview
manuelle — gardez l'History API (ne mettez `data-spa="off"` que si vous
assumez de perdre les navigations internes).
## Ce qui n'est jamais suivi / opt-out
Le tracker s'arrête de lui-même quand :
- `navigator.webdriver === true` (automatisation) ;
- la page est dans une **iframe** (`window.top !== window.self`) ;
- la page tourne sur **localhost** (`localhost`, `127.x.x.x`, `[::1]`) sans
`data-dev` ;
- **opt-out visiteur** : `localStorage.elvn_ignore === "true"`.
Côté serveur, le Worker rejette en silence (toujours `202`) : sites
inconnus/inactifs, origine non conforme au domaine déclaré (sous-domaines
acceptés), bots (User-Agent + ASN datacenter), dépassement du rate limit
(100 evt / 10 s par site + IP tronquée), et les requêtes portant
**`Sec-GPC: 1`** si le site respecte Global Privacy Control (option par site,
**activée par défaut**).
Snippet opt-out à proposer aux visiteurs (voir la
[page vie privée](/vie-privee/page-type/) pour la version complète) :
```html
Ne plus me compter dans les statistiques
```
Ré-inclusion : `localStorage.elvn_ignore = 'false'` (ou suppression de la
clé), puis rechargement.
## CSP
Si le site applique une Content Security Policy, autorisez uniquement :
- `script-src` : `https://elvn.live` (ou le path du proxy first-party) ;
- `connect-src` : `https://elvn.live` (ou l'endpoint `data-api` du proxy).
---
# Proxy first-party (anti-adblock)
Source: https://docs.elvn.live/integration/proxy-first-party/
Les domaines d'analytics connus finissent dans EasyPrivacy : 25 à 40 % des
visiteurs tech sont invisibles sans proxy. La parade : servir le script et
poster les événements depuis **le domaine du site suivi**, qui reverse-proxie
vers Elvn. Quasi imblocable sans casser le site.
## Principe
Deux réécritures, avec des noms neutres (`/stats/…` par convention —
n'importe quel path neutre convient, éviter `track`, `analytics`, `collect`) :
```
GET /stats/t.js -> https://elvn.live/js/t.js
POST /stats/e -> https://elvn.live/api/e
```
Snippet correspondant (le dashboard le génère dans **Settings → Tracking**,
onglet « proxy ») :
```html
```
Deux exigences côté proxy :
1. **Transmettre l'en-tête `Origin`** de la requête entrante (comportement par
défaut de tous les proxys ci-dessous) : le tracker envoie `u` en path
relatif, le Worker valide alors l'origine via ce header contre le domaine
déclaré du site.
2. Transmettre le **User-Agent** entrant (défaut partout) : il sert au hash
visiteur et aux dimensions navigateur/OS/appareil.
## Limites connues (à assumer)
Le Worker de collecte lit l'IP de connexion et la géolocalisation Cloudflare
**de la requête qui lui parvient**. Derrière un proxy serveur, c'est l'IP du
serveur du site :
- les **visiteurs uniques** sont dégradés (tous les visiteurs partagent l'IP
du proxy : ils ne sont plus distingués que par leur User-Agent) ;
- la **géolocalisation** affichée est celle du serveur, pas du visiteur ;
- le **rate limiting** (par site + IP) s'applique à l'IP du proxy, donc à tout
le trafic proxifié du site.
Aucun en-tête `X-Forwarded-For` n'est lu (il serait falsifiable sans mécanisme
de confiance). Le proxy first-party est donc un **compromis** : plus de
couverture, moins de précision uniques/geo. La variante « Worker route sur la
zone du site » a exactement la même limite.
## Variante nginx
```nginx
location = /stats/t.js {
proxy_pass https://elvn.live/js/t.js;
proxy_set_header Host elvn.live;
proxy_ssl_server_name on;
proxy_ssl_name elvn.live;
# cache local optionnel : le script est déjà servi avec max-age=86400 + ETag
}
location = /stats/e {
proxy_pass https://elvn.live/api/e;
proxy_set_header Host elvn.live;
proxy_ssl_server_name on;
proxy_ssl_name elvn.live;
}
```
`Origin` et `User-Agent` sont transmis par défaut (nginx ne réécrit que
`Host` et `Connection`).
## Variante Next.js (rewrites)
`next.config.js` (ou `.mjs`/`.ts`) :
```js
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{ source: "/stats/t.js", destination: "https://elvn.live/js/t.js" },
{ source: "/stats/e", destination: "https://elvn.live/api/e" },
];
},
};
module.exports = nextConfig;
```
Puis le snippet proxy dans le layout :
```jsx
```
(ou une balise `
```
Règles :
- L'insérer dans le `` (idéalement juste avant ``), sur
**toutes les pages** du site — donc dans le layout/template global.
- **Une seule occurrence** par page. Avant d'insérer, vérifiez qu'aucun
snippet Elvn n'est déjà présent : `grep -rn "elvn.live/js/t.js\|data-site" .`
(hors `node_modules`).
- Ne pas ajouter `async` : `defer` suffit et préserve l'ordre.
## Étape 2 — Détecter le framework et insérer
Détectez le framework par la présence de fichiers, dans cet ordre, et
appliquez la variante correspondante.
### Next.js (`next.config.*` présent)
App Router (`app/layout.tsx` ou `.jsx`) — utilisez `next/script` :
```tsx
import Script from "next/script";
// dans le JSX de RootLayout, à l'intérieur de :
```
Pages Router (`pages/_document.tsx` ou à défaut `pages/_app.tsx`) : balise
`
```
`is:inline` empêche Astro de bundler/déplacer le script (les attributs
`data-*` doivent rester sur la balise).
### SvelteKit (`svelte.config.*` présent)
Dans `src/app.html`, avant `` :
```html
```
### SPA Vite/CRA (React, Vue, Svelte sans SSR — `index.html` à la racine ou dans `public/`)
Dans le `index.html` du projet, avant `` (même snippet que SvelteKit).
### WordPress / PHP
Dans le template d'en-tête (`header.php` du thème, ou via un plugin
d'insertion de code), avant `` (même snippet).
### HTML statique (aucun framework détecté)
Dans **chaque** fichier `.html` servi, avant `` (même snippet).
## Étape 3 — Options selon le contexte
- **SPA** : activé par défaut, rien à faire (les navigations
`pushState`/`replaceState`/`popstate` sont suivies). N'ajoutez
`data-spa="off"` que sur demande explicite.
- **Adblockers / proxy first-party** : si l'humain le demande, suivez
[Proxy first-party](/integration/proxy-first-party/) et utilisez
`src="/stats/t.js" data-api="/stats/e"` à la place des URLs elvn.live.
- **Test local** : le tracker est muet sur localhost. Pour tester en local
uniquement, ajoutez temporairement l'attribut `data-dev` — et **retirez-le
avant commit/production**.
- **CSP** : si le projet définit une Content Security Policy, ajoutez
`https://elvn.live` à `script-src` et à `connect-src`.
- **Événements custom** : uniquement sur demande — voir
[Événements & goals](/integration/evenements-goals/). Jamais de donnée
personnelle dans les props.
- **Bandeau cookies** : n'en ajoutez pas pour Elvn (aucun cookie, exemption
CNIL). Ne conditionnez pas le chargement du script à un consentement.
## Étape 4 — Checklist de vérification
Exécutez dans l'ordre. Les étapes 1 à 3 sont automatisables ; la 4 exige
l'humain ou un déploiement réel.
```sh
# 1. Le script Elvn est joignable (attendu : HTTP/2 200, content-type text/javascript, ETag)
curl -sI https://elvn.live/js/t.js | head -5
# 2. L'endpoint de collecte répond (attendu : 202 — ATTENTION : 202 ne signifie
# PAS que l'événement est compté ; l'endpoint répond 202 même aux payloads
# rejetés, par design)
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://elvn.live/api/e --data '{}'
# 3. Le snippet est présent exactement une fois dans le HTML produit
# (adapter la commande de build/serve au projet)
grep -c "data-site=\"SITE_PUBLIC_ID\"" # attendu : 1
```
4. **Vérification finale (la seule qui fasse foi)** : déployer, visiter une
page depuis le domaine de production, puis constater dans le dashboard
Elvn que **Settings → Tracking** passe au vert et que la visite apparaît
dans **Realtime** (~30-60 s de latence). Si rien n'apparaît alors que le
`POST /api/e` part bien en 202, la cause la plus probable est un
**domaine visité différent du domaine déclaré** dans le dashboard —
demandez à l'humain de vérifier.
## Pièges connus
| Symptôme | Cause probable | Correction |
| --- | --- | --- |
| Aucune requête `POST /api/e` ne part | Test sur localhost sans `data-dev` ; ou page en iframe ; ou navigateur automatisé (`navigator.webdriver`) | Tester depuis le domaine réel, hors iframe, dans un navigateur normal |
| `POST /api/e` part en 202 mais rien dans Realtime | Domaine visité ≠ domaine déclaré ; `data-site` erroné ; signal GPC actif ; adblocker sur `t.js` | Vérifier le domaine du site dans le dashboard, l'ID, désactiver GPC/adblock pour le test |
| Le script ne charge pas (bloqué) | Adblocker | [Proxy first-party](/integration/proxy-first-party/) |
| Événement custom jamais reçu | Nom invalide (majuscules, espaces, accents) — abandonné en silence | Nom conforme à `/^[a-z0-9_-]{1,64}$/`, props plates et scalaires |
| Double comptage | Snippet inséré deux fois (layout + page) | Une seule occurrence |
---