Analytics

Un moyen simple de mesurer l’impact du scintillement des tests A/B

Le “scintillement” ou “Flash Of Original Content” (FOOC) est un phénomène où il y a un délai (généralement) léger mais observable dans le navigateur mettant à jour le site ou la disposition des éléments si l’utilisateur est inclus dans un groupe de variantes pour expérimentation. Cela se manifeste par le rendu de l’élément d’origine non modifié dans la partie visible de la page avant que la bibliothèque d’expériences ne le mette à jour avec la variante.

Il existe des moyens d’atténuer le scintillement :

  1. Ajoutez la bibliothèque de tests A/B directement dans le modèle de page et ne la chargez pas via une autre dépendance chargée de manière asynchrone (par exemple, Google Tag Manager).
  2. Chargez la bibliothèque de test A/B de manière synchrone et faites-la masquer l’élément qui est testé jusqu’à ce que la bibliothèque soit chargée.
  3. Utilisez une sorte de technologie anti-scintillement.
  4. Exécutez les expériences côté serveur et affichez le contenu avec la variante en place.

Test de scintillement

En règle générale, le seul moyen non intrusif et cohérent d’éviter le scintillement consiste à examiner le rendu côté serveur pour vos expériences. Par exemple, des outils comme Conductrics offrent un ensemble robuste d’API pour faire toute la logique de prise de décision dans votre serveur. Ensuite, il existe des outils comme Google Optimize qui vous obligent à sélectionner et à attribuer manuellement les variantes, mais l’outil peut ensuite gérer la collecte de données et la création de rapports.

Cependant, la raison pour laquelle vous avez lu jusqu’ici est probablement parce que vous vous inquiétez test côté client.

Table des matières

Table des matières

[+show] [–hide]

Présentation du problème

Avec les bibliothèques d’expérimentation basées sur JavaScript, vous êtes soumis aux règles et limitations du rendu de la page dans le navigateur. Les vaciller se produit parce que la page avec l’élément modifié est rendue à partir de la source HTML de la page, mais la bibliothèque d’expérimentation doit attendre une ouverture pour permettre au navigateur de traiter les données d’expérimentation.

C’est le plus souvent un problème lorsque vous exécutez des scripts de manière asynchrone. Le chargement asynchrone signifie qu’une fois que le navigateur commence à télécharger la bibliothèque, il n’attend pas la fin du téléchargement. Au lieu de cela, il procède au rendu de la page. Une fois le téléchargement terminé, et dès que le navigateur dispose d’un emplacement disponible dans son seul thread d’exécution, il commencera à analyser et à exécuter le JavaScript dans la bibliothèque.

En passant d’un chargement asynchrone à un chargement synchrone, vous résolvez une partie de ce problème. Cependant, ce n’est pas comme si le chargement synchrone corrige quoi que ce soit automatiquement. Étant donné que la bibliothèque est chargée en haut de <head>une bibliothèque chargée de manière synchrone n’a pas accès aux éléments qu’elle est censée modifier (puisque ces éléments sont créés dans le <body>qui n’a pas encore été généré).

Au lieu de cela, des bibliothèques telles que Google Optimize, lorsqu’elles sont chargées de manière synchrone, cacher l’élément qui est testé. Ils injectent un déclaration de style qui définit la visibilité de tous les éléments correspondant au sélecteur CSS des cibles d’expérimentation à hidden. Ce n’est qu’une fois l’élément réellement ajouté à la page qu’Optimize peut alors le modifier et l’afficher. C’est assez élégant, mais cela pourrait introduire un léger scintillement d’un autre type, où l’élément semble “apparaître” en place hors séquence avec le reste du rendu.

optimise-hides.jpg

Une solution similaire est anti-scintillement JavaScript. Le but ici est en fait masquer toute la page jusqu’à ce que la bibliothèque d’expérimentation soit chargée. C’est, et a été, ma plus grande objection sur la façon dont les outils de test A/B sont mis en œuvre. Je ne peux tout simplement pas comprendre la logique derrière le fait de sacrifier potentiellement la convivialité de la page entière juste pour obtenir une meilleure qualité des données pour votre expérimentation.

