← Accueil
⚙️

Documentation Technique — Impact Trackers

📖 Glossaire →

Ce document explique comment Impact Trackers fonctionne techniquement : comment les trackers sont détectés (regex, patterns réseau), comment l'analyse se déroule étape par étape, comment la performance web est mesurée, et comment l'éco-conception est calculée indépendamment.

Public cible : développeurs, auditeurs techniques, curieux avancés.

Sommaire

🏗️

Architecture Générale

Impact Trackers est composé de trois composants indépendants :

🌐 Frontend (Nuxt.js)

Interface utilisateur Vue 3 / Nuxt. Envoie une requête POST /api/analyze et lit la réponse en streaming SSE (Server-Sent Events) pour afficher la progression en temps réel.

Pages : index.vue, resultat/, definitions.vue, technique.vue

⚙️ Backend (FastAPI + Python)

Orchestrateur principal. Reçoit les requêtes, lance Playwright, coordonne tous les services d'analyse et persiste les résultats en base de données PostgreSQL.

Fichier principal : main.py → route /api/analyzeanalyzer.py

🗄️ Base de données (PostgreSQL)

Stockage des analyses complètes (trackers, cookies, scores, données personnelles) via SQLAlchemy async. Permet de retrouver les analyses récentes et de partager les résultats via un ID unique.

🧩 Extension Chrome (Companion)

Composant indépendant du backend Impact Trackers. Elle analyse l'HTML des sites via un fetch côté background script et écoute passivement le trafic réseau via chrome.webRequest.onBeforeRequest. Elle dispose de sa propre base de regex (plus simple) dans background.js.

🔄

Pipeline d'Analyse — Les 8 Étapes

Quand une URL est soumise, le backend analyzer.py orchestre une série d'étapes séquentielles. Chaque étape émet un événement SSE au frontend pour mettre à jour la barre de progression.

1
Initialisation (5%)

Génération d'un UUID unique pour l'analyse. Initialisation des variables. Normalisation de l'URL (ajout de https:// si absent).

2
Robots.txt (15%)

Le service robots_parser.py télécharge et analyse le fichier /robots.txt du site. Il vérifie si la directive Disallow: / est présente pour User-agent: *. Si oui, le site est marqué comme "scrap interdit". Cette détection influence la décision d'afficher ou masquer les résultats.

3
Headers HTTP & Cookies statiques (30%)

Une requête GET est effectuée via httpx (client HTTP asynchrone, sans JavaScript). Les headers de réponse sont analysés (server, x-powered-by, content-security-policy, strict-transport-security). Les cookies définis directement dans les headers Set-Cookie sont extraits pour l'analyse ultérieure.

httpx — Requête initiale
async with httpx.AsyncClient(
    timeout=15.0,
    follow_redirects=True,
    headers={"User-Agent": HTTP_USER_AGENT},
) as client:
    response = await client.get(url)
    headers = dict(response.headers)
    raw_set_cookies = response.headers.get_list("set-cookie")
4
Playwright — Navigateur Headless (50%)

C'est l'étape la plus longue. Un navigateur Chromium headless (invisible) charge la page en deux phases distinctes. Voir la section dédiée pour les détails des délais.

5
Détection des Trackers (70%)

La fonction detect_trackers(html, network_requests) du module tracker_detector.py applique toutes les regex de la base de données sur le HTML et les URLs réseau. Chaque tracker détecté est enrichi avec sa géolocalisation IP, l'analyse de son payload réseau, et son statut de consentement (pré/post/exempté).

6
Données Personnelles (85%)

Le module personal_data.py croise les trackers et les cookies détectés pour lister les catégories de données personnelles effectivement collectées (ip_address, unique_client_id, behavioral_profile, etc.).

7
Tracking Serveur (92%)

Le module server_side_detector.py analyse les patterns réseaux, headers et sous-domaines pour détecter un éventuel tracking côté serveur (GTM SS, CNAME cloaking, Measurement Protocol).

8
Scoring & Éco-conception (97%)

