Vous avez passé des heures à configurer Google Tag Manager. Chaque variable est mappée, chaque déclencheur se comporte parfaitement en mode Aperçu, vous publiez en confiance. Le lendemain, vous ouvrez les rapports GA4 : une grande partie de vos dimensions personnalisées affiche « (not set) ».

J’ai récemment débogué exactement ce cas sur un projet e-commerce multi-pays (anonymisé). Le setup GTM était irréprochable, le dataLayer suivait toutes les bonnes pratiques, et pourtant des centaines de milliers d’événements arrivaient dans GA4 avec des paramètres manquants. Le coupable : des race conditions entre l’initialisation du dataLayer et l’exécution du conteneur GTM. Voici pourquoi, comment diagnostiquer, et comment corriger durablement.

Ce que « (not set) » veut dire dans GA4

Quand GA4 affiche « (not set) » pour une dimension personnalisée, l’événement est le plus souvent arrivé sans ce paramètre (ou avec une valeur vide) : la variable était vide au moment où le tag s’est déclenché. Dès qu’un paramètre est enregistré comme dimension personnalisée, tout événement qui arrive sans lui apparaît en « (not set) ». C’est d’ailleurs utile : on voit que les événements partent bien, mais que les données ne suivent pas. À noter : « (not set) » s’affiche aussi pendant les 24 premières heures après la création d’une dimension (Google : [GA4] The value (not set)).

Les conséquences sont lourdes :

Le pire : cela passe souvent inaperçu des semaines. Les événements partent, les volumes semblent bons, mais on perd silencieusement en granularité.

Les trois causes que je vois chaque semaine

Cause n°1 : race condition entre dataLayer et conteneur GTM

C’est la plus fréquente, et la plus sournoise car elle fonctionne parfaitement en test. Le scénario type : le conteneur GTM est chargé dans le <head> et commence aussitôt à exécuter ses tags. Le déclencheur « All Pages » envoie un page_view GA4. À cet instant, le dataLayer existe (GTM le crée au besoin) mais il est vide : l’appel d’API qui fournit userID, userType et membershipLevel n’a pas encore répondu. GTM envoie donc l’événement avec des paramètres vides. L’API répond 300 ms plus tard, mais le page_view est déjà parti.

Pourquoi n’est-ce pas visible en mode Aperçu ? En local ou sur une connexion rapide, le serveur répond en 10 ms et non 300, le cache rend les chargements suivants instantanés, et le timing « marche par chance ». En production, avec de vrais utilisateurs en 3G, de la latence CDN et des API à 500 ms et plus, la race condition devient évidente.

Exemple réel (anonymisé) : le conteneur GTM était servi par le CDN de Google (rapide), le script d’initialisation du dataLayer depuis l’origine du site (plus lente). En 3G mobile, le conteneur arrivait systématiquement 400 à 800 ms avant le script : l’essentiel des valeurs vides venait du mobile, les utilisateurs desktop étant parfaits.

Cause n°2 : lire les variables au mauvais moment du cycle de l’événement

Même avec un dataLayer initialisé avant GTM, le moment où les variables sont lues compte. Principe : une valeur poussée dans le dataLayer reste dans le modèle de données de GTM jusqu’à ce qu’elle soit écrasée ou que la page se recharge. Dans une SPA, des valeurs anciennes survivent donc aux « changements de page ».

Le scénario courant : un site e-commerce pousse les données de transaction dans le dataLayer après le page_view. Séquence : la page se charge, le page_view part (transaction_id en « (not set) »), 50 ms plus tard le CMS renseigne la transaction, puis un événement personnalisé part avec la bonne valeur. On obtient un « split-brain » : certains événements ont la donnée, d’autres non.

Autre point : la version de la variable de couche de données. La version 1 n’interprète pas les points (elle lit a.b.c comme une seule clé), la version 2 lit les clés imbriquées en notation pointée et fusionne les pushes successifs (recommandée). Source : Google Tag Manager : types de variables définies par l’utilisateur. On voit encore beaucoup de variables en version 1 en production.

Cause n°3 : dataLayer jamais vidé entre deux pushes

Particulièrement gênant dans les SPA ou le contenu dynamique. Quand l’utilisateur passe du produit A au produit B sans rechargement, les clés poussées à nouveau sont écrasées. Le vrai risque, ce sont les clés que B ne pousse pas (par exemple un productVariant que seul A avait) : elles gardent la valeur de A. Et pour « vider » une clé, pousser une chaîne vide ne convient pas :

// Mauvais : une valeur vide finit en « (not set) » dans GA4
window.dataLayer.push({ 'productID': '', 'productName': '', 'productPrice': '' });

