Ça marche chez moi – guide pratique des bugs non reproductibles

QA

Un utilisateur signale que la page plante. Vous suivez ses étapes à la lettre. Tout fonctionne. Vous fermez le ticket avec la mention impossible à reproduire, et trois semaines plus tard, quatre autres personnes signalent la même chose.

« Ça marche chez moi » passe pour une blague, mais c'est un constat précis et utile : le bug dépend d'une différence entre votre environnement et le sien. Une question sans réponse devient une question qu'on peut traiter. On ne se demande plus « pourquoi ce bug est-il si bizarre », mais « qu'est-ce qui diffère entre ces deux machines ? »

Les différences possibles ne sont pas si nombreuses. Dans les applications web, elles se rangent presque toutes dans huit catégories, et chacune laisse une empreinte reconnaissable.

1. L'état

La cause la plus fréquente, de loin. Vous reproduisez à partir d'une page fraîchement chargée. L'utilisateur, lui, a cet onglet ouvert depuis deux heures.

Une application monopage ouverte depuis longtemps accumule de l'état : des entrées périmées dans un cache côté client, un websocket qui s'est reconnecté avec une autre session, un store modifié par un chemin de code visité il y a une heure, un jeton d'accès rafraîchi en cours de route. Votre page fraîchement chargée n'a rien de cet historique.

Empreinte : ça marche après un rafraîchissement. Si la personne dit « ça se règle quand je rafraîchis », vous êtes presque sûrement dans ce cas, et la reproduction dont vous avez besoin, ce ne sont pas ses étapes, mais ses étapes après vingt minutes d'une utilisation sans rapport.

Comment le repérer : la liste ordonnée de ce que l'utilisateur a réellement fait pendant la session, et non de ce qu'il a jugé pertinent. C'est ce que donne un journal d'actions et ce qu'un champ « étapes de reproduction » ne donnera jamais, parce que l'étape décisive est justement celle qu'il n'a pas pensé à mentionner.

2. Le timing

Sur votre machine, tout est rapide. API locale, cache chaud, disque rapide. Sur un téléphone dans un train, une requête qui vous prend 40 ms en prend 4 secondes, et une race condition qui ne perd jamais chez vous perd à coup sûr chez lui.

Empreinte : c'est intermittent. « Ça arrive peut-être une fois sur cinq. » Intermittent veut presque toujours dire timing : les deux opérations en concurrence ont des durées assez proches pour que la variance ordinaire change le gagnant.

Comment le repérer : les timings réseau. Regardez ce qui était en cours en même temps et combien de temps chaque requête a pris. Le cas classique, ce sont deux requêtes lancées ensemble alors que le code supposait que la première finirait la première.

Comment le reproduire : ralentissez. La limitation réseau et la limitation CPU des DevTools transforment un grand nombre de bugs « impossibles à reproduire » en bugs « qui se reproduisent à chaque fois ». Ce sont les cinq minutes les plus rentables de tout cet article.

3. L'environnement

Le navigateur, sa version, l'OS et, le plus sous-estimé, la taille du viewport.

Vous testez à 1440 px sur Chrome desktop. L'utilisateur est à 390 px sur Safari iOS, ou à 1280 px avec un zoom du navigateur à 125 %, ce qui n'est pas la même chose que 1024 px, et les mises en page cassent justement sur ces écarts. Safari, en particulier, diverge encore sur l'analyse des dates, les backdrop filters, le comportement du défilement et la façon dont il traite le stockage en navigation privée.

Empreinte : constant chez la personne qui signale le bug, jamais chez vous. Se reproduit 100 % du temps sur sa machine.

Comment le repérer : le navigateur, l'OS et le viewport, enregistrés automatiquement avec le rapport. C'est la catégorie où l'écart entre « trivial à corriger une fois connu » et « impossible à deviner » est le plus grand, et c'est justement pour ça que demander sa version de navigateur à un utilisateur gaspille un aller-retour avec le support.

4. L'identité et les permissions

Vous êtes connecté en admin, sur votre propre compte, dans votre propre organisation. L'utilisateur est membre en lecture seule d'une équipe de quarante personnes, sur un forfait où un feature flag est désactivé.

Les bugs qui dépendent du rôle passent systématiquement entre les mailles, parce que les développeurs ne parcourent presque jamais leur propre produit avec le compte le moins privilégié.

Empreinte : se reproduit pour une catégorie d'utilisateurs et pas pour une autre. Souvent signalé en même temps par plusieurs personnes qui partagent un rôle, ce qui ressemble à une régression soudaine alors que c'est cassé depuis des mois.

Comment le repérer : joignez au rapport le rôle de l'utilisateur, son forfait et l'état des feature flags. Tout SDK correct accepte votre propre contexte (dans BugDump, c'est identify() pour savoir qui est l'utilisateur et setContext() pour un état quelconque), et le rôle et le forfait sont les deux champs à joindre avant tous les autres.

5. Les données

Son compte contient un enregistrement avec un emoji dans le nom, ou un null là où vous attendiez une chaîne, ou 4 000 lignes là où vos fixtures en ont 12, ou une devise sans décimales, ou une date en 1970 parce qu'un import a mal tourné.