Calcul du score de confidentialité (0–100), de l'impact performance (Main Thread cumulé), et du bilan éco-conception (EcoIndex global vs. propre, GES, eau). Puis sauvegarde en base de données et envoi de l'événement SSE final type: "done".

🎭

Playwright — Fonctionnement & Délais d'Attente

Playwright est une bibliothèque Python qui pilote un navigateur Chromium en mode headless (sans interface graphique). C'est le seul moyen fiable de détecter les trackers chargés dynamiquement par JavaScript (ce qu'une simple requête HTTP ne peut pas faire).

Pourquoi des délais d'attente ? Le problème du chargement asynchrone

Les scripts de tracking modernes ne se chargent pas tous immédiatement. Beaucoup sont chargés :

  • Via un gestionnaire de balises (GTM) qui s'exécute après le DOM ;
  • De manière lazy (au scroll, après un délai, au clic) ;
  • Après l'acceptation du bandeau CMP ;
  • Uniquement après que le framework JavaScript (React, Vue) a fini son initialisation.

Pour capturer tous ces cas, Playwright suit le séquencement précis suivant :

Phase 1 — Chargement initial (Pre-consentement)

page.goto(url, wait_until="networkidle") : Playwright attend que le réseau soit au repos (networkidle = aucune requête réseau en cours depuis 500 ms).

Fallback : Si networkidle échoue (site trop actif), Playwright bascule sur wait_until="domcontentloaded".

Ensuite : attente de 2 000 ms supplémentaires pour laisser les scripts asynchrones s'initialiser (GTM, listeners, lazy-load initial).

⏱️ Timeout max : 30 000 ms (configurable via PLAYWRIGHT_TIMEOUT)
📸 Snapshot Pre-consentement

Les URLs réseau et les cookies à ce moment précis sont sauvegardés dans requests_pre et cookies_pre. Ce snapshot sert à identifier les trackers chargés sans consentement.

Phase 2 — CMP & Activité Utilisateur (Post-consentement)

1. Clic CMP automatique : Impact Trackers tente de cliquer sur le bouton "Accepter tout" du bandeau cookies via une liste de 15+ sélecteurs CSS (Didomi, Axeptio, OneTrust, Cookiebot, sélecteurs génériques) + filtres textuels.

Après un clic réussi : attente de 3 000 ms pour que les scripts post-consentement s'initialisent.

Boucle d'attente CMP : 10 tentatives × 500 ms = 5 secondes max.

2. Simulation d'activité (_simulate_user_activity) : Scrolls progressifs (0 → 33% → 66% → 100% → retour à 0) + mouvements de souris simulés. Cela déclenche les scripts de lazy-loading (tracking du scroll, heatmaps, etc.).

3. Délai de sécurité post-consent : 2 500 ms supplémentaires pour les cookies et requêtes asynchrones post-consentement.

📸 Capture finale

Récupération du HTML final (après exécution JavaScript complète) via page.content(). Toutes les requêtes réseau cumulées (pre + post) sont dans requests_all.

Résumé des délais (analyzer.py)
# Phase 1 : Chargement initial
await page.goto(url, timeout=30000, wait_until="networkidle")
await page.wait_for_timeout(2000)       # ← +2s pour scripts asynchrones

# Snapshot pre-consentement
requests_pre = list(network_requests)
cookies_pre  = await context.cookies()

# Phase 2 : Post-consentement
await _click_consent_banner(page)       # ← 10 tentatives × 500ms + 3000ms après clic
await _simulate_user_activity(page)     # ← Scrolls + mouvements souris

# Snapshot final
html = await page.content()
await page.wait_for_timeout(2500)       # ← +2.5s pour cookies asynchrones
cookies_all = await context.cookies()
requests_all = list(network_requests)

Configuration Playwright :

Contexte navigateur simulé
context = await browser.new_context(
    user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
    viewport={"width": 1280, "height": 800},
    locale="fr-FR",
    java_script_enabled=True,
    ignore_https_errors=True,
)
# Arguments anti-détection bot
args=[
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-blink-features=AutomationControlled",  # ← Cache le flag navigator.webdriver
]
🔍

