Calculator, le super cookie de ciblage de Google Tag Manager

La scène à laquelle nous assistons vous a lieu dans les locaux d’un site e-commerce, quelque part entre Levallois-Perret et Boulogne-Billancourt, dans un open space aux couleurs chaudes, entre une machine à smoothie et un totem « Retrouvez nous au salon Big Data Mulhouse 2018 ». Ronan, chargé d’acquisition, vient interpeller Sophie, experte GTM respectée et adulée de tous :
(les prénoms ont été changés, afin de respecter l’anonymat des personnes concernées)

Requiem pour un tag

[Ronan] « Hey Salut, je viens de signer un partenariat avec une super régie de retargeting pour cibler en post view les utilisateurs qui auraient, selon un modèle prédictif basé sur des random forests, une probabilité de devenir des clients fidèles en moins de 12 mois. En plus, il n’y a qu’un bout de Javascript à mettre sur toutes les pages! Tu peux mettre ça dans ton GTM, là? »

[Sophie] « Tu reviens de voir un chef de projet technique, non? »

[R] « Oui, comment tu le sais? »

[S] « Tu as le nez en sang, tu boîtes, et de la salive sur l’épaule »

[R] « Oui, c’est vrai, il l’a assez mal pris. Mais tu as vu, j’ai quasiment réussi à esquiver son crachat au visage »

[S] « Bref, dis-moi, ton snippet JS, là, c’est le même partout? »

[R] « Oui, oui, c’est un script tout gentil tout mignon. En plus, ils m’ont dit qu’il n’appellerait jamais d’autres scripts en cascade. Je sais pas trop ce que ça veut dire, je suis pas super fan des cascadeurs de toute façon lol »

[S] « Bon, oui, ça devrait être assez simple à faire du coup, je dois pouvoir passer ça dans la prochaine publication…Hum, disons jeudi prochain? »

[R] « Ah, oui, attends, en fait, c’est un peu compliqué que ça. Ils m’ont demandé de ne l’envoyer qu’au bout de la 4ème page de la 3ème session, uniquement pour les utilisateurs ayant vu 3 fiches produit »

[S] « …. »

[R] « Quoi? GTM ne sait pas faire ça? »

[S] « Hum…Je crois qu’il va falloir mettre ça dans le data layer, je vais devoir aller voir un chef de projet technique »

[R] « Ah…Attends, passe d’abord à mon bureau, je vais te prêter mon bouclier anti-émeute »

Un besoin? Un cookie. Un cookie? Un besoin.

Il est probable que vous (et quand je dis vous, je dis aussi moi, toi, un peu nous tous au fond) vous soyez déjà retrouvés dans cette situation où vous devez insérer un tag quelconque (souvent un tag média) selon des conditions particulières.

Et par « particulières », je vois plus loin que des règles du type pattern d’URL, variable de data layer, ou encore famille de device (mobile, tablette, PalmPilot, Game Boy Advance…). Non, je parle de conditions qui sortent du contexte de la page à proprement parler, et qui vont aller chercher des infos plus globales sur le parcours de l’utilisateur : son nombre de sessions, de pages vues, sa source de trafic, une typologie particulière de pages (ex : pages produits) vues un certain nombre de fois, voire une combinaison de tout ceci, comme le demande Ronan, qui ne manque décidément pas de culot.

Il est fort probable que, si vous êtes tombés sur cet article, vous avez déjà mis en place ce genre de mécanique : un petit tag en HTML / JS custom qui va me créer un cookie, j’appelle ça sur toutes les pages, puis je requête ce cookie dans une variable. Easy, non?

Voyons par exemple comment on ferait pour créer un simple cookie « gtmPV » s’incrémentant à chaque page vue :

L’idée est pour le moins simple : si mon cookie « gtmPV » n’existe pas, je le créé avec ‘1’ pour valeur, sinon je l’incrémente simplement. Quelques précisions de rigueur :

  • Bien penser au parseInt (fonction permettant de récupérer une chaîne et de la mettre sous forme d’entier), indispensable si on ne veut pas incrémenter une chaîne textuelle.
  • Oui, je sette l’expiration du cookie en 2034. Oui, c’est volontaire. Et oui, on reviendra plus tard sur l’expiration des cookies et sur la façon de la gérer proprement.
  • Ne pas oublier de créer la variable {{COO – gtmPV}} bien évidemment, qui va aller interagir avec le cookie du même nom.

On peut donc parfaitement exécuter ce script sur chaque page, et utiliser la variable allant chercher le cookie sus-mentionnée afin de caller ça bien proprement dans le trigger d’un tag média…

…ou bien dans une custom dimension (parce que je les adore, mais ça vous le savez déjà) :

(Et ce que j’ai fait des 723 custom dimensions précédentes, c’est mon problème)

Et voici le sccript en version joliekikoo si besoin :

<script>

if({{COO - gtmPV}}){
    var gtmPVIncr = parseInt({{COO - gtmPV}},10)+1;
    document.cookie = 'gtmPV='+gtmPVIncr+';expires=Thu, 18 Dec 2034 12:00:00 UTC;domain=.monsupersite.fr;path=/';
    }