Empreinte : se reproduit sur un enregistrement précis et nulle part ailleurs. La personne dira souvent « il n'y a que ce projet-là qui fait ça », et c'est tout le diagnostic qu'on vous tend.

Comment le repérer : l'identifiant de l'enregistrement et la réponse d'API en échec. Un corps de réponse qui contient la valeur fautive règle cette catégorie instantanément. C'est en grande partie pour ça que les corps de réponse sont si utiles, et pour ça qu'il faut les manier avec soin : le même champ qui contient la date mal formée contient aussi tout le reste sur l'utilisateur.

6. Les extensions et les scripts injectés

Les bloqueurs de pub bloquent votre endpoint d'analytics, et votre code ne gérait pas l'échec de cette requête. Les gestionnaires de mots de passe injectent du DOM dans votre formulaire et cassent la gestion des événements. Les extensions de traduction réécrivent les nœuds texte et font voler en éclats la réconciliation de React. Les proxys d'entreprise réécrivent les réponses.

Empreinte : personne n'arrive à le reproduire, pas même d'autres utilisateurs, et la console montre une requête échouée sans statut, ou une erreur DOM qui pointe vers un élément que vous n'avez jamais rendu.

Comment le repérer : les requêtes échouées sans réponse sont le signe révélateur. Tout comme une relecture de session qui montre un DOM que votre application ne produit pas.

7. Le réseau lui-même

Portails captifs, interception TLS en entreprise, pannes DNS, un edge de CDN qui sert un bundle périmé à une région, un déploiement en cours où le HTML est neuf et le chunk qu'il référence a déjà disparu.

Empreinte : les rapports se regroupent dans l'espace ou dans le temps. Plusieurs rapports dans un court intervalle, ou tous du même pays.

Comment le repérer : les horodatages et l'URL de la ressource en échec. Une ChunkLoadError dans la console, c'est presque par définition cette catégorie, et cela veut dire que votre stratégie de déploiement supprime les anciens chunks avant que les anciens onglets en aient fini avec eux.

8. L'heure

Fuseaux horaires, changements d'heure, passages d'un mois à l'autre, formats de date locaux, et la machine dont l'horloge a onze minutes d'écart, assez pour qu'un jeton paraisse expiré dès son arrivée.

Empreinte : se reproduit seulement à certaines heures, ou seulement pour les utilisateurs de certaines régions, ou seulement vers la fin du mois.

Comment le repérer : le décalage horaire et l'heure locale de la personne, à côté de l'horodatage du serveur. Ça ne coûte presque rien à enregistrer, et ça tranche cette catégorie au premier coup d'œil.

La méthode

Face à un rapport que vous n'arrivez pas à reproduire, le bon réflexe n'est pas de s'acharner. C'est de le ranger dans l'une des huit catégories ci-dessus, et chacune a une question qui l'identifie vite :

  • Un rafraîchissement règle-t-il le problème ? → état

  • Est-ce intermittent ? → timing

  • Est-ce constant à 100 % chez l'utilisateur ? → environnement

  • Partage-t-il un rôle ou un forfait avec les autres personnes qui l'ont signalé ? → identité

  • Est-ce un enregistrement précis ? → données

  • Une requête a-t-elle échoué sans statut ? → extensions ou réseau

  • Est-ce regroupé dans le temps ou dans l'espace ? → réseau

  • Seulement à certaines heures ? → heure

Remarquez que chacune de ces questions trouve sa réponse dans des données, pas dans une conversation. C'est la vraie raison pour laquelle « impossible à reproduire » est si courant : les preuves qui rangeraient le bug dans sa catégorie se trouvaient dans le navigateur de l'utilisateur au moment où il a cassé, et personne ne les a capturées.

Capturez la différence, pas la description

Cela vaut la peine d'en faire une méthode, parce que les bugs non reproductibles ne sont pas de rares cas limites. Ce sont ceux qui restent ouverts. Les bugs reproductibles sont corrigés dans la semaine. Le backlog est fait des autres, et les autres sont presque entièrement un problème de données.

Un rapport qui arrive avec le navigateur, l'OS, le viewport, le fuseau horaire, le rôle de l'utilisateur, l'historique de session, la console et le journal réseau n'oblige personne à deviner une catégorie : la différence entre sa machine et la vôtre est écrite dans le rapport. C'est ce que BugDump capture automatiquement à partir d'une seule balise script, si bien que les huit questions ci-dessus ont leur réponse avant que quiconque ait à en poser une.

La chose la plus rare et la plus précieuse de cette liste, c'est la deuxième personne qui signale le bug. Un rapport non reproductible est un mystère. Trois rapports, avec trois environnements différents joints, forment une expérience contrôlée : les variables qu'ils partagent sont la cause, les autres sont du bruit. Regrouper les rapports en double sur une seule tâche n'est pas du rangement. Pour les bugs que vous n'arrivez pas à reproduire, c'est précisément le diagnostic.

Pour aller plus loin