# Suivi côté navigateur (`t.js`) — guide d'instrumentation
Comment envoyer des données depuis un site web vers dataAnalytics. Ce document sert **à la fois de
référence pour un développeur** et de **consignes pour une IA** chargée d'instrumenter un site.
Le suivi passe par un unique script (`t.js`) : léger, asynchrone, **sans cookie**. Le comportement
décrit ici est celui réellement implémenté dans `apps/ingestion/public/t.js` — voir aussi le contrat
`docs/api-contract.md` §Ingestion.
---
## 1. Installation
Le backoffice génère le snippet exact pour chaque site. Il se colle dans le `
` :
```html
```
- La 1ʳᵉ ligne crée une **file d'attente** : les `da(...)` appelés avant le chargement du script ne
sont pas perdus.
- `data-site` (**obligatoire**) : le token public du site (fourni par le backoffice).
- `data-endpoint` (optionnel) : URL de collecte personnalisée ; par défaut `origine-du-script/e`.
Le domaine doit être dans l'**allowlist** du site (l'apex et `www` sont autorisés automatiquement à la
création). Un événement provenant d'un autre domaine est ignoré silencieusement.
## 2. Ce qui est mesuré automatiquement — **rien à coder**
Dès que le script est chargé :
- **Pages vues** au chargement, **et** sur les navigations SPA (interception de `history.pushState` +
`popstate`) — utile pour React/Vue/etc.
- **UTM** lus depuis l'URL (`?utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`).
- **Référent** (réduit à son hôte côté serveur), **langue** du navigateur, **largeur d'écran**.
⇒ Le trafic, les sources, les pays (via IP côté serveur, IP jetée ensuite), les appareils et les
sessions remontent **sans instrumentation**. Le travail d'instrumentation ne concerne donc que les
**objectifs** (conversions) et le **revenu**.
## 3. Objectifs (événements nommés)
Un objectif est un **événement nommé**. Deux façons de le déclencher.
### a. Déclaratif — pour un clic (préféré, sans JavaScript)
Ajouter `data-goal` (et éventuellement des `data-goal-`) sur l'élément cliquable :
```html
```
Au clic (capté par délégation, fonctionne aussi sur un enfant du bouton), ceci déclenche l'objectif
`signup` avec les propriétés `{ plan: "pro", location: "hero" }` (les tirets des attributs deviennent
des `_`).
### b. Programmatique — `da(name, props)`
Pour un objectif déclenché par du code (après une réponse serveur, une étape validée…) :
```js
da('signup') // objectif simple
da('signup', { plan: 'pro', method: 'google' }) // avec propriétés
da('scroll_depth', 80) // 2ᵉ argument NUMBER → champ `value` (entier)
```
> Règle : si le 2ᵉ argument est un **nombre**, il est envoyé dans `value` (ex. profondeur de scroll).
> Si c'est un **objet**, ses paires deviennent des propriétés (`props`).
**Quand utiliser quoi** : `data-goal` pour une **intention** (un clic) ; `da(...)` pour une
**conversion confirmée** (après le succès réel — inscription validée, paiement confirmé sur la page de
remerciement). Ne compte pas un « achat » sur le simple clic du bouton Payer.
## 4. Revenu
Le revenu est une propriété **réservée** `revenue` (un nombre, en unité monétaire — €). Il est extrait
dans un champ dédié (stocké en centimes côté serveur) ; les autres propriétés restent libres :
```js
da('purchase', { revenue: 49.90, plan: 'pro' })
```
À déclencher sur la **confirmation** de paiement (page de succès / webhook front), pas au clic. Le
revenu est attribué à la source/UTM de la session en cours (cookieless, côté client).
## 5. Le contrat — limites à respecter
| Champ | Règle |
| --- | --- |
| Nom d'objectif (`name`) | ≤ 64 caractères |
| Propriétés (`props`) | **≤ 10 paires** ; clés ≤ 64 ; **valeurs = chaînes de caractères** ≤ 255 |
| `revenue` | nombre ≥ 0, ≤ 1 000 000 |
| `value` | entier (2ᵉ argument numérique) |
Points importants :
- **Les valeurs de propriété doivent être des chaînes.** `da('signup', { plan: 'pro' })` ✅ ;
`da('signup', { seats: 5 })` ❌ (à écrire `{ seats: '5' }`). Un événement invalide est **rejeté
silencieusement** (l'ingestion répond toujours `204`, mais rien n'est stocké).
- **Nommage** : `snake_case`, minuscules, stable dans le temps (ex. `signup`, `add_to_cart`,
`purchase`). Un nom = une ligne dans l'onglet **Conversions › Objectifs**.
- **Cardinalité** : ne mets pas de valeurs uniques par utilisateur en propriété (id, email, timestamp).
Utilise des **catégories** (`plan=pro`, `location=hero`) — c'est ce qui rend le drill-in par
propriété lisible dans le backoffice.
## 6. Configuration par défaut (jeu d'objectifs standard)
Pour obtenir des données exploitables **immédiatement**, instrumenter ce socle commun quand les
éléments correspondants existent sur le site. Noms **canoniques** (à réutiliser tels quels) :
| Objectif | Quand le déclencher | Propriétés suggérées |
| --- | --- | --- |
| `signup` | inscription / création de compte réussie | `plan`, `method` (google/email) |
| `login` | connexion réussie | `method` |
| `lead` | formulaire soumis (contact, démo, newsletter) | `form` (contact/newsletter/demo) |
| `cta_click` | clic sur un appel à l'action clé | `location` (hero/nav/pricing/footer) |
| `add_to_cart` | ajout au panier | `product` (catégorie, pas un id unique) |
| `checkout_start` | début du paiement | — |
| `purchase` | **paiement confirmé** | `revenue` (obligatoire), `plan` |
| `contact` | clic sur email / téléphone | `channel` (email/phone) |
Ce socle alimente directement le backoffice : onglet **Conversions** (compte + visiteurs par objectif,
drill-in par propriété), **Revenu** (via `purchase`+`revenue`), **Acquisition/Campagnes** (via les UTM
automatiques). On peut ensuite **épingler** un objectif comme « KPI n°1 » (carte en tête de la vue
d'ensemble) — typiquement `signup` ou `purchase`.
## 7. Playbook pour une IA qui instrumente un site
Objectif : poser la **configuration par défaut** (§6) sans instrumentation superflue.
1. **Ne rien coder pour les pages vues, sources, UTM, appareils** — c'est automatique (§2).
2. **Repérer les conversions** de la config par défaut présentes sur le site (bouton d'inscription,
formulaire de contact, tunnel d'achat, CTA du hero…).
3. **Clics → déclaratif** : ajouter `data-goal=""` (+ `data-goal-` pour les
catégories) sur l'élément. Préférer ça à du JS quand l'objectif = un clic.
4. **Succès confirmés → programmatique** : sur la page/état de succès réel, appeler
`da('', { … })`. En particulier `da('purchase', { revenue: , plan: '' })` sur la
page de confirmation de paiement — **jamais** au clic du bouton Payer.
5. **Respecter le contrat (§5)** : noms `snake_case`, ≤ 10 props, **valeurs de props en chaînes**,
pas de valeurs à haute cardinalité.
6. **Ne pas ré-instrumenter** un objectif déjà couvert par un `data-goal` en ajoutant aussi un `da()`
(double comptage).
7. En cas de doute sur un nom, réutiliser un nom de la config par défaut plutôt que d'en inventer un.
### Exemple minimal complet
```html
Commencer
```
## 8. Vérifier que ça marche, et dépanner
**Vérification en 30 secondes** : installe le snippet, visite le site, puis ouvre le backoffice →
onglet **Temps réel**. La visite doit apparaître en quelques secondes. Au tout premier événement reçu,
le site passe automatiquement de « en attente » à **vérifié** et les statistiques se débloquent.
**Vérifier depuis le navigateur** : onglet *Réseau* des outils de développement, filtre `t.js`. Tu
dois voir le script se charger, puis une requête `POST` vers `/e` répondant **204**. Pour tester un
objectif à la main, tape dans la console : `da('test_manuel')` → une nouvelle requête doit partir.
**Point crucial — l'ingestion répond TOUJOURS `204`**, même quand elle rejette l'événement (c'est
volontaire : ne donner aucun signal exploitable à un attaquant). Donc « `204` reçu » ne veut pas dire
« enregistré ». Si rien n'apparaît dans le backoffice, les causes possibles sont :
| Symptôme | Cause probable |
| --- | --- |
| Aucune requête `/e` dans l'onglet Réseau | Le snippet n'est pas chargé (vérifie le ``) |
| `POST /e` en erreur `ERR_BLOCKED_BY_CLIENT` | Un bloqueur de publicité de **ton** navigateur ; la requête ne part pas. Rien à corriger côté site — vérifie en navigation privée sans extension |
| Requêtes `204` mais rien dans le backoffice | Le domaine du site n'est pas dans l'**allowlist** (ex. site en `.com` alors que le site enregistré est en `.fr`), ou le `data-site` est erroné |
| Les pages vues remontent, mais pas un objectif | Événement invalide : nom > 64 caractères, plus de 10 propriétés, ou **valeur de propriété qui n'est pas une chaîne** (voir §5) |
| Rien depuis un sous-domaine (ex. `boutique.exemple.com`) | Ce sous-domaine doit être ajouté à l'allowlist du site (seuls l'apex et `www` le sont d'office) |
**Bloqueurs de publicité** : une partie du trafic manquera toujours, quel que soit l'outil d'analytics.
Le blocage a lieu **dans le navigateur du visiteur** — la requête ne part jamais
(`ERR_BLOCKED_BY_CLIENT` dans la console) et n'atteint aucun serveur. Il n'y a donc rien à corriger
côté installation, et la mesure ne peut pas « rattraper » ces visites.
Le script et l'endpoint portent des noms neutres pour ne pas déclencher les règles génériques des
listes de filtres (`/collect`, l'endpoint de Google Analytics, y figure tel quel) : ça limite la perte
sans l'éliminer. En pratique, les **tendances restent fiables** — la perte est à peu près constante
dans le temps — c'est le **niveau absolu** qui est sous-estimé, et davantage sur les audiences
techniques et sur desktop que sur mobile.