else{
    document.cookie = 'gtmPV=1;expires=Thu, 18 Dec 2034 12:00:00 UTC;domain=.monsupersite.fr;path=/';
    }
    
</script>

Sur le principe, on pourrait imaginer faire la même chose pour calculer le nombre de sessions ou toute autre diablerie à base de JS, en ayant autant de cookies dédiés à chaque info.

L’avantage, c’est que pour chaque méthode, j’ai un tag, un cookie, une variable GTM, ce qui est plutôt modulaire comme approche, et permet de bien maintenir tout ça (et clairement, c’est comme ça que j’ai moi même procédé jusqu’à récemment).

Le problème, c’est que ce genre d’approche peut vite être chaotique, et ce, pour plusieurs raisons :

  • Si on exécute tous ces tags au chargement de la page (trigger « Page vue »), et qu’on en a besoin au moment de déclencher le tag de page GA, ça risque d’être un peu le chantier : GTM enverra tout ça en même temps. Et NON, ne vous amusez pas à envoyer vos tags « de calcul » au page view, et GA au DOM Ready, cela ne garantit rien du tout.
  • Corollaire : si on veut capitaliser sur un cookie pour en calculer un autre (ex : si j’incrémente mon nombre de sessions, je vais remettre mon compteur de pages vues de la session courante à zéro), cela devient encore plus le chaos.
  • Alors oui, on pourrait bien faire partir un tag en séquentiel avant le tag de page GA, mais ça ne marche que pour un seul tag, dézo. Cela dit, nous allons exploiter cette mécanique plus tard, mais je garde un peu de suspense pour augmenter l’engagement de mes lecteurs  (et peut-être être racheté par le Washington Post, mais c’est mon projet secret, je ne peux pas encore en parler).

Un cookie pour les gouverner tous. Mais genre un seul cookie.

L’idée est donc d’avoir un seul bon gros script qui va calculer tout ça, et venir concomitamment alimenter un seul cookie, contenant toutes ces infos. Ce cookie, nous l’appellerons CALCULATOR, le cookie qui calcule à mort.

Commençons par un petit cahier des charges, issu de mon expérience de « trucs à faire pour conditionner des tags média qu’on me demande souvent ». Calculator devra être en mesure de capter, par ordre de difficulté croissante :
1/ Le nombre de pages vues au total par l’utilisateur
2/ Le nombre de sessions (selon la règle de GA qui veut que la session expire au bout de 30mn d’inactivité)
3/ Le nombre de pages vues durant la session courante
4/ La source de trafic de la session, selon une définition custom : emailing, SEO, Facebook, direct (bah oui, parce que jamais accessible en dehors de l’interface de GA, qui en a lui-même sa définition custom).
5/ Le numéro de refresh de la page (mon dada)
6/ Le nombre d’articles vus (ou autre typologie de page)
7/ Super bonus : Le nombre de jours actifs de l’utilisateur au cours du mois courant (super utile dans GA)

En bout de course, l’objectif est de créer un cookie sous forme de JSON, ressemblant à quelque chose comme ceci :

{"pv":3,"sessions":1,"pvSession":3,"lastTimeStamp":1541276692746,"source":"Direct","lastActiveDay":"2018|11|3","NBActiveDays":1,"NoRefresh":1,"NbArticles":null}

L’avantage du JSON, comme on va le voir plus tard, c’est qu’il est extrêmement simple à parser pour récupérer l’info qui nous intéresse, soit dans le cadre d’un trigger, soit dans une custom dimension GA.

Structure de base du JS

Allez, je suis un ouf dans ma tête, je vous crache tout le JS afin que vous puissiez déjà vous en faire une idée :

<script>

function gtmTStoUTC(d){
    return new Date(d).toUTCString();
    }
    
var gtmCib = {},
    gtmCurrentTimeStamp = + new Date(),
    gtmTimeSt = new Date(Date.now()),
    gtmJA = gtmTimeSt.getFullYear()+'|'+(parseInt(gtmTimeSt.getMonth(),10)+1)+'|'+gtmTimeSt.getDate(),
    gtmExpirationTimeStamp = gtmCurrentTimeStamp+3.4186698e10, //Timestamp date actuelle + 13 mois
    gtmExpirationDtCoo = gtmTStoUTC(gtmExpirationTimeStamp), //Timestamp date actuelle + 13 mois, format UTC
    gtmDomain = '.aristide-riou.fr'; //On le sette en dur, plus simple et évite erreurs de parsing bêtes - Remplacer par votre domaine
    
if(window.performance.timing){
    gtmCurrentTimeStamp = window.performance.timing.navigationStart;
    };
    
function gtmCalcSource(){
    if({{URL - paramètre utm_source}}.indexOf('crm')!==-1){
        return 'Emailing';
        }    
    else if({{URL - Referrer}}.indexOf('facebook')!==-1){
        return 'Facebook';
        }
    else if({{URL - Referrer}}.indexOf('cdn.ampproject.org')!==-1){
        return 'AMP';
        }
    else if({{URL - Referrer}}.indexOf('google')!==-1){
        return 'SEO';
        }
    else if(({{URL - Referrer}})===''){
        return 'Direct';
        }
    else if(({{URL - Referrer}})!==''){
        return 'Referal';
        }
    else{
        return 'autre'
        }
    };