Mécanisme de Détection des Trackers

La détection est réalisée dans tracker_detector.py via la fonction detect_trackers(html, network_requests). Elle croise deux sources de données pour maximiser la couverture.

Source 1 — HTML statique

Deux stratégies sont appliquées :

  1. Extraction des src de balises <script> : Toutes les URLs de scripts externes sont extraites via une regex simple :
    re.findall(r'<script[^>]+src=["\']([^"\']+)["\']', html)
    Chaque URL est ensuite testée contre les patterns du tracker.
  2. Recherche inline dans le HTML brut : Si aucun match n'est trouvé dans les src, la regex est appliquée directement sur le contenu HTML complet (pour détecter les snippets inline, les configurations JavaScript).

Source 2 — Requêtes réseau (Playwright)

Chaque URL chargée par le navigateur (scripts, pixels, images, requêtes XHR/Fetch) est testée contre toutes les regex. C'est cette source qui permet de détecter les trackers chargés après le chargement initial (via GTM, lazy, post-consentement) que le HTML statique ne contient pas.

Extraction de l'ID de tag

Certains trackers ont un tag_pattern en plus des patterns de détection. Ce pattern secondaire (regex avec groupe capturant) extrait l'ID spécifique du tracker dans le contenu analysé :

Exemples de tag_pattern
# Google Tag Manager → extrait "GTM-ABCD123"
"tag_pattern": r"GTM-([A-Z0-9]{4,7})"

# Google Analytics → extrait "G-XXXXXXXXX" ou "UA-XXXXXX-X"
"tag_pattern": r"(?:G|UA)-[A-Z0-9]+-?\d*"

# Meta Pixel → extrait l'ID numérique du pixel
"tag_pattern": r"fbq\('init',\s*'(\d+)'"

# LinkedIn Insight → extrait l'ID partenaire
"tag_pattern": r"_linkedin_partner_id\s*=\s*['\"]?(\d+)['\"]?"

Déduplication et priorisation

Un tracker n'est listé qu'une seule fois même si plusieurs de ses patterns matchent. Les patterns et URLs matchantes sont dédupliqués via list(dict.fromkeys(...)). Les URLs sont limitées à 10 pour ne pas surcharger le stockage.

Détection complémentaire par cookies

En plus de la détection HTML/réseau, Impact Trackers peut déduire des trackers à partir des cookies déposés. Si un cookie correspond à un tracker connu (ex: _fbp → Meta Pixel) mais que le tracker n'a pas été détecté par regex, il est ajouté à la liste avec detected_in: ["cookie"].

🕵️

Détection du Tracking Serveur — Patterns Techniques

Le module server_side_detector.py applique 7 méthodes de détection combinées pour calculer un score de confiance :

1
Headers HTTP suspects (+30 points)

Recherche de headers spécifiques dans la réponse HTTP : x-measurement-id, x-analytics, x-tracking, x-gtm-server, x-stape-config. Ces headers sont ajoutés par certaines configurations de tracking serveur custom.

2
Endpoints de collecte sur domaine propre (+30 points)

Requêtes vers des URLs du type monsite.fr/collect, monsite.fr/beacon, monsite.fr/analytics détectées dans le trafic réseau Playwright. Pattern : /collect(?:\?|$), /events?(?:\?|$), /mp/collect(?:\?|$) (GA4 Measurement Protocol), etc.

3
Paramètres Measurement Protocol (+35 points)

Détecte les paramètres GA/Mixpanel dans les URLs réseau :

r"[?&]tid=UA-"   # GA Universal
r"[?&]tid=G-"    # GA4
r"[?&]cid=[\w\-]+" # Client ID GA
r"[?&]v=1&"      # Measurement Protocol version
r"[?&]t=pageview" # Type de hit GA
r"[?&]en=page_view" # GA4 MP event name
4
CNAME Cloaking (+40 points)

Détecte des sous-domaines du site analysé dont le nom ressemble à du tracking : analytics.monsite.fr, tracking.monsite.fr, metrics.monsite.fr, collect.monsite.fr, stat.monsite.fr, sgtm.monsite.fr... via une liste de 14 patterns de sous-domaines.