// Mieux : undefined (testez null et '' dans Tag Assistant avant de vous y fier)
window.dataLayer.push({ 'productID': undefined, 'productName': undefined, 'productPrice': undefined });

Exemple vécu : sur un site de réservation avec un formulaire en plusieurs étapes, l’étape 3 contenait encore les champs de l’étape 1 dans le dataLayer : données en double ou contradictoires, valeurs périmées, et des variables vides quand la validation échouait face aux conflits.

Ma checklist de débogage en 5 étapes

Étape 1 : vérifier la position du dataLayer dans le code source

Ouvrez le code source de la page et repérez où le dataLayer est initialisé. L’ordre doit être : d’abord le dataLayer, ensuite le snippet GTM.

<!-- 1. Initialisation du dataLayer EN PREMIER -->
<script>
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({ 'userID': '12345', 'pageType': 'product' });
</script>
<!-- 2. Conteneur GTM ENSUITE -->
<script>(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXX');</script>

Erreurs fréquentes : sur WordPress, les plugins GTM injectent le conteneur dans le <head> alors que l’initialisation du dataLayer est dans le pied de page du thème ; sur Shopify, un script qui pousse les valeurs Liquid placé dans le <body> s’exécute après le snippet GTM du <head> ; dans les builds sur mesure, charger le script dataLayer en async après le conteneur crée la race condition. Le correctif : un script inline placé au plus tôt dans le <head>, au-dessus du snippet GTM.

Étape 2 : utiliser l’Aperçu GTM avec limitation du réseau

Le mode Aperçu vous ment si vous ne simulez pas de vraies conditions réseau. Dans Chrome DevTools, onglet Network, choisissez « Fast 3G » ou « Slow 3G » et rechargez avec l’Aperçu actif. Dans l’onglet Variables de chaque événement, on voit souvent le Page View partir à T+0 ms avec des variables undefined, qui ne se remplissent que quelques centaines de millisecondes plus tard, trop tard. Testez aussi sur un vrai mobile en 3G : la limitation desktop ne simule ni la perte de paquets, ni les reconnexions, ni les délais DNS et TLS.

Étape 3 : inspecter l’état du dataLayer en temps réel

Étape 4 : contrôler la configuration des variables dans GTM

  1. Version de la variable de couche de données : presque toujours « Version 2 » (testez avant de migrer depuis la version 1).
  2. Nom : doit correspondre exactement à la clé poussée, sensible à la casse (userID, userid et UserID sont trois clés distinctes), avec la notation pointée pour l’imbriqué (ecommerce.transaction_id).
  3. Valeur par défaut : en général vide. Une valeur par défaut masque le problème : plus de « (not set) », mais plus aucun moyen de savoir quand la donnée manque.
  4. Format Value : vérifiez qu’il ne convertit pas des nombres en chaînes sans le vouloir, ne retire pas des espaces significatifs et qu’une regex qui échoue ne renvoie pas une chaîne vide.

Étape 5 : tester en conditions réelles

Les correctifs qui marchent vraiment

Correctif n°1 : des événements personnalisés plutôt que les déclencheurs automatiques

Le déclencheur intégré « All Pages » s’exécute sur l’événement gtm.js poussé par le snippet : il ne voit que les valeurs poussées avant le snippet. Tout ce qui est poussé après n’est visible que des événements suivants. Poussez vos données, puis un événement explicite :

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ 'userID': '12345', 'userType': 'premium', 'pageCategory': 'product' });
// PUIS l'événement
window.dataLayer.push({ 'event': 'dataReady' });

Dans GTM, remplacez le déclencheur du tag Page View par « Événement personnalisé : dataReady ». Vous maîtrisez le moment de déclenchement et la donnée est garantie présente. On peut empiler des événements par type de donnée (pageDataReady, userDataReady après un appel API, productDataReady) et brancher chaque tag sur celui dont il a besoin.

Correctif n°2 : vider explicitement les valeurs

Pour les SPA et le contenu dynamique, poussez undefined sur les anciennes clés avant les nouvelles. Pourquoi pas null ou '' ? Avec undefined, GTM considère la variable comme inexistante et n’envoie pas le paramètre ; avec null, GTM peut envoyer « null » en chaîne ; avec '', la valeur vide finit en « (not set) » dans GA4. Alternative avancée : la méthode reset du dataLayer, poussée sous forme de fonction (window.dataLayer.push(function() { this.reset(); })), qui vide les valeurs du modèle de données de GTM pour la page. Source : Google : The data layer.

Correctif n°3 : une condition de garde dans les déclencheurs