//Modif du cookie déjà existant
if({{COO - gtmCiblage}}){
    //Transformatoin du cookie en objet
    gtmCib = JSON.parse({{COO - gtmCiblage}});

    //Incrémentation PV
    gtmCib.pv += 1;

    //Incrémentation article si besoin
    if({{DL - mdModele}}.indexOf('article')!==-1){
        if(gtmCib.NbArticles){
            gtmCib.NbArticles += 1;
            }
        else{
            gtmCib.NbArticles = 1;
            }
        }
    
    //Update sessions, pages vues, source
    var gtmExistDiffTS = (gtmCurrentTimeStamp - gtmCib.lastTimeStamp)/1000/60;
    if(gtmExistDiffTS>30){
        //Cas d'une nouvelle session
        gtmCib.sessions += 1; 
        gtmCib.pvSession = 1; 
        gtmCib.source = gtmCalcSource();
        }
    else{
        //Cas de poursuite de la session en cours
        gtmCib.pvSession += 1;
        }
    
    //Update du No de refresh
    var gtmExistRefreshDiff = gtmCurrentTimeStamp - gtmCib.lastTimeStamp,
        gtmExistRefreshReferrer = {{URL - Referrer}} === {{URL - URL complète}},
        gtmExistRefreshValueMin = {{JS - metaRefresh}}*1000-15000,
        gtmExistRefreshValueMax = {{JS - metaRefresh}}*1000+60000;
      
    if(gtmExistRefreshDiff>gtmExistRefreshValueMin && gtmExistRefreshDiff<gtmExistRefreshValueMax && gtmExistRefreshReferrer){
    //Nous sommes visiblement sur un refresh
        gtmCib.NoRefresh = parseInt(gtmCib.NoRefresh,10)+1;
        }
    else{
        gtmCib.NoRefresh = 1;
        }
    
    //Update jours actifs
    var gtmExistJACookie = gtmCib.lastActiveDay.split('|'),
        gtmExistJACurrent = gtmJA.split('|'),
        gtmExistYearDiff = gtmExistJACurrent[0] - gtmExistJACookie[0],
        gtmExistMonthDiff = gtmExistJACurrent[1] - gtmExistJACookie[1],
        gtmExistDayDiff = gtmExistJACurrent[2] - gtmExistJACookie[2];
    if (gtmExistMonthDiff > 0 || gtmExistYearDiff > 0){
        //Nouveau mois
        gtmCib.NBActiveDays = 1;
        gtmCib.lastActiveDay = gtmJA;
        }
    else if(gtmExistDayDiff > 0){
        //Nouveau jour du même mois
        gtmCib.NBActiveDays = parseInt(gtmCib.NBActiveDays,10)+1;  
        gtmCib.lastActiveDay = gtmJA;
        }
    
    //Update du timestamp
    gtmCib.lastTimeStamp = gtmCurrentTimeStamp;
    
    //Update de la date d'expiration
    gtmCib.Exp = gtmExpirationTimeStamp.toString();
        
    gtmExpirationDtCoo = gtmTStoUTC(parseInt(gtmCib.Exp,10));
    }

//Création du nouveau cookie
else{
    gtmCib.pv = 1;
    gtmCib.sessions = 1;
    gtmCib.pvSession = 1;
    gtmCib.lastTimeStamp = gtmCurrentTimeStamp;
    gtmCib.source = gtmCalcSource();
    gtmCib.lastActiveDay = gtmJA;
    gtmCib.NoRefresh = 1;
    gtmCib.NBActiveDays = 1;
  
    //Si type de page contient article
    if({{DL - typePage}}.indexOf('article')!==-1){
        gtmCib.NbArticles = 1;
        }
    else{
        gtmCib.NbArticles = 0;
        }
    
    gtmCib.Exp = gtmExpirationTimeStamp.toString();
    }

//Stringification de l'objet et pose du cookie
document.cookie = 'gtmCiblage='+JSON.stringify(gtmCib)+';expires='+gtmExpirationDtCoo+';domain='+gtmDomain+';path=/';  

</script>

139 lignes de bonheur total. Cela peut sembler (un peu) effrayant au premier abord, mais nous allons voir comment il fonctionne tronçon par tronçon.

A noter que j’utilise différentes techniques JS de base (stringification, création et requêtage de cookie, récupération de dates…), que je tâcherai d’expliquer au maximum pour ceux qui seraient moins à l’aise avec la technique, et qui, je le pense, doivent faire partie de la boîte à outils de tout expert en analytics, car elles s’avèrent très souvent utiles.

Les variables utilitaires

On commence par le s’intéresser au tout début du script :

function gtmTStoUTC(d){
    return new Date(d).toUTCString();
    }
    