5
GTM Server-Side (sGTM / Stape) (+35 points)

Patterns : sgtm., stape., server-side-tagging, server-container, servercontainer dans le HTML ou les URLs réseau. Stape est un hébergeur populaire de sGTM.

6
GTM présent mais peu de scripts tiers (+25 points)

Si GTM est détecté côté client mais que moins de 4 scripts tiers ont été chargés en réseau et que des paramètres Measurement Protocol sont présents, c'est un signe fort que GTM s'exécute principalement côté serveur (les tags sont déclenchés server-side, pas browser-side).

7
Beacon sans script client correspondant (+30 points)

Si des requêtes vers des endpoints de collecte connus (ex: google-analytics.com) sont détectées en réseau, mais qu'aucun script GA n'est chargé côté client, c'est un signe fort d'un hit GA4 envoyé depuis le serveur.

Calcul du verdict :

suspected = confidence_score >= 30  # Seuil minimal pour suspicion

if   confidence_score >= 65: confidence = "high"
elif confidence_score >= 35: confidence = "medium"
elif confidence_score > 0:   confidence = "low"
else:                         confidence = "none"
🌱

Éco-conception — Calcul Technique

L'éco-conception est calculée séparément et après la détection des trackers (dans l'étape 8 du pipeline). Elle est gérée entièrement dans analyzer.py, indépendamment du module tracker_detector.py.

Données d'entrée :

  • Nombre total de requêtes réseau Playwright (len(requests_all))
  • Taille HTML de la page (len(html) / 1024 en ko)
  • Nombre de nœuds DOM (estimé : max(380, len(html) / 120))
  • Profils éco de chaque tracker (table statique ECO_TRACKER_PROFILES dans analyzer.py)

Calcul EcoIndex officiel :

analyzer.py — Calcul EcoIndex via la lib officielle
from ecoindex.compute import compute_ecoindex

# 1. EcoIndex Global (état réel avec traceurs)
ecoindex_global = await compute_ecoindex(
    nodes   = global_dom_elements,
    size    = int(global_page_weight_kb * 1024),  # en octets
    requests = global_requests_count
)

# 2. EcoIndex Propre (hypothétique sans traceurs)
ecoindex_clean = await compute_ecoindex(
    nodes    = clean_dom,
    size     = int(clean_weight_kb * 1024),
    requests = clean_requests
)

Formule de repli (fallback) :

En cas d'échec de la bibliothèque officielle, une formule simplifiée est utilisée :

# Quantiles de référence EcoIndex
score_dom  = metric_score(dom_elements, q50=600,  q90=2400)
score_req  = metric_score(requests,     q50=35,   q90=120)
score_size = metric_score(size_kb,      q50=1000, q90=5000)

ecoindex = max(0, 100 - (3 * score_dom + 2 * score_req + 1 * score_size) / 6)

# GES et eau (formule EcoIndex officielle simplifiée)
ges   = 2.0 + 2.0 * (50.0 - ecoindex_score) / 100.0  # gCO2e
water = 0.5 + 0.5 * (50.0 - ecoindex_score) / 100.0  # cl

Points perdus EcoIndex dus aux traceurs :

ecoindex_points_lost = round(
    0.3 * trackers_requests_sum        # Impact requêtes réseau
  + 0.3 * (trackers_weight_sum / 100)  # Impact poids (ko)
  + 0.4 * (trackers_dom_sum / 100),    # Impact nœuds DOM
  1
)

Web Performance — Mesure Synthétique Double Passe (Lab Data)

Impact Trackers intègre un **nouveau moteur d'analyse synthétique automatisée (Lab Data)** qui remplace les estimations statistiques par de la mesure réelle et physique. Il exécute un protocole de test double passe (A/B Test) sous émulation et bridages stricts.

1. Paramètres stricts de l'Émulation Mobile

