Skip to content

Bonnes pratiques pour la maintenance d'une application web

Published: at 12:00 AM

Comment ne pas jeter son application tous les deux ans ?

Retour d’expérience basé sur les bonnes pratiques appliquées à la plateforme web développée chez Bedrock Streaming

Un peu de contexte

Chez Bedrock Streaming de nombreuses équipes développent et maintiennent des applications frontend pour nos clients et utilisateurs. Certaines ne sont pas toute jeune. En effet, l’application sur laquelle je travaille principalement est un site web dont les développements ont commencé en 2014. Je l’ai d’ailleurs déjà évoquée dans différents articles de ce blog.

impression d'écran du nombre de commit sur master de notre projet 15668

Vous pourriez vous dire: “Oh les pauvres maintenir une application vieille de presque 10 ans ça doit être un enfer !”

Rassurez-vous, ce n’est pas le cas ! J’ai travaillé sur des projets bien moins vieux mais sur lesquels le développement de nouvelles fonctionnalités était bien plus pénible.

Aujourd’hui le projet reste à jour techniquement, on doit être sur la dernière version de React alors que celui-ci avait commencé sur une version 0.x.x. Dans ce monde des technologies web souvent décrié (ex: les nombreux articles sur la Javascript Fatigue) dont les outils et les pratiques évoluent constamment, conserver un projet “à jour” reste un vrai challenge.

nombre de versions de l'application 1445

De plus, dans le contexte de ce projet, en presque 10 ans, nous avons connu une centaine de contributeurs. Certains ne sont restés que quelques mois/années. Comment garder au maximum la connaissance sur “Comment on fait les choses et comment ça marche ?” dans un contexte humain si mouvant ?

liste des 100 contributeurs du projet

C’est ce que je vous propose de vous présenter.

Avec l’aide de mes collègues, j’ai rassemblé la liste des bonnes pratiques qui nous permettent encore aujourd’hui de maintenir ce projet en état. Avec Florent Dubost, on s’est souvent dit qu’il serait intéressant de la publier. Nous espèrons que cela vous sera utile.

S’imposer des règles et les automatiser

Un projet qui résiste au temps c’est tout d’abord un ensemble de connaissances qu’on empile les unes sur les autres. C’est en quelque sorte la tour de Kapla que vous assembliez petit en essayant d’aller le plus haut possible. Une base solide sur laquelle on espère pouvoir ajouter le plus possible avant une potentielle chute.

Dès le début d’un projet on est donc amené à prendre des décisions importantes sur “Comment on souhaite faire les choses ?”. On pense par exemple à “Quel format pour nos fichiers ? Comment on nomme telle ou telle chose ?” Écrire une documentation précise de “Comment on fait les choses” pourrait paraitre une bonne idée.

Cependant la documentation c’est cool, mais ça a tendance à périmer très vite. Nos décisions évoluent mais pas la documentation.

“Les temps changent mais pas les README.”

Olivier Mansour (deputy CTO à Bedrock)

Automatiser la vérification de chacune des règles qu’on s’impose (sur notre codebase ou nos process) est bien plus pérenne. Pour faire simple, on évite dans la mesure du possible de dire “On devrait faire les choses comme cela”, et on préfère “on va coder un truc qui nous le vérifie à notre place”. En plus de ça, coté JS on est vraiment bien équipé avec des outils comme Eslint qui nous permettent d’implémenter nos propres règles.

Le réflexe qu’on essaie donc d’adopter est donc le suivant:

L’intégration continue d’un projet est la solution parfaite pour ne rien louper sur chacune des Pull Request que nous proposons. Les reviews n’en sont que plus simples car vous n’avez plus à vous soucier de l’ensemble des règles qui sont déjà automatisées. Dans ce modèle, la review sert donc plus au partage de connaissance qu’au flicage de typo et autre non respect des conventions du projet.

Dans ce principe, il faut donc essayer de bannir les règles orales. Le temps des druides est terminé, s’il faut transmettre oralement toutes les bonnes pratiques d’un projet, l’accompagnement de nouveaux développeurs dans votre équipe n’en sera que plus long.

la recette de la potion magique de panoramix est perdue car secrète

Un projet n’est pas figé. Ces règles évoluent donc avec le temps. On préfèrera alors l’ajout de règles qui possèdent un script qui autofixera toute la codebase intelligemment. De nombreuses règles Eslint le proposent, et cela est vraiment un critère de sélection très important dans nos choix de nouvelles conventions.

eslint --fix

Une règle très stricte qui vous obligera à modifier votre code manuellement avant chaque push est pénible à la longue et énervera vos équipes. Alors qu’une règle (même très stricte) qui peut s’autofixer automatiquement au moment du commit ne sera pas perçue comme gênante.

Comment décider d’ajouter de nouvelles règles ?