Considérant à quel point les performances de la page et perçu la performance de la page est de nos jours, j’évite les extraits anti-scintillement qui masquent la page entière. Peu importe si des mesures d’atténuation sont en place pour les bloqueurs de publicités et les erreurs de téléchargement. Si le point de terminaison ne répond pas ou est en retard, l’extrait de code anti-scintillement par défaut de Google Optimize fait attendre la page pendant un maximum de 4 secondes (c’est réglable) avant de révéler le contenu. Naturellement, si le conteneur se charge avant cela, la page se révèle plus rapidement. Mais quand même, OUCH !

Mesurer l’impact du scintillement

Supposons donc que la situation est la suivante :

Vous avez une expérience en cours qui traite un élément de la page d’accueil, qui est visible au-dessus du pli si la page est chargée sans seuil de défilement en place.

Vous avez déployé Google Optimize à l’aide du nouvel extrait de code. Vous avez déployé le asynchrone extrait, et vous êtes ne pas en utilisant le JavaScript anti-scintillement, il y a donc un scintillement visible et mesurable en place.

Scintillement de l'original (fond gris) avant la variante (fond rouge)

Scintillement de l’original (fond gris) avant la variante (fond rouge)

Afin de mesure la gravité de ce scintillement, nous devons collecter un certain nombre de timings :

  1. Heure à laquelle l’élément d’origine a été ajouté à la page,
  2. Heure à laquelle l’élément d’origine est devenu visible dans la fenêtre,
  3. Heure à laquelle la bibliothèque d’expérimentation a été chargée,
  4. Heure à laquelle la modification du test a été appliquée à la page.

Les vaciller est le delta de temps entre (2) et (4). Si l’élément n’est pas visible dans la fenêtre ou si l’expérience est appliquée avant l’élément de base devient visible, le scintillement n’est pas un problème. (3) sont des métadonnées intéressantes sur le fonctionnement de la bibliothèque d’expérimentation elle-même et sur la rapidité avec laquelle elle parvient à appliquer le changement après le chargement.

Introduction au JavaScript dont nous aurons besoin

La solution reposera sur deux morceaux de Javascript code en cours d’exécution directement dans le modèle de page. Toi ne peut pas exécuter ce code de manière fiable via une dépendance telle que Google Tag Manager, car Google Tag Manager charge dans de nombreux cas après toutes les étapes (1) à (4) ont déjà eu lieu, ce qui signifie que vous n’obtiendrez pas de mesures précises.

Le premier bit de JavaScript est exécuté tout en haut de <head>, avant même l’extrait Optimize. Ce script utilise le optimize.callback API pour collecter l’horodatage du chargement de la bibliothèque d’expérimentation. C’est le numéro de synchronisation (3) dans la liste ci-dessus.

Le deuxième extrait de code JavaScript est ajouté au sommet de <body>parce que les observateurs ont besoin d’accéder à document.body. Voici ce qu’il fait :

  • UN MutationObserver attend sur la page et réagit à deux changements : lorsque l’élément est ajouté pour la première fois à la page et lorsque l’élément est mis à jour avec la variante. Ce sont les horaires (1) et (4), respectivement, dans la liste ci-dessus.
  • Une IntersectionObservateur est ajouté à la page dès que l’élément d’origine est rendu. Le but de IntersectionObserver est de déclencher un rappel dès que l’élément d’origine est visible dans la fenêtre. C’est le timing (2) dans la liste ci-dessus.

Une fois les timings collectés, ils sont poussés dans dataLayer à utiliser dans Google Tag Manager.

Autres préparations

Pour mesurer au mieux l’application de l’élément expérimental, j’ai ajouté le attribut de données data-test="true" à la variante. Cela me permet de localiser plus facilement l’élément à l’aide des sélecteurs CSS.

L’attribut est ajouté via l’éditeur Optimize, et n’est donc présent sur l’élément qu’après sa modification par Google Optimize.

attribut data-test sur l'élément

Enfin, je collecte toutes ces données à l’aide de Google Tag Manager, et je les envoie à App + Web car je souhaite les collecter dans BigQuery pour une analyse plus granulaire.

Vous pouvez tout aussi bien calculer le delta directement dans le client et l’envoyer, par exemple, à Universal Analytics sous forme d’événement. Cela dépend entièrement de vous. J’ai opté pour l’approche BigQuery – je le justifie plus loin dans l’article.

Installation de la bibliothèque et du rappel Optimize