var gtmCib = {},
    gtmCurrentTimeStamp = + new Date(),
    gtmTimeSt = new Date(Date.now()),
    gtmJA = gtmTimeSt.getFullYear()+'|'+(parseInt(gtmTimeSt.getMonth(),10)+1)+'|'+gtmTimeSt.getDate(),
    gtmExpirationTimeStamp = gtmCurrentTimeStamp+3.4186698e10, //Timestamp date actuelle + 13 mois
    gtmExpirationDtCoo = gtmTStoUTC(gtmExpirationTimeStamp), //Timestamp date actuelle + 13 mois, format UTC
    gtmDomain = '.aristide-riou.fr'; //On le sette en dur, plus simple et évite erreurs de parsing bêtes - Remplacer par votre domaine
    
if(window.performance.timing){
    gtmCurrentTimeStamp = window.performance.timing.navigationStart;
    };

Ici, on va définir quelques variables génériques, qui vont nous servir tout au long de notre script :

  • La fonction « gtmTStoUTC » est créée en amont, et permet de rendre une date « UTC compliant ». On en reparle juste après.
  • La variable « gtmCib » est un objet JS, vide pour le moment, qui nous servira de « marmite » pour déverser toutes les infos qui vont bien par la suite.
  • « gtmCurrentTimeStamp » nous donne le timestamp courant au format UNIX (nombre de secondes depuis le 1er janvier 1970, si je ne m’abuse), par exemple « 1541839338568 » à l’heure où j’écris cet article.
  • « gtmJA » nous permet de récupérer la date actuelle, mais cette fois-ci au format « AAAA|MM|JJ » (ex : « 2018|11|10 »), ce qui va nous servir plus tard pour calculer le nombre de jours actifs (point 7 de notre brief). On le récupère depuis « gtmTimeSt », qui, quant à lui, donne la date non pas sous forme de nombre, mais au format UTC : « Sat Nov 10 2018 09:42:18 GMT+0100 (heure normale d’Europe centrale) ». Les méthodes « getMonth » & cie, associées à une élégante concaténation, seront vos meilleurs amis.
  • Enfin, « gtmDomain » permet d’obtenir le top level domain par rapport au site où je me trouve (ex : le top level domain de « www.monsite.com » est « .monsite.com »), ce qui sera utile pour setter le fameux cookie en fin de script. On choisit de setter ce paramètre à la main (‘.aristide-riou.fr’, que vous devrez bien entendu adapter à votre cas) plutôt que de le faire automatiquement, ce qui est faisable mais pas super lisible (plus d’infos ici pour ceux que ça intéresse). Si jamais, d’aventure, votre container GTM était posé sur plusieurs sites, il est tout à fait possible de faire une règle custom basée sur une lookup ou autre.
  • Petit bonus : si le navigateur est compatible avec l’API Navigation Timing (ce qui devrait être le cas si l’utilisateur n’est pas sur Netscape 1992), je modifie la variable gtmCurrentTimeStamp avec le timestamp du responseEnd (en gros, le début du chargement du DOM de la page). Pourquoi se prendre autant la tête? Eh bien parce que cela permet d’être aussi précis que possible sur le timestamp « réel », et d’éviter de calculer le timestamp trop « tard », c’est à dire au moment du chargement de Calculator (un cookie en or). Cela nous permettra d’être aussi précis que possible pour notre calcul du refresh. Et le fait de le faire après coup (après avoir défini la variable avec le timestamp courant) me permet d’éviter de faire totalement planter mon script pour les vieux navigateurs (c’est ce qu’on appelle un polyfill dans le jargon). Parce que ça serait dommage d’être ultra précis sur 98% des utilisateurs, mais de ne rien avoir du tout sur les 2% restants. N’hésitez pas à consulter la doc de l’API Navigation Timing, très bien écrite et utile pour calculer toutes sortes d’indicateurs de perf.

Le calcul de la source custom

function gtmCalcSource(){
    if({{URL - paramètre utm_source}}.indexOf('crm')!==-1){
        return 'Emailing';
        }    
    else if({{URL - Referrer}}.indexOf('facebook')!==-1){
        return 'Facebook';
        }
    else if({{URL - Referrer}}.indexOf('cdn.ampproject.org')!==-1){
        return 'AMP';
        }
    else if({{URL - Referrer}}.indexOf('google')!==-1){
        return 'SEO';
        }
    else if(({{URL - Referrer}})===''){
        return 'Direct';
        }
    else if(({{URL - Referrer}})!==''){
        return 'Referal';
        }
    else{
        return 'autre'
        }
    };

Ici, le principe est exactement le même : on va définir en amont de toute notre mécanique une fonction de calcul de la source de trafic, basée sur des règles custom. Pour l’instant, la fonction n’est pas exécutée, mais elle sera « à dispo » des boucles ultérieures. Le fait de la définir en amont évite de se répéter à chaque fois que l’on l’invoque, et permet de donner une meilleure lisibilité au script.
Bien évidemment, vous devrez adapter cette fonction selon la façon dont vous tagguez vos campagnes et/ou sur les familles de sources qui vous intéressent (typiquement, la présence de la chaîne « crm » dans votre UTM source). J’attire votre précieuse attention sur le fait que l’ordre des « else » est fondamental et doit être soigneusement « crafté » comme on dit dans les agences en transformation digitale.