Cette question peut paraitre épineuse, prenons par exemple le cas des <tab> / <space> dans les fichiers. Pour cela, on essaie d’éviter les débats sempiternels et on se plie à la tendance et aux règles de la communauté. Par exemple, notre base de configuration Eslint) est basée sur celle d’Airbnb qui semble avoir un certain succès dans la communauté JS. Mais si la règle qu’on souhaite s’imposer n’est pas disponible dans Eslint ou d’autres outils, il nous arrive de préférer ne pas suivre la règle plutôt que de se dire “On le fait sans CI qui vérifie”.

La liste presque exhaustive 🤞

Notre workflow d&#x27;intégration continue

Tester, tester, tester

J’espère qu’en 2021 il n’est plus nécessaire d’expliquer pourquoi tester automatiquement son application est indispensable pour la rendre pérenne. En JS on est plutôt bien équipé en terme d’outils pour tester aujourd’hui. Il reste cependant l’éternelle question:

“Qu’est-ce qu’on veut tester ?”

Globalement si on recherche sur internet cette question, on voit que des besoins différents font émerger des pratiques et des outils de testing bien différents. Ce serait très présomptueux de penser qu’il y a une bonne manière de tester automatiquement son application. C’est pourquoi il est préférable de définir une ou plusieurs stratégies de test qui répondent à des besoins définis et limités.

Nos stratégies de tests reposent sur deux volontés bien distinctes:

Pour cela, nous réalisons deux “types de tests” que je propose de vous présenter ici.

Nos tests E2E

On les appelle “tests fonctionels”, ce sont des tests End-to-end (E2E) sur une stack technique très efficace composée de CucumberJS, WebdriverIO avec ChromeHeadless Il s’agit d’une stack technique mise en place au début du projet (à l’époque avec PhantomJS pour les plus anciens d’entre-vous)

Cette stack nous permet d’automatiser le pilotage de tests qui contrôlent un navigateur. Ce navigateur va réaliser des actions qui se rapprochent le plus de celles que nos vrais utilisateurs peuvent faire tout en vérifiant comment le site réagit.

Il y a quelques années, cette stack technique était plutôt compliquée à mettre en place, mais aujourd’hui il est plutôt simple de le faire. Le site qui héberge cet article de blog en est lui-même la preuve. Il ne m’a fallu qu’une dizaine de minutes pour mettre en place cette stack avec le WebdriverIo CLI pour vérifier que mon blog fonctionne comme prévu.

J’ai d’ailleurs récemment publié un article présentant la mise en place de cette stack.

Voici donc un exemple de fichier de test E2E pour vous donner une idée:

Feature: Playground

  Background: Playground context
    Given I use "playground" test context

  Scenario: Check if playground is reachable
    When As user "toto@toto.fr" I visit the "playground" page
    And I click on "playground trigger"
    Then I should see a "visible playground"
    And I should see 4 "playground tab" in "playground"

    When I click on "playground trigger"
    Then I should not see a "visible playground"

    # ...

Et ça donne ça en local avec mon navigateur Chrome !

Exemple d&#x27;exécution de test fonctionnel

Voilà un schéma qui explique comment cette stack fonctionne:

schéma qui explique le fonctionnement de notre stack

Aujourd’hui, l’application web de Bedrock possède plus de 800 scénarios de tests E2E qui tournent sur chacune de nos Pull Request et sur la branche master. Ils nous assurent que nous n’introduisons pas de régression fonctionnelle et c’est juste génial !

👍 Les points positifs

👎 Les complications

Nos tests “unitaires”

Pour compléter nos tests fonctionnels nous avons également une stack de tests écrits avec Jest. On qualifie ces tests d’unitaires car nous avons comme principe d’essayer de toujours tester nos modules JS en indépendance des autres.

Ne débattons pas ici sur “Est-ce que ce sont des vrais tests unitaires ?”, suffisamment d’articles sur internet traitent de ce sujet.

On utilise ces tests pour différentes raisons qui couvrent des besoins que nos tests fonctionnels ne couvrent pas:

Avec ces tests, on se met au niveau d’une fonction utilitaire, d’une action Redux, d’un reducer, d’un composant React. On se base essentiellement sur la fonctionnalité d’automock de Jest qui nous propose d’isoler nos modules JS lorsqu’on teste.

représentation visuelle de l&#x27;automock

L’image précédente représente la métaphore qui nous permet d’expliquer notre stratégie de tests unitaires aux nouveaux arrivant.

“Il faut s’imaginer que l’application est un mur composé de briques unitaires (nos modules ecmascript), nos tests unitaires doivent tester une à une les briques en indépendance totale des autres. Nos tests fonctionnels sont là pour tester le ciment entre les briques.”

Pour résumer, on pourrait dire que nos tests E2E testent ce que notre application doit faire, et nos tests unitaires s’assurent eux de vérifier comment ça marche.