Pour installer la bibliothèque Optimize, j’ajoute le <script> élément avec le async attribut au sommet de <head>.

Optimiser l'extrait

En l’ajoutant en haut de <head>je n’élimine pas le scintillement (car il est toujours chargé de manière asynchrone), mais je m’assure que le téléchargement de la bibliothèque commence dès que le rendu de la page démarre. Cela permet d’atténuer le scintillement d’un génial accord.

Ensuite, pour ajouter le rappel Optimize, j’exécute le script suivant avant l’extrait Optimize au tout début de <head>.

<html>   <head lang="en-us">     <meta name="generator" content="Hugo 0.61.0" />     <script>       function gtag() { window.dataLayer = window.dataLayer || []; window.dataLayer.push(arguments); }       gtag('event', 'optimize.callback', { 	    callback: function(e) { 	      window.__flickerTestMilestones = window.__flickerTestMilestones || {};           window.__flickerTestMilestones.experimentLoaded = new Date().getTime(); 	    }       });     </script>     ...   </head>   ... </html>

Ici, nous créons d’abord le gtag file d’attente (car c’est ce qu’Optimize utilise pour son contrôle d’API). Ensuite, nous poussons un rappel sous la forme d’un événement gtag. Je passe une fonction anonyme à l’argument de rappel. Cette fonction fait référence à un objet global que nous utiliserons pour collecter les jalons. Le seul jalon que nous remplissons dans ce rappel est experimentLoadedet nous y attachons l’horodatage actuel.

Le rappel est invoqué dès que la bibliothèque d’expériences est chargée et qu’Optimize a établi à quelle variante appartient l’utilisateur (et donc quelle version de l’élément lui montrer).

Installation des scripts d’observation

Voici la partie délicate. Vous devez installer deux observateurs (un MutationObserver Et un IntersectionObserver). Le premier vérifie si un élément a été ajouté à la page, et le second vérifie si un élément est dans la fenêtre. Je vais d’abord vous montrer le code, puis vous expliquer ce qu’il fait.

<body>   <script>       (function() {         var ftm = window.__flickerTestMilestones = window.__flickerTestMilestones || {};         var testState = 'success';         var dpush = function() {           if (testState !== 'noObservers') {             // If milestones are incomplete and it's not because of lack of support, do nothing             if (!ftm.experimentLoaded ||                 !ftm.baseElementAddedToPage ||                 !ftm.testElementAddedToPage) return;              // If all other milestones are in place, but baseElementVisible is not,             // send the other timings and make note that base element was not visible.             if (!ftm.baseElementVisible) { testState = 'baseNotVisible'; }           }            // Push everything to dataLayer           window.dataLayer = window.dataLayer || [];           window.dataLayer.push({             event: 'optimize_flicker_test',             testMilestones: {               baseElementAddedToPage: ftm.baseElementAddedToPage,               baseElementVisible: ftm.baseElementVisible,               experimentLoaded: ftm.experimentLoaded,               testElementAddedToPage: ftm.testElementAddedToPage,               testState: testState             }           });                      // Reset the test           window.__flickerTestMilestones = {};         };                  // Only run if observers are supported by the browser         if (window.MutationObserver && window.IntersectionObserver) {           var observer = new MutationObserver(function(mutations) {             mutations.forEach(function(mutation) {               var node = !!mutation.addedNodes.length && mutation.addedNodes[0];               if (node && node.matches && node.matches('span.talks')) {                 if (node.matches('[data-test]')) {                   ftm.testElementAddedToPage = new Date().getTime();                   dpush();                 } else {                   ftm.baseElementAddedToPage = new Date().getTime();                   dpush();                   var intersectionObserver = new IntersectionObserver(function(entries) {                     if (entries.some(function(e) {                         return e.intersectionRatio > 0                       })) {                       ftm.baseElementVisible = new Date().getTime();                       dpush();                     }                   });                   intersectionObserver.observe(node);                 }               }             });           });           observer.observe(document.body, {             childList: true,             subtree: true           });         } else {           // Make note that there was no support for observers           window.__flickerTestMilestones = {};           testState = 'noObservers';           dpush();         }       })();     </script>   ... </body> </html>

Ce script s’exécute tout en haut de <body> afin que les observateurs puissent être amorcés le plus rapidement possible.

La première chose à vérifier est de savoir si le navigateur prend en charge les deux MutationObserver et IntersectionObserver. Nous n’avons pas besoin de prendre en charge tous les navigateurs pour cela – nous avons juste besoin d’un échantillon représentatif. S’il n’y a pas de support, alors le dataLayer.push() comprend le testState clé avec la valeur noObserverset nous pouvons l’utiliser dans nos analyses.

Je n’ai pas opté pour une solution de repli en interrogeant simplement la page jusqu’à ce que l’élément soit trouvé. Cela aurait rendu le code plus complexe qu’il ne l’est déjà, et cela aurait potentiellement introduit des problèmes de performances que je préfère éviter lors de l’expérimentation de données.

Le script insère alors le MutationObserver. Ce modèle d’observateur peut être utilisé pour détecter des éléments tels que l’ajout d’éléments DOM à la page ou la modification d’attributs pour des éléments individuels.

Seuls les nœuds enfants ajoutés à la page m’intéressent, car l’élément d’origine (par le moteur du navigateur analysant la source HTML) et la variante (par la bibliothèque Optimize) sont ajoutés en tant que nouveaux éléments à la page. L’observateur est amorcé comme ceci :

var observer = new MutationObserver(callback); observer.observe(document.body, {   childList: true,   subtree: true }); 

Nous attachons l’observateur à document.body, et nous réagissons à toute modification des nœuds enfants, quelle que soit leur profondeur dans la sous-arborescence. Si un changement est détecté, la fonction de rappel est exécutée.

Dans ce cas, je m’assure que le rappel de l’observateur ne réagit que lorsque l’élément que je teste actuellement est ajouté à la page :

if (node && node.matches && node.matches('span.talks')) { 

Ensuite, le code vérifie si le nœud qui a été ajouté est l’élément expérimental :

if (node.matches('[data-test]')) { 

Si vous vous souvenez, j’ai mentionné ci-dessus que j’ajoute le data-test="true" attribut à l’élément de test pour faciliter le débogage.

Si l’élément ajouté était l’élément de test, je mets à jour le jalon pour testElementAddedToPage avec l’horodatage actuel. C’est le moment où Optimize a ajouté l’élément modifié à la page et servira de point final de notre mesure delta.

Si l’élément était ne pas l’élément expérimental, il a être l’élément de base, donc je mets à jour le jalon baseElementAddedToPage avec l’horodatage.

Puisque je veux connaître le moment où l’élément de base est devenu visible dans la fenêtre, dans le rappel où je traite l’élément non expérimental, j’attache un IntersectionObserver à elle aussi.

var intersectionObserver = new IntersectionObserver(function(entries) {   if (entries.some(function(e) {     return e.intersectionRatio > 0;   })) { ... } }); intersectionObserver.observe(node); 

Les IntersectionObserver s’active chaque fois que l’élément observé entre dans la fenêtre d’affichage du navigateur. Je peux alors vérifier si l’élément est visible même le plus infime (intersectionRatio > 0), puis mettez à jour le jalon pour baseElementVisible avec l’horodatage.

Après chaque jalon, je vérifie si au moins baseElementAddedToPage, experimentLoadedet testElementAddedToPage les jalons ont été mis à jour. Si c’est le cas, les jalons et l’état du test sont poussés dans dataLayer.

Jalons de test

Il y a deux raisons pour lesquelles je n’attends pas baseElementVisible:

  • Parfois, l’expérience charge l’élément mis à jour si rapidement que l’élément de base est déjà supprimé de la page lorsque l’IntersectionObserver est censé s’éteindre.
  • Parfois, l’utilisateur a fait défiler au-delà du pli, et le baseElementVisible ne se déclenche tout simplement pas (parce que l’élément de base n’est pas, eh bien, visible).

Les deux signifient que le scintillement n’est fondamentalement pas un problème, donc c’est OK pour moi de simplement collecter un null dans ces cas. je mets à jour testState avec "baseNotVisible" pour faciliter leur analyse dans l’analyse.

Configuration des ressources Google Tag Manager

Dans GTM, le déclencheur J’ai besoin ressemble à ceci:

Optimiser le déclencheur d'événement personnalisé

Tu auras besoin cinq Variables de la couche de données. Chacun est configuré comme ceci :

Variable de couche de données de temps d'expérience

Les noms de variables dont vous aurez besoin sont :

  • testMilestones.baseElementAddedToPage
  • testMilestones.baseElementVisible
  • testMilestones.experimentLoaded
  • testMilestones.testElementAddedToPage
  • testMilestones.testState

Et c’est ce que le Application + Web la balise d’événement ressemble à :

Balise d'événement Application + Web

Comme vous pouvez le voir, j’envoie également un “ID de page”. Je peux l’utiliser pour regrouper un peu plus facilement tous les horaires d’une combinaison utilisateur/session/page donnée. Ce n’est pas strictement nécessaire, mais cela pourrait faciliter un peu certaines analyses.

function() {   window.__flickerTestPageId = window.__flickerTestPageId || {{Random GUID}};   return window.__flickerTestPageId; } 

La variable « GUID aléatoire » est une autre variable JavaScript personnalisée qui renvoie un identifiant aléatoire et assez unique.

En définissant une variable globale, nous nous assurons que le même ID est utilisé pour tous les événements mesurés.

Lorsque je charge maintenant la page d’accueil de mon site, voici ce que je vois être envoyé à App + Web :

Charge utile de la demande

Sortie BigQuery

Dans BigQuery, nos paramètres d’événement sont ajoutés au event_parameters enregistrer. Ce n’est pas la manière la plus élégante de transmettre des paires clé-valeur, d’autant plus que les valeurs sont distribuées sous forme de colonnes avec une colonne par type (potentiel). Cependant, c’est le seul moyen pris en charge pour exporter des paramètres personnalisés pour le moment.

Sortie complète du tableau

Voici ce que ces optimize_flicker_test les hits ressemblent dans notre table d’événements :

SELECT   * FROM   `project.dataset.events_yyyymmdd` WHERE   event_name = "optimize_flicker_test"

Table complète SQL

Nombre d’états de test

Nous pouvons explorer et compter le nombre respectif de chaque état de test :

SELECT   ep.value.string_value AS test_state,   COUNT(*) as count FROM   `project.dataset.events_yyyymmdd`,   UNNEST(event_params) as ep WHERE   event_name = "optimize_flicker_test"   AND ep.key = "testState" GROUP BY   1 ORDER BY   2 DESC

Nombre d'états de test

Tableau avec deltas

Nous pouvons également construire une requête qui renvoie tous les jalons avec les deltas calculés :

WITH milestones AS (SELECT   (SELECT value.int_value FROM t.event_params WHERE key = "baseElementAddedToPage") as baseElementAddedToPage,   (SELECT value.int_value FROM t.event_params WHERE key = "baseElementVisible") as baseElementVisible,   (SELECT value.int_value FROM t.event_params WHERE key = "experimentLoaded") as experimentLoaded,   (SELECT value.int_value FROM t.event_params WHERE key = "testElementAddedToPage") as testElementAddedToPage FROM   `project.dataset.events_yyyymmdd` t WHERE   t.event_name = "optimize_flicker_test") SELECT     baseElementAddedToPage,   testElementAddedToPage-baseElementAddedToPage AS injection_delta,   baseElementVisible,   testElementAddedToPage-baseElementVisible AS flicker_delta,   experimentLoaded,   experimentLoaded-baseElementAddedToPage AS experiment_delta,   testElementAddedToPage FROM    milestones ORDER BY    flicker_delta DESC

Jalons calculés

Ce n’est qu’une façon d’aborder les données.

Ici, je calcule injection_delta comme le temps qu’il faut pour que l’élément de test soit ajouté à la page après l’élément de base a été ajouté. Cela pourrait être utilisé comme substitut pour mesurer la potentiel vaciller.

Les flicker_delta est le temps écoulé depuis que l’élément de base devient visible jusqu’à ce qu’il soit remplacé par la variante.

Enfin, experiment_delta est le temps écoulé entre l’ajout de l’élément de base à la page et le chargement du test. Cette valeur peut être négatifce qui signifie que la bibliothèque d’expériences est chargée avant l’élément de base a été ajouté à la page. Cela est possible surtout si la bibliothèque d’expériences se charge très rapidement après avoir été, par exemple, mise en cache…

Source : www.simoahava.com

Articles similaires

Laisser un commentaire

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

Bouton retour en haut de la page
Index