Ajoutez au déclencheur une condition du type {{DLV - productID}} ne vaut pas undefined. Attention : un déclencheur GTM ne peut pas attendre une donnée, la condition filtre seulement au moment du traitement, et un tag dont la valeur manque est sauté, pas retardé. À ne pas sur-utiliser : corrigez la cause (le timing du dataLayer) plutôt que de multiplier les rustines.

Correctif n°4 : centraliser l’initialisation du dataLayer dans le head

La référence : initialiser le dataLayer avec toutes les données critiques, rendues côté serveur (PHP, Node.js, Python), avant le snippet GTM. La donnée existe avant le chargement de GTM, sans appel async ni race condition. Les données qui doivent absolument être asynchrones (API tierces) passent par des événements personnalisés, comme au correctif n°1. Si vous ne contrôlez pas le rendu serveur (site statique), lisez des valeurs de manière synchrone depuis localStorage ou les cookies avant GTM, puis poussez un événement userDataReady.

Cas avancé : les conteneurs multiples

La plupart des sites n’ont pas besoin de plusieurs conteneurs GTM. S’il y en a, les problèmes de timing se multiplient : chaque conteneur charge indépendamment et lit le dataLayer à son propre moment. Le conteneur 1 (tags cœur) charge à T+0 ms, dataLayer vide, et envoie des valeurs vides ; le conteneur 2 (marketing) charge à T+200 ms avec les bonnes valeurs ; le conteneur 3 (analytics) à T+400 ms. Même utilisateur, même session, mais des données contradictoires dans GA4.

Si vous devez les garder : un seul script datalayer-init.js chargé avant tous les conteneurs, qui remplit les données puis pousse {'event': 'dataLayerReady'}, et dans tous les conteneurs des tags déclenchés sur cet événement. Mon opinion tranchée : si vous vous battez plus d’une journée contre le timing multi-conteneurs, reconsidérez le besoin. Espaces de travail, dossiers, conventions de nommage et permissions résolvent la plupart des cas, sans le cauchemar de débogage.

La checklist définitive

ÉtapePoints à vérifier
Avant mise en lignedataLayer initialisé dans le <head> avant le snippet GTM ; variables critiques remplies de façon synchrone (serveur ou localStorage) ; snippet GTM juste après l’initialisation (snippet async officiel de Google inchangé).
Configuration GTMVariables en Version 2 ; noms identiques aux clés (casse comprise) ; pas de valeur par défaut sauf stratégie de repli voulue ; déclencheurs sur événements personnalisés, pas seulement « All Pages ».
TestsAperçu avec limitation réseau (Fast 3G au minimum) ; window.dataLayer inspecté en console ; vrais mobiles ; bloqueurs activés ; DebugView GA4 en temps réel ; rapports de production surveillés après déploiement.
ProductionAlertes GA4 (« custom insights ») sur la part de « (not set) » (Google : custom insights) ; suivi des erreurs JavaScript ; audits réguliers ; contraintes de timing documentées.

Mes outils quotidiens : dataslayer, l’Aperçu GTM (toujours avec limitation réseau), l’onglet Network de DevTools et GA4 DebugView ; en production, Looker Studio pour suivre la part d’événements aux paramètres vides et BigQuery pour repérer les événements au plus fort taux de « (not set) ».

Quand tout refaire

Parfois, il est plus sain d’admettre que l’implémentation est cassée : plus de deux jours de lutte sans progrès, plusieurs conteneurs interdépendants que personne ne comprend, un dataLayer alimenté par cinq scripts ou plus, un conteneur de 200 tags dont la moitié est inconnue. L’approche : auditer l’existant, concevoir un nouveau schéma de dataLayer, rendre les données critiques côté serveur, n’avoir qu’un seul script d’initialisation, utiliser des événements personnalisés pour l’async, puis tout tester et documenter.

Conclusion

Les dimensions personnalisées en « (not set) » sont le plus souvent un problème de timing, pas de configuration. Initialisez le dataLayer avant le conteneur, remplissez les données critiques de façon synchrone, utilisez des événements personnalisés pour l’async, testez avec un réseau bridé, et demandez-vous si plusieurs conteneurs sont vraiment nécessaires. L’objectif n’est pas une architecture élégante, c’est une donnée exacte.

Pour la version Commanders Act du même problème : Tag Commander multi-container: a complete timing breakdown.

Sources

Pour aller plus loin, lisez la version complète de l’article (en anglais) : Why Your GA4 Custom Dimensions Show ‘(not set)’ (And How to Fix It).

Besoin d’aide sur ce sujet ? Consultant GA4 et audit analytics.

Vous doutez de votre tracking ?

Réserver un appel Ou commencez par l’audit analytics gratuit →