Le navigateur headless Chromium est configuré pour simuler précisément un smartphone moderne récent :

  • User-Agent : Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X)... Mobile/15E148 Safari/604.1
  • Viewport : 390x844 avec ratio de pixels de 3 et support du tactile (touch events).

2. Throttling CPU et Réseau (CDP)

Pour obtenir des mesures de performance stables et représentatives des conditions mobiles réelles, Impact Trackers se connecte directement à la session **CDP (Chrome DevTools Protocol)** de la page pour y injecter deux bridages physiques stricts :

  • CPU Throttling (Ralentissement 4x) : Émule la puissance calculatoire modérée d'un smartphone milieu de gamme.
  • Network Throttling (Réseau 3G/4G bridé) : Établit des conditions réseau fixes (Latence de 40ms, Download de 10 Mbps soit 1.25 Mo/s, Upload de 1.5 Mbps soit 187.5 Ko/s).
performance_engine.py — Session CDP and Throttling
cdp = await page.context.new_cdp_session(page)

# Throttling Réseau
await cdp.send("Network.emulateNetworkConditions", {
    "offline": False,
    "latency": 40,
    "downloadThroughput": (10 * 1024 * 1024) // 8,  # 10 Mbps
    "uploadThroughput": (1.5 * 1024 * 1024) // 8     # 1.5 Mbps
})

# Throttling CPU (4x)
await cdp.send("Emulation.setCPUThrottlingRate", {
    "rate": 4
})

3. Mesure par Double Passe (A/B Test)

Le calcul de l'impact réel des traceurs se fait par différence directe entre deux chargements de page successifs :

  • Passe A (Contrôle) : Chargement initial complet du site avec l'ensemble de ses traceurs et scripts tiers actifs sous throttling mobile. On mesure le TBT, le poids réseau et le temps de chargement.
  • Passe B (Bloquée) : Chargement du même site en interceptant à la volée toutes les requêtes réseau (via route.abort()) dont le domaine appartient à la liste des traceurs détectés, bloquant ainsi leur exécution. On enregistre les nouvelles métriques.
performance_engine.py — Interception active (Passe B)
async def intercept_route(route, request):
    req_url = request.url
    domain = urlparse(req_url).netloc
    
    # Si le domaine appartient aux traceurs détectés -> blocage
    if any(block_d in domain for block_d in liste_domaines_traceurs):
        await route.abort()
    else:
        await route.continue_()

await page.route("**/*", intercept_route)

4. Calcul Physique du TBT (Total Blocking Time)

Plutôt que d'estimer la charge CPU, Impact Trackers injecte dès le démarrage de chaque passe un script d'écoute de tâches longues (Long Tasks) via l'API PerformanceObserver native. Le TBT correspond au cumul de la partie supérieure à 50ms pour toutes les tâches bloquantes mesurées lors du chargement :

PerformanceObserver injecté (TBT)
window.tbtLongTasks = [];
new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        window.tbtLongTasks.push({
            startTime: entry.startTime,
            duration: entry.duration
        });
    }
}).observe({ entryTypes: ["longtask"] });

5. File d'Attente FIFO et Garantie CPU

Comme le throttling CPU simule une réduction logicielle de la puissance du processeur, toute autre tâche en parallèle sur le serveur hôte fausserait les calculs. Impact Trackers intègre donc un **verrou d'exclusion mutuelle asynchrone (Mutex)** qui force une file d'attente FIFO stricte (concurrence = 1). Une seule analyse synthétique s'exécute à la fois sur le serveur, lui garantissant 100% de la ressource processeur.

📊

Score de Confidentialité — Algorithme

analyzer.py — _calculate_privacy_score()
score = 100  # Départ

# Pénalités par niveau de risque (hors CMP et exemptés CNIL)
score -= min(very_high_count * 20, 60)   # Max -60
score -= min(high_count * 10, 40)        # Max -40
score -= min(medium_count * 5, 20)       # Max -20

# Pénalité session recording (Hotjar, Clarity, Contentsquare)
if has_session_recording: score -= 15

# Pas de CMP + trackers invasifs
if not has_cmp and has_invasive_trackers: score -= 10