Aujourd’hui ce sont plus de 6000 tests unitaires qui couvrent l’application et permettent de limiter les régressions.

👍

👎

Nos principes

Nous essayons de toujours respecter les règles suivantes lorsqu’on se pose la question “Dois-je ajouter des tests ?”.

  1. Si notre Pull Request introduit des nouvelles fonctionnalités utilisateurs, il faut intégrer des scenarios de test E2E. Des tests unitaires avec Jest peuvent les compléter / remplacer en fonction.
  2. Si notre Pull Request a pour but de corriger un bug, cela signifie qu’il nous manque un cas de test. On doit donc essayer de rajouter un test E2E ou à défaut un test unitaire.

C’est en écrivant ces lignes que je me dis que ces principes pourraient très bien faire l’objet d’une automatisation. 🤣

Le projet reste, les fonctionnalités non

“La seconde évolution d’une fonctionnalité est très souvent sa suppression.”

Par principe, nous souhaitons faire en sorte que chaque nouvelle fonctionnalité de l’application ne base pas son activation sur le simple fait d’être dans la codebase. Classiquement, le cycle de vie d’une “feature” dans un projet peut être le suivant (dans un Github Flow):

Pour simplifier certaines étapes, il a été mis en place du feature flipping sur le projet.

Comment ça marche ?

Dans notre config il y a une map clé/valeur qui liste toutes les fonctionnalités de l’application associées à leur statut d’activation.

const featureFlipping = {
  myAwesomeFeature: false,
  anotherOne: true,
};

Dans notre code, nous avons donc implémenté des traitements conditionnels qui disent “Si cette feature est activée alors…”. Cela peut changer le rendu d’un composant, changer l’implémentation d’une action Redux ou bien désactiver une route de notre react-router.

Mais à quoi ça sert ?

Pour vous donner un exemple plus concret, entre 2018 et 2020 nous avons complètement refondu l’interface de l’application. Cette évolution graphique n’était qu’une clé de featureFlipping. La refonte graphique n’a donc pas été la remise à zéro du projet, on continue encore aujourd’hui de vivre avec les deux versions (tant que la bascule de tous nos clients n’est pas terminée).

screenshot comparatif v4 / v5 sur 6play

L’A/B testing

Grâce au super travail des équipes backend et data, on a pu même étendre l’usage du feature flipping en rendant cette configuration modifiable pour des sous groupes d’utilisateurs.

Cela permet de déployer des nouvelles fonctionnalités sur une portion plus réduite des utilisateurs afin de comparer nos KPI.

Prise de décision, amélioration des performances techniques ou produit, expérimentations, les possibilités sont nombreuses et nous les exploitons de plus en plus.

Le futur flipping

Sur une idée originale de Florent Lepretre.

Nous avions régulièrement le besoin d’activer des feature à des heures très trop matinales dans le futur. Pour cela nous devions être connecté à une heure précise sur notre poste pour modifier la configuration à chaud.

Afin d’éviter d’oublier de le faire, ou de le faire en retard, nous avons fait en sorte qu’une clé de configuration puisse être activée à partir d’une certaine date. Pour cela, nous avons fait évoluer notre selector redux qui indiquait si une feature était activée pour qu’il puisse gérer des formats de date et les comparer à l’heure courante.

const featureFlipping = {
  myAwesomeFeature: {
    offpubDatetime: "2021-07-12 20:30:00",
    onpubDatetime: "2021-07-12 19:30:00",
  },
};

De nombreux cafés ☕️ à 9h ont été sauvés grâce au futur flipping

Monitorer, Mesurer, Alerter

Pour maintenir un projet aussi longtemps que l’application web de bedrock, des tests, de la documentation et de la rigueur ne suffisent pas. Il faut également de la visibilité sur ce qui marche en production.

“Comment sais-tu que l’application que tu as en production en ce moment même fonctionne comme prévu ?”

On part du principe qu’aucune fonctionnalité ne marche tant qu’elle n’est pas monitorée. Aujourd’hui le monitoring à Bedrock coté Frontend se matérialise par différents outils et différentes stacks. Je pourrais vous citer NewRelic, un Statsd, une stack ELK ou bien encore Youbora pour la vidéo.

Pour vous donner un exemple, à chaque fois qu’un utilisateur commence une session de navigation on envoie un Hit de monitoring anonyme pour incrémenter un compteur dans Statsd. On a alors plus qu’à définir un dashboard qui affiche dans un graphique l’évolution de ce nombre. Si on observe une variation trop importante, cela peut nous permettre de détecter un incident.

exemple de dashboard de suivi

Le monitoring nous offre aussi des solutions pour comprendre et analyser un bug qui s’est produit dans le passé. Comprendre un incident, l’expliquer, en trouver sa root cause sont les possibilités qui s’offrent à vous si vous monitorez votre application. Le monitoring peut également permettre de mieux communiquer avec les clients sur les impacts d’un incident et également d’estimer le nombre d’utilisateurs impactés.