Le cas par défaut

On va délibérément zapper pour quelques minutes la partie « cookie déjà existant » (lignes 43 à 112), la plus complexe, pour s’intéresser aux lignes 114 à 134, à savoir le cas où l’utilisateur ne dispose pas encore du cookie. Il va donc falloir le setter avec les valeurs qui vont bien :

//Création du nouveau cookie
else{
    gtmCib.pv = 1;
    gtmCib.sessions = 1;
    gtmCib.pvSession = 1;
    gtmCib.lastTimeStamp = gtmCurrentTimeStamp;
    gtmCib.source = gtmCalcSource();
    gtmCib.lastActiveDay = gtmJA;
    gtmCib.NoRefresh = 1;
    gtmCib.NBActiveDays = 1;
  
    //Si type de page contient article
    if({{DL - typePage}}.indexOf('article')!==-1){
        gtmCib.NbArticles = 1;
        }
    else{
        gtmCib.NbArticles = 0;
        }
    
    gtmCib.Exp = gtmExpirationTimeStamp.toString();
    }

Vous vous souvenez de notre objet « gtmCib », créé à vide au tout début du script? Eh bien on va lui passer toutes les valeurs par défaut, en utilisant la notation objet.propriété, fondamentale pour ce qui est de la manipulation d’objets en JS :

  • Le nombre de pages vues (.pv), de sessions (.sessions), de pages vues au cours de la session (.pvSession), de refresh de la page courante (.NoRefresh), de jours actifs (NBActiveDays) valent bien sûr « 1 » au démarrage, pas besoin d’avoir fait Polytechnique (ou l’ESC Aurillac) pour comprendre.
  • On sette un attribut lastTimeStamp en allant chercher le timestamp courant calculé au début du script (et notre histoire de window performance timing), pas de problème.
  • Pour ce qui est de la source, idem, on invoque la fonction tkCalcSource qui a été définie au début du script.
  • Le dernier jour actif, que l’on veut de la forme AAAA|MM|JJ, a déjà été setté au tout début, lui aussi, il n’y a qu’à se servir.
  • Ensuite, on se retrouve face au dernier cas, à savoir le nombre d’articles vus. Ici, je choisis de m’appuyer sur une hypothétique variable data layer « typePage » (avec la variable GTM qui va avec), mais je pourrais tout à fait appliquer cette logique de plein de façons différentes : imaginons plutôt que je sois sur un site e-commerce, où je veux tracker le nombre de pages vues sur les produits de sport, et que les URLs desdites pages contiennent « /sport » dans l’URL : je peux bien entendu aller faire un {{URL – URL complète}}.indexOf(‘sport’)!== -1, cela fonctionnera exactement de la même façon. A vous de l’adapter selon votre besoin.
  • Pour finir, on ajoute un attribut « Exp » qui va aller chercher la date correspondant au timestamp courant + 13 mois (le « gtmExpirationDtCoo » setté au début du script). L’idée est simple : on veut que notre cookie ait une durée de 13 mois non glissants (bisous la CNIL, tout ça tout ça). Or, comme on le verra plus tard, il n’est pas possible, en JS, de faire quelque chose du type « prend ce cookie déjà existant, et modifie sa valeur tout en gardant sa date d’expiration ». Dommage hein? Il faudra donc le resetter à chaque fois. Et histoire de rendre les choses encore un peu plus simples, il n’est pas non plus possible de récupérer, pour un cookie déjà posé, sa date d’expiration. Conséquence : le cookie doit porter sa propre date d’expiration (sous forme de timestamp, donc…). Oui, c’est chiant. Et Non, il n’y a pas mieux à ma connaissance.

Notons au passage que ce « chaine.indexOf(‘sous-chaine’)!==-1 » est la méthode classique, en JS, pour tester si une chaîne textuelle contient une autre sous-chaîne.

Donc, juste après avoir setté tout ceci, on se retrouve bien avec un objet tkCib qui est juteusement garni, et contient toutes les propriétés pv, sessions, lastTimestamp… Mais en l’état, cet objet est limité à la page ; il va donc falloir mettre ça en forme proprement et poser le cookie Calculator, qui, faut-il le rappeler, calcule très vite et super fort :

Passage en JSON, et pose du cookie

On s’intéresse donc aux deux dernières lignes du script :

//Stringification de l'objet et pose du cookie
document.cookie = 'gtmCiblage='+JSON.stringify(gtmCib)+';expires='+gtmExpirationDtCoo+';domain='+gtmDomain+';path=/';

Créer un cookie en JS obéit à une syntaxe pas franchement flexible, d’où les différentes variables utilisées : il faut indiquer un nom au cookie (gtmCiblage), sa date d’expiration (variable « gtmExpirationDtCoo » que nous avons pris soin de calculer en amont), son domaine (idem, le top level domain a été déjà setté en dur au tout début du script), et un chemin (on met « / » , ce qui est indispensable pour pouvoir y accéder depuis toute page du site).
Le JSON.stringify(tkCib) va transformer notre objet JS en JSON : concrètement, on met la variable « à plat », pour qu’elle ne soit plus qu’un string, mais de la forme {« pv »:1, »sessions »:1, »pvSession »:1}…, afin que ça soit plus facilement requêtable sur les pages suivantes (comme on va le voir juste après, ohlala comme ça tombe bien, quel sens du timing, pfiou).