# Plus de 10 cookies tiers
if len(third_party_cookies) > 10: score -= 5

# Tracking server-side suspecté
if server_side.suspected: score -= 10

return max(0, score)  # Minimum 0

Catégories de score :

🧩

Extension Chrome — Architecture & Détection

L'extension Chrome est totalement indépendante du backend Impact Trackers. Elle embarque ses propres regex dans background.js et utilise deux méthodes de détection :

Méthode 1 — Sniffer réseau passif

Le background script écoute en permanence toutes les requêtes réseau via chrome.webRequest.onBeforeRequest. Pour chaque requête, il teste l'URL contre les regex de tous les trackers connus. C'est une écoute non-bloquante : elle ne ralentit pas le chargement des pages.

background.js — Sniffer réseau passif
chrome.webRequest.onBeforeRequest.addListener(
  function(details) {
    const tabId = details.tabId;
    if (tabId < 0) return;  // Ignorer les onglets "background"

    TRACKER_PATTERNS.forEach((tracker) => {
      // Regex locale pour éviter les effets de bord du mode /g
      const regex = new RegExp(tracker.rule.source, "i");
      if (regex.test(details.url)) {
        if (!detectedTrackers[tabId]) {
          detectedTrackers[tabId] = new Set();  // Set = pas de doublons
        }
        const beforeSize = detectedTrackers[tabId].size;
        detectedTrackers[tabId].add(tracker.id);

        // Notifier le Side Panel seulement si c'est un NOUVEAU tracker
        if (detectedTrackers[tabId].size > beforeSize) {
          chrome.runtime.sendMessage({
            action: "NETWORK_TRACKER_FOUND",
            tabId, trackerId: tracker.id, trackerName: tracker.name
          });
        }
      }
    });
  },
  { urls: ["<all_urls>"] }
);

Méthode 2 — Analyse HTML par fetch

Quand une URL de site tiers est saisie manuellement, le background script effectue un fetch() de l'URL et applique les regex sur le HTML reçu. Cette méthode est complémentaire car le sniffer réseau ne capture que les requêtes postérieures à l'ouverture du Side Panel.

Boucle de détection LinkedIn (content.js) :

Pour détecter les changements de page sur LinkedIn (SPA — Single Page Application), le content script utilise un setInterval qui vérifie toutes les 1 000 ms si le window.location.pathname a changé. Si oui, il notifie le background.

content.js — Surveillance navigation SPA LinkedIn
let lastPath = window.location.pathname;
const intervalId = setInterval(() => {
  // Sécurité : arrêt si contexte d'extension invalidé
  if (!chrome.runtime || !chrome.runtime.id) {
    clearInterval(intervalId);
    return;
  }
  if (window.location.pathname !== lastPath) {
    lastPath = window.location.pathname;
    if (window.location.hostname.includes("linkedin.com")) {
      chrome.runtime.sendMessage({
        action: "url_changed",
        url: window.location.href
      });
    }
  }
}, 1000);  // ← Vérification toutes les secondes

Boucle de retry pour le scan LinkedIn :

Les données LinkedIn sont extraites via le JSON interne de l'API Voyager (objets <code> cachés dans le DOM) ou par scraping DOM. Comme la page SPA ne recharge pas entre les profils, les données peuvent être "périmées". Une logique de retry est implémentée :

sidepanel.js — Retry de scan
const tryScan = (retryCount) => {
  chrome.tabs.sendMessage(activeTab.id, {action: "scan_profile"}, (response) => {
    if (chrome.runtime.lastError) {
      if (retryCount < 3) {
        setTimeout(() => tryScan(retryCount + 1), 1000);  // Retry toutes les secondes
      } else {
        setStatus("Prêt (ouvrez un profil)");  // Abandon après 3 tentatives
      }
      return;
    }
    if (response && response.success) {
      // Afficher les données
    } else if (retryCount < 3) {
      setTimeout(() => tryScan(retryCount + 1), 1500);
    }
  });
};
setTimeout(() => tryScan(0), 1200);  // Premier essai après 1.2s (DOM LinkedIn)