Avec la multiplication de nos clients, bien monitorer nos plateformes n’est plus suffisant. Trop de données, trop de dashboards à surveiller, il devient très facile de louper quelque chose. Nous avons donc commencé à compléter notre suivi des mesures par de l’alerting automatique. Une fois que les mesures nous apportent suffisamment de confiance, on peut facilement mettre en place des alertes qui vont nous prévenir en cas de valeur incohérente.

Nous essayons cependant de toujours déclencher des alertes uniquement quand celle-ci est actionnable. Dans d’autres termes, si une alerte sonne, nous avons quelque chose à faire. Faire sonner des alertes qui ne nécessitent aucune action immédiate humaine génèrent du bruit et de la perte de temps.

alerte générale

Limiter, surveiller et mettre à jour ses dépendances

Ce qui périme plus vite que votre ombre dans un projet web basé sur des technologies javascript, ce sont vos dépendances. L’écosystème évolue rapidement et vos dépendances peuvent vite se retrouver non maintenues, plus à la mode ou bien complètement refondues avec de gros breaking changes.

On essaye donc dans la mesure du possible de limiter nos dépendances et d’éviter d’en ajouter inutilement. Une dépendance, c’est souvent très facile à ajouter mais elle peut devenir un vrai casse-tête à enlever.

Les librairies de composants graphiques (exemple React bootstrap, Material Design) sont un bel exemple de dépendance que nous tenons à ne pas introduire. Elles peuvent faciliter l’intégration dans un premier temps mais celles-ci bloquent souvent la version de votre librairie de composant par la suite. Vous ne voulez pas figer la version de React dans votre application pour deux composants de formulaires.

La surveillance fait aussi partie de nos routines de gestion de nos dépendances. Depuis l’ajout du signalement de failles de sécurité dans un package NPM, il est possible de savoir si un projet intègre une dépendance qui contient une faille de sécurité connue par une simple commande. Nous avons donc des jobs journaliers sur nos projets qui lancent la commande yarn audit afin de nous forcer à appliquer les correctifs.

La maintenance de dépendances est grandement facilité par notre stack de tests E2E qui sonnent direcement si la montée de version génère une regression.

Aujourd’hui, hors failles de sécurité, nous mettons à jour nos dépendances “quand on a le temps”, souvent en fin de sprint. Cela ne nous satisfait pas car certaines dépendances peuvent se retrouver oubliées. J’ai personnellement l’habitude d’utiliser des outils comme yarn outdated et Dependabot sur mes projets personels pour automatiser la mise à jour de mes dépendances.

Accepter sa dette technique

Un projet accumulera toujours de la dette technique. C’est un fait. Que ce soit de la dette volontaire ou involontaire, un projet qui résiste aux années va forcément accumuler de la dette. D’autant plus, si pendant toutes ces années vous continuez d’ajouter des fonctionnalités.

Depuis 2014, nos bonnes pratiques, nos façons de faire ont bien évolué. Parfois nous avons décidé ces changements mais parfois nous les avons subi (un exemple, l’arrivée des composants fonctionnels avec React et l’api des Hooks).

Notre projet n’est pas complètement “state of art” et on l’assume.

ça tiendra !

Nous essayons de prioriser nos sujets de refactoring sur les parties de l’application sur lequel on a le plus de souci, le plus de peine. On considère qu’une partie de l’application qui ne nous plaît pas mais sur laquelle on n’a pas besoin de travailler (apporter des évolutions) ne mérite pas qu’on la refactorise.

Je pourrais vous citer de nombreuses fonctionnalités de notre application qui n’ont pas évolué fonctionnellement depuis plusieurs années. Mais comme nous avons couvert ces fonctionnalités de tests E2E depuis le début, nous n’avons pas vraiment eu à y retoucher.

Comme dit plus haut, la prochaine évolution d’une feature de code est parfois sa désactivation. Alors pourquoi passer son temps à ré-écrire toute l’application ?

Pour résumer

Les bonnes pratiques présentées ici restent bien évidemment subjectives et ne s’appliqueront pas parfaitement/directement dans vos contextes. Je suis cependant convaincu qu’elles peuvent probablement vous aider à identifier ce qui peut faire passer votre projet de fun à périmé. À Bedrock nous avons mis en place d’autres pratiques que je n’ai pas listées ici mais ce sera l’occasion de faire un nouvel article un jour.

Enfin, si vous souhaitez que je revienne plus en détail sur certains chapitres présentés ici, n’hésitez pas à me le dire, je pourrais essayer d’y dédier un article spécifique.


Previous Post
Best practices for Web application maintenance
Next Post
Comment mettre en place des tests bout en bout avec WebdriverIo ?