Si tout se passe bien, on devrait retrouver dans le débuggueur, une fois ce script exécuté, un cookie tout beau tout nouveau, qui ressemble à ceci :

(oui, oui, faites pas attention aux trucs Taboola et AB Tasty autour, je vous les donne avec, non, non, vraiment, ça me fait plaisir).

Le cas du cookie déjà existant

Passons donc au gros morceau de ce script, à savoir les lignes 34 à 97, où on traite le cas où l’utilisateur possède déjà le cookie, et où on va incrémenter les valeurs selon différents calculs :

Passage du JSON à un objet

if({{COO - gtmCiblage}}){
    //Transformatoin du cookie en objet
    gtmCib = JSON.parse({{COO - gtmCiblage}});

La première ligne est fondamentale : on va récupérer le contenu du cookie Calculator (un string ressemblant à quelque chose comme « {« pv »:1, »sessions »:1, »pvSession »:1} »), et le transformer en objet grâce à la méthode JSON.parse (c’est exactement l’inverse du JSON.stringify vu juste au dessus).

Incrémentation pages vues, sessions & cie

//Incrémentation PV
    gtmCib.pv += 1;

    //Incrémentation article si présence dans le data layer
    if({{DL - typePage}}.indexOf('article')!==-1){
        gtmCib.NbArticles += 1;
        }
    
    //Update sessions, pages vues, source
    var gtmExistDiffTS = (gtmCurrentTimeStamp - gtmCib.lastTimeStamp)/1000/60;
    if(gtmExistDiffTS>30){
        //Cas d'une nouvelle session
        gtmCib.sessions += 1; 
        gtmCib.pvSession = 1; 
        gtmCib.source = gtmCalcSource();
        }
    else{
        //Cas de poursuite de la session en cours
        gtmCib.pvSession += 1;
        }

Je mets dans le même paquet les calculs suivant, qui sont assez semblables.

  • Quoi qu’il arrive, on incrémente le compteur de pages vues (.pv), sans condition particulière, sachant que le script (comme on le verra plus tard) va être exécuté sur chaque page. A noter que la notation « += » est une façon concise et élégante d’incrémenter une variable.
  • Pour ce qui est du nombre d’articles vus, même principe que sur le script initial, on n’incrémente que si on est sur une page identifiée comme un article par le data layer.
  • Un peu plus de subtilité sur la 3ème partie : l’objectif est de vérifier si nous sommes sur une nouvelle session (soit plus de 30mn écoulées depuis le dernier déclenchement du script). Pour cela, on calcule la différence entre gtmCurrentTimeStamp, calculé au début de l’exécution du script, et lastTimeStamp, que l’on a récupéré depuis l’objet tkCib, et donc du cookie. On divise cela par 1000 puis par 60 pour passer des millisecondes aux minutes, et, si nous sommes au-dessus de 30, on fait 3 choses : on incrémente le nombre de sessions, on réinitialise le nombre de pages vues au cours de la session, et enfin, on refait tourner notre calcul de la source. Dans le cas où nous sommes sur une poursuite de session (moins de 30mn depuis le dernier calcul), on ne fait qu’incrémenter le nombre de pages vues au cours de la session.

Et c’est précisément sur cette partie que l’on met le doigt sur l’intérêt majeur d’avoir un gros script unifié, à savoir la possibilité de combiner / comparer les valeurs des différentes variables entre elles, ce qui serait un véritable enfer s’il fallait le faire sur des cookies dédiés (et croyez moi, je me suis salement cassé les dents à essayer de le faire).

Update du refresh

Alors oui, je sais, c’est une lubie de site média, et vous pouvez tout à fait zapper cette partie si jamais vous n’avez pas de refresh auto (enfin bon, franchement, ça serait dommage de ne pas proposer une feature aussi utile à votre audience, enfin bon après je suis pas expert en UX vous faites ce que vous voulez venez juste pas pleurer après mais bon je m’égare un peu là).

Pour commencer, il faut absolument disposer d’une petite variable allant chercher la meta refresh (qui donne la durée en secondes), que vous pouvez créer de la sorte :

function(){
	var rob = '0';
    //Dans le cas où la page ne comporte pas de meta refresh, on la sette à 0.
    if(document.querySelector('meta[http-equiv="Refresh"]')){
    	rob = document.querySelector('meta[http-equiv="Refresh"]').content;
    	//Si la page comporte une meta robots, on la lit
    	}
    return rob;
	}

(A noter que cette variable ne fonctionne pas si vous avez un refresh à base de JS et non de meta).

Donc, revenons à nos moutons : une fois la variable créée, voici le bout du script qui calcule si la page est un refresh :

/Update du No de refresh
    var gtmExistRefreshDiff = gtmCurrentTimeStamp - gtmCib.lastTimeStamp,
        gtmExistRefreshReferrer = {{URL - Referrer}} === {{URL - URL complète}},
        gtmExistRefreshValueMin = {{JS - metaRefresh}}*1000-15000,
        gtmExistRefreshValueMax = {{JS - metaRefresh}}*1000+15000;
      
    if(gtmExistRefreshDiff>gtmExistRefreshValueMin && gtmExistRefreshDiff<gtmExistRefreshValueMax && gtmExistRefreshReferrer){
    //Nous sommes visiblement sur un refresh
        gtmCib.NoRefresh = parseInt(gtmCib.NoRefresh,10)+1;
        }
    else{
        gtmCib.NoRefresh = 1;
        }

Cela peut sembler un peu compliqué, mais en fait pas tant que ça. Mais un peu quand même. Nous avons besoin de non pas une, non pas deux, mais oui Monsieur, QUATRE variables qui vont nous servir à savoir si la page est vue dans un contexte de refresh :

  • gtmExistRefreshDiff calcule la différence entre le timestamp courant et celui de la dernière page, en utilisant les variables génériques définies au tout début de notre script.
  • gtmExistRefreshReferrer compare les 2 variables natives GTM que vous aurez pris le soin de créer au préalable. Ce triple égal (===) nous renverra  un boléen true si l’URL courante est identique au referrer.
  • gtmExistRefreshValueMin et gtmExistRefreshValueMax vont nous permettre de déterminer quel est l’intervalle de confiance que nous accordons pour déterminer si nous sommes en présence d’un refresh. En effet, si mon refresh est de 5 minutes (donc, 300 secondes, donc 300 000 millisecondes), la différence entre les 2 timestamps ne va pas être exactement de 300 000 ms, ça serait beaucoup trop simple. Donc, on se laisse une petite marge (-15 à +15, largement suffisant par expérience, à éventuellement diminuer si votre site est très rapide).

Une fois ma petite cuisine faite, je vais mettre tout ça dans un gros if (si je suis entre la durée minimale, la durée maximale, ET que l’URL courante est identique au referrer, si avec ça vous êtes pas contents je sais pas ce qu’il vous faut), et, le cas échéant, j’incrémente gtmCib.NoRefresh, en prenant le soin de le passer à la moulinette « parseInt » pour être sûr de ne pas ajouter un 1 derrière un string (ça serait balot).

Calculs des jours actifs

Last but not least, nos fameux jours actifs mensuels, qui, je n’en doute pas une seconde, vont ravir même les plus Javascrisceptiques (ça fait un peu nom de pastilles pour fosse sceptique, mais passons) d’entre vous :

//Update jours actifs
    var gtmExistJACookie = gtmCib.lastActiveDay.split('|'),
        gtmExistJACurrent = gtmJA.split('|'),
        gtmExistYearDiff = gtmExistJACurrent[0] - gtmExistJACookie[0],
        gtmExistMonthDiff = gtmExistJACurrent[1] - gtmExistJACookie[1],
        gtmExistDayDiff = gtmExistJACurrent[2] - gtmExistJACookie[2];
    if (gtmExistMonthDiff > 0 || gtmExistYearDiff > 0){
        //Nouveau mois
        gtmCib.NBActiveDays = 1;
        gtmCib.lastActiveDay = gtmJA;
        }
    else if(gtmExistDayDiff > 0){
        //Nouveau jour du même mois
        gtmCib.NBActiveDays = parseInt(gtmCib.NBActiveDays,10)+1;  
        gtmCib.lastActiveDay = gtmJA;
        }

Comme pour notre mécanique de jours actifs, nous allons avoir besoin de quelques variables préliminaires. Nous créons donc « gtmExistJACookie » et « gtmExistJACurrent », qui vont nous donner respectivement taper dans le cookie, et la date courante, en les transformant en tableau JS grâce à un élégant « split ». Cela va nous permettre de les comparer grâce à nos calculs : « gtmExistYearDiff » calcule la différence entre les années, « gtmExistMonthDiff » entre les mois, « gtmExistDayDiff » entre les jours.

Ainsi, une fois ces calculs faits, il n’y a plus qu’à exécuter la sentence : si on est sur un nouveau mois, ou (le fameux ||) une nouvelle année, on réinitialise le compteur de jours actifs. Si on est simplement sur le nouveau jour d’un même mois, on incrémente. Sinon? Eh ben sinon, on ne fait rien, parce que nous sommes sur le même jour.

Timestamp, date d’expiration, et terminé bonsoir

    //Update du timestamp
    gtmCib.lastTimeStamp = gtmCurrentTimeStamp;
    
    //Update de la date d'expiration
    gtmCib.Exp = gtmExpirationTimeStamp.toString();
        
    gtmExpirationDtCoo = gtmTStoUTC(parseInt(gtmCib.Exp,10));

Ne pas oublier de resetter « gtmCib.lastTimeStamp » après tous nos calculs pour le prochain chargement de page, puisque c’est cette variable qui va, ne l’oublions pas, nous servir de base à tous nos calculs par la suite.

Ensuite, petite subtilité : on n’oublie pas d’aller récupérer; grâce à « gtmCib.Exp », la date d’expiration initiale de notre cookie. L’idée est de faire du 13 mois, mais pas n’importe quel 13 mois, Monsieur, du 13 mois bio, du 13 mois non glissants.

Ah, oui, vous vous demandez sûrement pourquoi 13 mois non glissants, et pas 13 mois tout court? Je vous laisse deviner, mais petit indice. Un sigle, 4 lettres, et votre DPO, juste derrière vous, dont vous sentez le souffle dans votre cou.

Nous en avons terminé avec la boucle de l’enfer « le cookie existe déjà », mais n’oublions pas que notre « gtmCib » n’est encore qu’un modeste objet JS, et qu’il passera tout de même au travers de la moulinette de cookification que constitue la dernière ligne du script.

Et donc, comme promis, terminé bonsoir.

Question existentielle : ce tag, il est bien joli, mais à quel moment est ce que je l’exécute? Idéalement, j’en aurai besoin si je veux récupérer les infos pour mon tag de page GA (on y revient juste après). La solution la plus simple consiste donc à l’envoyer, en séquentiel, avant le tag de page (ça s’appelle « Balise setup » dans l’interface de GTM, qui devait être en RTT au moment d’être traduite en français) :

Utilisation des variables du cookie

Donc, tout ça, c’est bien joli, nous avons un cookie qui fait de belles choses, mais il va falloir l’utiliser à bon escient.

J’ai déjà dû vous le dire, que le JSON c’était fantastique? Eh bien, vous allez le voir, grâce à ce merveilleux format que nous avons utilisé pour alimenter notre cookie, nous allons pouvoir récupérer de façon très smooth les différentes valeurs contenues dans ce JSON.

Pour commencer, voyons comment nous allons setter nos variables GTM afin d’aller parser le cookie. Bien sûr, on commence par la très classique variable native qui va chercher la valeur du cookie :

Et ensuite, il est ultra facile de récupérer une des valeurs du JSON, par exemple, le nombre de pages vues de la session en cours :

Notez l’utilisation du « return un truc » direct, petite astuce #élégance une fois de plus, plus rapide à exécuter qu’un « var toto = un truc puis return toto ».

Pas la peine de se fader toute la liste, absolument toutes ces variables sont récupérables exactement de la même façon.

Maintenant, prenons un peu de recul, pour revenir quelque chose comme 4200 signes en arrière : Sophie, qui a installé Calculator bien sagement, veut maintenant pouvoir aller cibler avec swag son super tag média. Rien de plus simple :

Mais de rien Sophie, tout le plaisir est pour moi. Mes amitiés à ton chef de projet technique.

Alors bien sûr, il est possible, et même souhaitable, d’insérer également ces infos dans les custom dimensions (ou info équivalente si vous n’utilisez pas GA) :

L’utilisation de ces infos en dimension secondaire, mais aussi en custom reports sont éloquents :

Eh ouais les copains, connaître, sur 100 pages vues pour une page donnée, son breakdown par numéro de page peut-être, comme on dit sur l’internet mondial, un excellent insight quant à ce qui touche à l’engagement.

J’insiste en particulier sur la notion de « jours actifs mensuels », qui, callés bien au chaud dans une custom dimension de scope user, permettant là encore de faire une segmentation  fort intéressante :

N’hésitez pas à vous reportez à mon article sur les scopes de dimensions pour mélanger tout ça avec vigueur, mais dans le respect de la personne, bien sûr.

Améliorations

Au fur et à mesure du dèv de ce jovial script, j’ai amélioré le script, mais il reste encore quelques pistes d’optimisation :

  • Crypter le contenu en base 64 au moment de la pose du cookie, et décrypter pour le parsing. Plus propre question sécurité, mais moins pratique côté débug.
  • Encapsuler tout le script dans une fonction IEF (Immediately Executed Functions) permettant de ne pas taper dans des variable de scope global.
  • Améliorer le contrôle d’intégrité du cookie, lorsqu’il existe déjà, pour contrôler qu’il contient bien les clés / valeurs qui vont bien, et possiblement rectifier le tir, afin d’éviter de gros bugs très bruts de décoffrage.
  • Bien entendu, il est possible de faire une foultitude de choses (utilisateurs ayant déjà fait un achat, s’étant déjà loggués…) et même possiblement de mettre en place des cohortes sur cette base. The sky is the limit, comme dit Loïc le Meur.

Comme d’habitude, tous commentaires et suggestions sont les bienvenus. Jouez avec Calculator, tordez le, expérimentez le sur vos sites, je suis convaincu qu’il est possible de faire des tonnes de choses avec. Allez, à bientôt sur l’internet.

3 commentaires

  1. Excellent article
    Pour ma part, je trouve tellement dommage que les gestionnaires tags ne gèrent pas nativement davantage de choses (privacy, cookie virtuel, persistence de variables entre pages…) et qu’il soit régulièrement nécessaire de mettre la main à la pâte.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *