Enquête, expérimentations et résolution d'anomalies sur mobile
On a rencontré une anomalie qui nous a donné du fil à retordre fin mars. J’ai envie de raconter cette histoire parce que je trouve intéressant de partager certains éléments que j’utilise pour m’aider dans ces situations. Je vais essayer de ne pas donner trop de détails futiles.
Tout commence lorsque notre PO, vient nous voir pour nous montrer un problème avec la liste des offres mises en favoris. Pour une catégorie d’offre en particulier, l’icône favori n’est pas dans le bon état.
Première action, j’essaie de reproduire le problème. Avant de foncer dans le code, je cherche les scénarios utilisateurs qui provoquent le problème et je les note. Et ensuite, avec ça, je peux me prouver que l’anomalie est résolue en ayant écrit des tests et en rejouant le scénario avec/sans correctif. C’est un des premiers conseils que je voudrais partager dans cette histoire.
Grâce à un scénario précis, on peut également affiner l’origine du problème. Par exemple, ici, ça m’a permis de voir que le problème n’était absolument pas lié à la catégorie de l’offre. L’origine se situait dans notre cache HTTP - il nous manquait une invalidation du cache suite à une action spécifique. On corrige le problème en pair-programming et, tout content de notre avancée, on informe notre PO qu’une nouvelle version de l’application est déployée en recette. On passe à autre chose. Fin de l’histoire, tous les problèmes sont résolus.
Tous ? Non ! Un village peuplé d’irréductibles anomalies résiste encore et toujours aux développeurs. Le fil à retordre va devenir bien rigide.
À notre grande surprise, notre PO revient me voir : “je fais comme avant et j’ai le même problème, la correction n’aide pas”. Ok. Très étrange. On a pourtant bien constaté la résolution avec nos scénarios. La cause identifiée était parfaitement logique. La partie métier est très bien couverte par des tests. Je recommence le scénario sur mon téléphone, c’est OK. Qu’est-ce qui diffère entre nos téléphones ?
Frédéric a constamment le problème alors que de mon côté, je ne l’ai jamais. En creusant et en essayant diverses choses (même modèle de téléphone, s’échanger les comptes utilisateurs, …), on finit par trouver. Il télécharge l’application depuis Firebase Distribution alors que moi je l’exécute directement depuis mon IDE. On a la même base de code, mais la compilation diffère légèrement - lorsqu’une application est déployée, elle est compilée en mode release (il y a des optimisations, des règles plus strictes, etc).
Super ! On sait reproduire à 100%. Super ! Compiler en mode release signifie aussi que je me coupe de tout un tas d’outils de développeurs pour observer ce qu’il se passe. Je ne peux pas debugger avec des breakpoints, je ne peux pas lire les logs, je ne peux pas observer les échanges réseaux, et, très embêtant, les build en mode release sont bien plus long à générer que l’exécution en mode debug.
À ce moment-là, je me dis qu’on a un bon problème mystique. Je n’arrive pas à reproduire à 100% en mode debug, et on ne sait rien de la cause. C’est à mon tour de changer de mode : je passe en mode “enquête”.
Je commence par ouvrir un document pour écrire ce que je sais du contexte et du scénario qui reproduit le problème. Ça me permet de rassembler toutes les informations, de vérifier si je passe à côté de quelque chose d’évident en l’explicitant, et de pouvoir mieux communiquer. Puis je lance une multitude d’hypothèses. On a plusieurs appels HTTPs qui se déclenchent au lancement de l’application, dont la liste des favoris. Est-ce que notre backend rejette certaines requêtes parce qu’un même compte utilisateur fait trop d’appels simultanés ? Est-ce le client HTTP utilisé dans l’application ? Est-ce le cache HTTP ? Est-ce l’OS du téléphone qui empêche de faire trop de requêtes dans un trop court laps de temps ? Etc. J’essaie de ne fermer aucune porte.
Ensuite, je me lance dans des expérimentations rapides. Je cherche à valider ou invalider des hypothèses pour affiner progressivement l’origine. Pour le moment l’éventail des possibilités est trop larges. Je cherche aussi à apprendre de nouvelles informations avec les résultats de ces expérimentations.
Je commence par m’orienter sur le backend. Je veux voir ce qu’il se passe alors j’ouvre Elastic (un outils d’analyse de logs très puissant ajouté sur notre backend) et je filtre les logs sur l’identifiant de mon compte utilisateur. Tiens, la requête des favoris n’apparaît pas. Je vais voir un développeur backend pour creuser cette curiosité. On fait plusieurs vérifications, notre backend n’a pas de mécanisme pour rejeter la requête, et ce n’est pas l’infrastructure non plus. J’invalide donc l’hypothèse du backend, et je repars avec un apprentissage intéressant : la requête n’arrive pas au backend.
Je repasse sur l’application mobile. Je veux travailler l’hypothèse du cache. Au tout début, j’ai effectué un correctif mais il reste un problème. Le mécanisme dans sa globalité a un peu de complexité, je cherche un réponse stricte et binaire : est-ce que ça vient du cache, OUI ou NON ? Je désactive le cache simplement, au plus haut niveau, sur toutes les routes - il n’est plus du tout utilisé. Résultat, le problème est toujours là, l’hypothèse est invalidée.
Il faut chercher ailleurs, mais je manque d’indices. J’ai besoin de rendre l’application davantage observable. Pour rappel, en mode release, l’application n’écrit pas dans la console lorsque l’on appelle la fonction notre logger. Néanmoins, la fonction est tout de même appelée. Eh bien, je peux modifier la fonction pour stocker les logs en mémoire, et ajouter un bouton sur la page d’accueil qui me permet de me partager les logs (via un mail ou une note synchronisée dans le cloud). Je peux faire ça très rapidement, en quelques minutes.
J’insiste à nouveau sur rapide. Tout le long de mon enquête, je veux une boucle de feedback très courte, je ne cherche pas à coder pendant 3h un truc over engineered ou trop clean ou trop future-proof, ce n’est pas le but. Je ne conserverais aucun code à la fin. Lorsque j’aurais quelque chose qui fonctionne et qui aura corrigé le problème, je recommencerais proprement de zéro en sachant où je dois aller.
Maintenant que j’ai accès à des logs provenant de l’application, je constate que la requête HTTP ne part pas. Ça confirme ce qu’on a pu voir sur le backend, et le besoin d’avoir plus d’informations pour comprendre pourquoi la requête ne part pas. J’ajoute alors davantage de logs, pour voir si chaque brique est bien appelée. Middleware, repository, action, etc. Tout le cheminement est bien fait, et ce n’est pas surprenant vu que le use case est couvert par des tests.
Il y a quelque chose de plus fin à trouver. Je mets des logs juste avant et juste après l’appel HTTP. Bingo. Une bizzarerie se met en évidence : le log après l’appel HTTP n’est jamais appelé, et il n’y a pas non plus d’exception soulevée. La fonction semble ne jamais rendre la main. Première hypothèse, on pense que c’est dû à la co-existance de deux clients HTTPs (le temps d’une migration douce). Alors on migre vulgairement ces quelques requêtes sur le même client. Invalidée. On commente les appels réseaux au démarrage, sauf ceux des favoris. Invalidée.
Je saute dans le temps pour arriver à la fin. On fini par identifier la source du problème et y apporter une correction. On comprend au passage qu’il y avait deux problèmes distincts, le premier correctif était aussi nécessaire. Tout fonctionne parfaitement. Victoire, par toutatis !
Au travers de cette histoire, j’ai essayé de mettre en lumière certains aspects. L’utilité d’avoir des scénarios de reproduction précis, tout d’abord, pour vérifier que l’anomalie est corrigée, écrire des tests, éliminer/affiner de premières hypothèses. Puis le fonctionnement en expérimentations rapides. On essaie d’avoir un tas d’hypothèses, d’en sélectionner une à la fois, de valider ou invalider rapidement avec des actions simples, d’apprendre de nouvelles informations pour la suite de l’enquête.
Il y a aussi une notion d’observabilité. On a besoin de comprendre ce qu’il se passe pendant qu’un logiciel est utilisé. Côté backend, on est très bien équipé avec la suite ELK par exemple. Mais côté mobile, Crashlytics n’est pas du tout suffisant. Les crashs ne sont qu’un sous-ensemble des problèmes qui peuvent arriver, et l’interface est loin d’être aussi puissante qu’ElasticSearch. On peut s’améliorer dans ce domaine tout en respectant les contraintes liées à l’écosystème mobile.
À de multiples reprises, j’étais également en pair-programming, avec plusieurs collègues. Rien ne vaux plusieurs cerveaux, plusieurs idées, visions, façons de faire.