Tutoriel
Conserver gclid et UTM avant consentement : la méthode GTM
Gardez gclid et UTM sur les pages visitées avant consentement, sans cookie ni localStorage : balise GTM commentée, pièges GA4 et recette de test.
Dans ce tutoriel
Un visiteur qui arrive par une annonce avec gclid ou utm_*, puis navigue avant d’accepter le bandeau, perd ces paramètres dès sa deuxième page, et la session part en (direct). La solution la plus propre n’est pas de stocker ces valeurs dans le navigateur avant consentement, car c’est un dépôt de traceur. Elle consiste à recopier les paramètres d’attribution dans les liens internes tant que le visiteur n’a pas répondu, pour que la page qu’il accepte plus tard porte encore l’information.
Le problème est le même que celui décrit dans l’article sur la bannière centrale ou discrète. Ici, je détaille la méthode complète, utile si vous gardez une bannière discrète sur un site en production. Si votre dispositif de consentement est à reprendre, je le fais en tant que consultant Consent Mode v2.
Avant de commencer
- Accès éditeur au conteneur GTM web
- Une CMP qui expose l'état du consentement (événement dataLayer ou API)
- Consent Mode v2 déjà en place
- Un site dont les liens internes sont des <a href> classiques (pas uniquement du routeur SPA)
Le problème, vu depuis GA4
Dans GA4, la source d’une session est lue sur le premier événement de la session. Si vos balises ne se chargent qu’après consentement, ce premier événement part de la page où le visiteur clique sur « Accepter ». Si cette page n’a plus les paramètres d’arrivée, la session perd sa source. Le gclid est le plus touché : il n’existe que dans l’URL d’arrivée, et sans consentement sur la page d’arrivée, aucun cookie _gcl_aw ne le relaie. Les détails sont dans les erreurs sur les click identifiers.
Google documente le cas inverse : avec l’auto-tagging, le gclid a la priorité sur les paramètres manuels, et les utm_* ne servent que lorsque le gclid est absent. Si le gclid disparaît de l’URL, c’est donc tout le classement de la session qui bascule.
Pourquoi on ne stocke rien avant consentement
La tentation est de lire le gclid à l’arrivée, de le mettre dans un cookie ou un localStorage, puis de le relire plus tard. C’est précisément un dépôt et une lecture d’information sur le terminal de l’utilisateur. L’article 82 de la loi Informatique et Libertés soumet cette opération au consentement, sauf exemption. La CNIL inclut le localStorage dans le périmètre de ses lignes directrices, et une exemption mesure d’audience ne couvre pas, à ma lecture, un identifiant publicitaire conservé pour être relu plus tard.
La méthode ci-dessous ne stocke rien. Les paramètres restent en mémoire pendant la navigation, puis ils voyagent dans les URL que le navigateur demande déjà. Cette lecture reste à valider avec votre DPO, comme toute interprétation juridique.
La méthode en trois temps
- Au chargement, la balise lit
location.searchet ne retient que les paramètres d’attribution :gclid,gbraid,wbraid,fbclid,msclkidet lesutm_*. - Tant que le consentement n’est pas accordé, chaque clic sur un lien interne (même hôte) reçoit ces paramètres, s’ils ne sont pas déjà présents.
- Dès que le consentement est accordé, la propagation s’arrête. Les liens déjà réécrits restent tels quels, la navigation suivante est normale.
Le code GTM
Balise « HTML personnalisé », déclencheur « All Pages » (chargement initial). Le code ne touche ni aux cookies, ni au localStorage, ni au sessionStorage.
<script>
// Balise HTML GTM, déclencheur : All Pages.
// N'écrit rien dans le navigateur. Paramètres gardés en mémoire seulement.
(function () {
if (window.__attrPropagation) return; // déjà initialisé
// 1. Paramètres d'attribution présents à l'arrivée
var ATTR_KEYS = ['gclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid'];
var attr = new URLSearchParams();
new URLSearchParams(window.location.search).forEach(function (value, key) {
if (ATTR_KEYS.indexOf(key) !== -1 || key.indexOf('utm_') === 0) {
attr.append(key, value);
}
});
if (!attr.toString()) return; // rien à propager
window.__attrPropagation = true;
// 2. Réécriture d'un lien interne
function decorate(link) {
if (window.__attrStop) return; // consentement accordé
var raw = link.getAttribute('href');
if (!raw || raw.charAt(0) === '#') return; // ancres
var target;
try { target = new URL(raw, window.location.href); } catch (e) { return; }
if (target.hostname !== window.location.hostname) return; // externe : jamais
if (target.protocol !== 'https:' && target.protocol !== 'http:') return;
attr.forEach(function (value, key) {
if (!target.searchParams.has(key)) target.searchParams.set(key, value);
});
link.setAttribute('href', target.href);
}
// 3. Liens présents au chargement
document.querySelectorAll('a[href]').forEach(decorate);
// 4. Liens ajoutés ensuite (menus, AJAX, SPA)
new MutationObserver(function (mutations) {
mutations.forEach(function (m) {
m.addedNodes.forEach(function (node) {
if (node.nodeType !== 1) return;
if (node.matches && node.matches('a[href]')) decorate(node);
if (node.querySelectorAll) node.querySelectorAll('a[href]').forEach(decorate);
});
});
}).observe(document.documentElement, { childList: true, subtree: true });
// 5. Filet de sécurité au clic, juste avant la navigation
document.addEventListener('click', function (e) {
var a = e.target.closest && e.target.closest('a[href]');
if (a) decorate(a);
}, true);
})();
</script>
Ce que fait le code, point par point :
- Le filtre
hostnameexclut les liens externes. Sans lui, votregclidpartirait chez des tiers. - Le
has()ne remplace jamais un paramètre déjà présent sur le lien, donc un lien interne déjà taggé gagne. - Le clic en phase de capture rattrape les liens construits d’une manière que l’observateur ne voit pas.
Quand l’arrêter
Créez un déclencheur « Événement personnalisé » sur l’événement que votre CMP pousse à l’acceptation (son nom dépend de la CMP, à vérifier dans sa documentation). La balise associée tient en une ligne :
<script>window.__attrStop = true;</script>
Une fois le flag posé, decorate ne réécrit plus rien. Le refus ne l’active pas : sans consentement, la propagation continue, car elle ne dépose rien.
Les pièges
- Formulaires GET. Le navigateur remplace la query string de l’action par les champs du formulaire. Les paramètres d’attribution disparaissent à la soumission. Ajoutez-les en
<input type="hidden">ou passez le formulaire en POST si votre back-end le permet. - Liens externes et sous-domaines. Le code traite un seul hôte. Si votre site vit sur plusieurs domaines, listez-les explicitement plutôt que d’élargir le filtre.
- Canonical. Votre balise
<link rel="canonical">doit rester propre. Ne la réécrivez pas avec les paramètres. - Doublons dans GA4. Une même page apparaît sous plusieurs URL dans les rapports qui utilisent l’URL complète. Dans vos rapports de pages, préférez la dimension de chemin de page, qui ignore la query string. Je n’ai pas trouvé de réglage GA4 équivalent à l’ancienne liste d’exclusion de paramètres d’Universal Analytics : vérifiez l’interface de votre propriété avant de compter sur lui.
- Applications monopage (SPA). Le code lit les paramètres au premier chargement. Si votre routeur réécrit la query string à la navigation, la valeur retenue au chargement reste valable, mais vérifiez que le routeur ne supprime pas les paramètres avant l’exécution de votre balise.
- Ancres et liens vides. Ignorés volontairement : une ancre n’envoie aucune requête qui pourrait porter l’attribution.
Ce que la méthode ne fait pas
Elle ne résout pas tout, et il vaut mieux le savoir avant de la déployer. Elle ne rattrape que les visiteurs qui naviguent sur votre propre site avant de répondre. Un visiteur qui ferme l’onglet après la landing, ou qui revient plus tard par un autre appareil, reste hors de portée. Elle ne couvre pas non plus le pixel Meta ou LinkedIn : ces outils attendent un consentement explicite avant tout envoi, et les paramètres propagés dans les URL ne changent rien à cela. Pour ces plateformes, la perte reste entière, et seule une bannière qui recueille le consentement avant lecture la réduit.
Elle ne corrige pas non plus une landing mal taguée. Si une campagne Google Ads pointe vers une URL sans auto-tagging ni utm_*, aucun code ne retrouvera la source. Vérifiez ce point dans les URL finales de vos annonces avant tout autre réglage.
Enfin, les boutons qui naviguent via window.location sans balise <a href> ne sont pas traités par le code.
Mesurer l’effet avant de généraliser
Avant de généraliser la balise à tout le site, comparez deux semaines d’avant et deux semaines d’après sur trois indicateurs. Le premier est la part de sessions (direct) / (none) provenant d’une URL d’arrivée qui contenait un gclid ou un utm_*. Ce chiffre se calcule en croisant vos logs serveur, ou les données de votre CMP, avec les sessions GA4. Le deuxième est le nombre de clics facturés par Google Ads sur la même période, que vous rapprochez des sessions google / cpc dans GA4. Le troisième est le nombre de conversions importées vers Google Ads, qui doit remonter si la propagation fonctionne.
Gardez en tête un biais : une partie de la baisse du (direct) peut venir d’autres changements, comme une modification de la bannière ou une saisonnalité de trafic. Pour isoler l’effet, déployez la balise sur une partie du trafic seulement, par exemple sur les landings payantes, et gardez le reste du site en l’état. C’est la même logique que pour un test de bannière, décrite dans A/B tester sa bannière CMP.
Recette
- Ouvrez une fenêtre de navigation privée, puis Tag Assistant sur l’URL
?gclid=TEST&utm_source=test. - Dans la console, vérifiez que
window.__attrPropagationvauttrue. - Dans l’onglet Application, constatez qu’aucun cookie ni aucune entrée de
localStoragen’est créée par la balise. Seuls ceux de votre CMP doivent apparaître. - Cliquez sur un lien interne : l’URL cible doit contenir
gclid=TESTetutm_source=test. Testez aussi un lien ajouté en JavaScript. - Acceptez le bandeau, puis cliquez sur un lien interne qui n’était pas encore réécrit. Il ne doit plus recevoir les paramètres.
- Dans GA4 en temps réel, la session doit porter la source
google / cpcsur la page acceptée, et non(direct).
Alternatives
- Advanced Consent Mode. Les pings sans cookie transportent l’URL d’arrivée et alimentent la modélisation de conversions. C’est une récupération statistique, pas une donnée brute, comme le détaille Basic vs Advanced Consent Mode.
- Bannière centrale pour le trafic payant. Si la landing est payante et que le taux d’interaction est faible, un bandeau qui demande une réponse avant lecture règle le problème à la source. Le détail de cette variante est dans la bannière CMP conditionnelle au trafic payant.
- Ne rien faire. Si le trafic payant est marginal, la perte est peut-être acceptable. Mesurez-la d’abord.
Questions fréquentes
Pourquoi ne pas simplement mettre le gclid dans un cookie ?
Parce que c’est un dépôt de traceur sans exemption. Même un cookie « technique » qui sert à la mesure doit respecter l’article 82, sauf à remplir strictement les conditions d’une exemption, ce qui n’est pas le cas d’un identifiant publicitaire relu plus tard.
Le serveur (sGTM) peut-il remplacer cette méthode ?
Non. Le serveur renvoie à Google ce que le navigateur lui a envoyé, et il ne récupère pas un clic dont l’URL a perdu le gclid. La propagation se fait dans le navigateur, sur la navigation.
Faut-il propager les paramètres après refus ?
Le code les propage tant que le consentement n’est pas accordé, refus compris, car aucun stockage n’est impliqué. Si votre DPO juge préférable de cesser aussi après refus, ajoutez la même condition que pour l’acceptation.
Sources
- Google, Auto-tagging et paramètre gclid dans GA4 : priorité du
gclidsur les paramètres manuels. - Google, Présentation du mode Consentement : comportement des balises sans consentement.
- CNIL, Cookies et autres traceurs : lignes directrices modificatives et recommandation : périmètre des traceurs, dont le stockage local.