Il y a une phrase que tout développeur a déjà entendue, ou prononcée lui-même :
“Je n’arrive pas à reproduire.”
En général, elle arrive après plusieurs tentatives, des logs ajoutés un peu partout, et une certaine frustration. Le bug existe, il est confirmé en production, parfois critique… mais en local, tout fonctionne.
On finit souvent par conclure que le bug est “intermittent”, ou pire, “impossible à reproduire”.
Dans la réalité, il ne l’est presque jamais.
Le problème n’est pas le bug
Un bug n’est pas difficile à reproduire. Il est difficile à reproduire dans les bonnes conditions.
Ce qui manque, ce n’est pas une reproduction mais une compréhension complète du contexte dans lequel il apparaît.
Le mythe de l’environnement identique
On part souvent du principe que :
- local = staging = production
Dans les faits, les différences sont nombreuses :
- version de PHP / Node
- configuration du runtime
- latence réseau
- volumétrie de données
- comportement des dépendances externes
Un exemple simple : une requête externe qui met 50ms en local peut en prendre 400ms en production. Cela suffit à révéler des problèmes invisibles ailleurs.
L’état caché
Beaucoup de bugs ne dépendent pas uniquement du code exécuté.
Ils dépendent de l’état du système au moment précis où le code s’exécute.
- cache déjà rempli ou non
- session utilisateur
- contenu de la base
- feature flags
- ordre d’exécution des requêtes
Un cas classique :
if (!$cache->has($key)) {
$value = compute();
$cache->set($key, $value);
}
En local, cela fonctionne parfaitement.
En production, sous charge, deux requêtes peuvent passer simultanément dans le has() avant que le set() ne soit exécuté.
Résultat :
- double computation
- état incohérent
- voire corruption de données
Impossible à reproduire… tant que l’on ne reproduit pas la concurrence.
Le facteur temps
Certains bugs n’apparaissent que dans des conditions temporelles très précises.
- race conditions
- timeouts
- expirations de cache
- synchronisation entre services
Un exemple typique :
$order = $repository->find($id);
if ($order->isPaid()) {
process($order);
}
En apparence, rien de problématique, mais si l’état "paid" change entre deux requêtes concurrentes, ou via un autre service, le comportement devient non déterministe.
Ce type de bug ne dépend pas uniquement du code, mais du moment exact où il s’exécute.
Le piège des logs classiques
Quand un bug survient, le réflexe est d’ajouter des logs.
logger()->info('processing order', ['id' => $id]);
C’est utile, mais insuffisant.
Le problème n’est pas le manque de logs mais le manque de contexte.
Sans :
- correlation id
- état complet
- séquence d’exécution
les logs restent des fragments difficiles à assembler.
Le problème d’observabilité
La plupart des systèmes sont mal observés, non pas par manque d’outils, mais par manque de bons signaux. Un log isolé ne suffit pas et une stacktrace non plus.
Ce qu’il faut, c’est pouvoir répondre à des questions simples :
- quelle requête a déclenché ce bug ?
- dans quel contexte utilisateur ?
- avec quelles données ?
- après quelles autres actions ?
Sans cela, chaque investigation repart de zéro.
Ce qui fonctionne réellement
Avec le temps, certaines pratiques font une différence nette.
1. Corrélation
Associer chaque requête à un identifiant unique :
$requestId = bin2hex(random_bytes(8));
Et le propager partout :
- logs
- appels externes
- messages async
2. Logs structurés
Plutôt que du texte :
logger()->info('processing order ' . $id);
Préférer :
logger()->info('processing_order', [
'order_id' => $id,
'user_id' => $userId,
'status' => $status,
]);
Cela permet :
- filtrage
- agrégation
- analyse
3. Tracing
Comprendre le parcours complet d’une requête :
- API → service → DB → queue → worker
Avec des outils type :
- OpenTelemetry
- Datadog APM
- AWS X-Ray
4. Reproduction contrôlée
Quand possible :
- rejouer une requête réelle
- utiliser les mêmes données
- simuler la concurrence
Sans cela, on ne reproduit qu’une approximation.
Ce que font les systèmes qui tiennent
La différence n’est pas dans la qualité du code, elle est dans la capacité à comprendre ce qui se passe réellement en production. Les systèmes robustes ne sont pas ceux sans bugs, ce sont ceux où les bugs sont compréhensibles.
Conclusion
Un bug n’est presque jamais “impossible à reproduire”. Il est simplement mal observé, mal contextualisé, ou dépendant de conditions que l’on ne reproduit pas.
TL;DR
- Un bug dépend d’un contexte, pas seulement d’un code
- Environnement, état et timing sont déterminants
- Les logs seuls ne suffisent pas
- L’observabilité est la clé
- Comprendre > reproduire