Toute session de débogage commence au même endroit : vous ouvrez les DevTools. La console affiche l'erreur, l'onglet réseau montre la requête en échec, et en une minute vous savez ce qui a mal tourné.
Le problème, c'est que les DevTools sont ouverts sur votre machine, et que le bug se produit sur celle de l'utilisateur.
Ce guide sert à combler cette distance : instrumenter une page pour que, quand quelque chose casse dans le navigateur d'un utilisateur, vous obteniez les trois panneaux que vous auriez ouverts vous-même. Il couvre ce qu'il faut patcher, dans quel ordre, et les pièges précis qui transforment une implémentation naïve en source de nouveaux bugs.
Commencez par l'ordre de chargement
Presque tous les guides sur la capture de la console commencent par le monkey patch. Ce n'est pas le bon point de départ, parce que la difficulté n'est pas de savoir comment patcher, mais quand.
Votre instrumentation ne voit que ce qui se passe après son chargement. Tout ce qui a été journalisé avant est perdu. Or les erreurs qui vous intéressent le plus arrivent souvent tôt : un chargement de configuration en échec, un script tiers qui a levé une exception à l'initialisation, une incohérence d'hydratation. Le temps que votre bundle de rapport de bug soit téléchargé, analysé et initialisé, la partie intéressante de la chronologie appartient déjà au passé.
Il y a deux façons de régler ça, et il vous faut les deux.
Chargez la couche de capture en premier. Placez-la avant le bundle de votre application, en synchrone, dans <head>. Gardez-la petite : c'est tout l'intérêt d'un stub de capture qui installe les hooks et rien d'autre, l'interface plus lourde se chargeant ensuite à la demande.
Bufferisez avant même d'avoir où envoyer les données. Les hooks doivent écrire dans un buffer circulaire en mémoire dès la première milliseconde, bien avant que quiconque clique sur « signaler un bug ». Quand un rapport finit par être ouvert, le buffer est déjà rempli d'historique. C'est ce qui permet de capturer des logs antérieurs à l'ouverture du widget : ce n'est jamais le widget qui capturait.
Patcher la console sans la casser
La mécanique est simple :
const BUFFER = [];
const LEVELS = ['log', 'info', 'warn', 'error', 'debug'];
for (const level of LEVELS) {
const original = console[level].bind(console);
console[level] = (...args) => {
BUFFER.push({ level, ts: Date.now(), args: serialize(args) });
original(...args);
};
}
En pratique, quatre choses tournent mal.
Vous cassez l'attribution des sources dans les DevTools. La console indique désormais que chaque ligne vient de votre wrapper et non du point d'appel. Chrome et Firefox respectent tous deux un commentaire //# sourceURL et les règles de blackboxing, mais la solution fiable est de capturer une stack trace au moment de l'appel et d'enregistrer la frame de l'appelant avec l'entrée. Vous voudrez de toute façon cette frame dans le rapport.
Votre code s'appelle lui-même en boucle. Si serialize lève une exception, que votre gestionnaire d'erreurs journalise et que votre journalisation est patchée, vous obtenez une boucle infinie qui bloque l'onglet. Protégez-vous avec un drapeau de réentrance, et faites en sorte que tout le corps du hook ne fasse rien si quoi que ce soit échoue à l'intérieur. Une instrumentation qui fait planter la page est pire que pas d'instrumentation du tout.
Vous gardez des objets en vie. Pousser les arguments bruts dans un buffer conserve une référence forte vers chaque objet jamais journalisé : nœuds DOM, corps de réponse, arbres de composants entiers. Sur une application monopage qui reste ouverte longtemps, c'est une fuite mémoire qui grossit tant que l'onglet est ouvert. Sérialisez tout de suite vers une représentation textuelle bornée, et lâchez la référence.
Vous n'êtes pas le seul à patcher. Les scripts d'analytics, les outils de suivi d'erreurs et les outils de développement des frameworks patchent eux aussi console. Si vous capturez l'original au chargement et que quelqu'un patche après vous, son wrapper se place au-dessus du vôtre et vous voyez toujours tout. S'il a patché avant, vous voyez la sortie de son wrapper. Les deux fonctionnent ; ce qui ne fonctionne pas, c'est de supposer que vous êtes seul. Ne remettez jamais l'original dans console[level] lors du nettoyage : vous écraseriez celui qui l'a enveloppé après vous. Ne restaurez que si vous êtes toujours en haut de la chaîne.
La sérialisation, c'est l'essentiel du travail
JSON.stringify échoue sur les références circulaires, ignore silencieusement les fonctions et undefined, lève une exception sur BigInt et transforme les objets Error en {}, c'est-à-dire précisément le cas qui vous intéresse le plus.
Un sérialiseur assez bon pour des rapports de bug doit gérer : les références circulaires (remplacées par un marqueur), les instances Error (nom, message et stack, explicitement), les nœuds DOM (balise, id, classes, mais pas le sous-arbre), Map et Set, ainsi que des limites de profondeur et de longueur, pour qu'un seul objet énorme ne puisse pas occuper tout le buffer. Tronquez les chaînes longues avec un marqueur explicite plutôt qu'en silence. Une ligne de log qui affiche "[truncated at 2000 chars]" dit quelque chose au développeur ; une ligne qui s'arrête net ne dit rien.
Les erreurs que vous n'avez jamais journalisées
La plupart des erreurs en production ne passent jamais par console.error dans votre code. Elles sont levées, et c'est autre chose qui les traite. Deux hooks globaux couvrent ce trou :
window.addEventListener('error', (e) => {
record({ kind: 'uncaught', message: e.message, filename: e.filename,
line: e.lineno, col: e.colno, stack: e.error?.stack });
}, true);
window.addEventListener('unhandledrejection', (e) => {
record({ kind: 'unhandled-rejection', reason: serialize(e.reason) });
});
Utilisez la phase de capture pour error, afin de voir les événements même quand quelque chose en aval stoppe la propagation. Notez aussi que l'événement error se déclenche pour les ressources qui n'ont pas pu se charger (une image cassée, un script en 404) : la cible est alors un élément, pas un objet d'erreur. Ces cas méritent d'être enregistrés à part : un bundle manquant explique un grand nombre de rapports du type « la page reste blanche ».
Capturer le réseau
Deux API à envelopper, et une troisième en filet de sécurité.
fetch est la plus simple. Enveloppez-la, enregistrez la méthode, l'URL, l'heure de début, puis le statut et la durée quand la promesse se résout. Enregistrez aussi les rejets : une panne réseau produit une promesse rejetée, pas une réponse, et « la requête n'a jamais abouti » est un rapport bien différent de « la requête a renvoyé 500 ».
XMLHttpRequest compte toujours. Beaucoup de dépendances l'utilisent en interne, et si vous n'enveloppez que fetch, vous recevrez des rapports où la requête en échec est invisible. Patchez open pour stocker la méthode et l'URL sur l'instance, puis patchez send et écoutez loadend.
PerformanceObserver sur le type d'entrée resource sert de filet de sécurité. Il voit toutes les requêtes faites par le navigateur, y compris les images, les feuilles de style, les polices, sendBeacon et tout ce qu'émet un script qui a récupéré une référence à fetch avant votre patch. Il fournit le timing et la taille transférée, mais ni code de statut ni corps : c'est un complément des wrappers, pas un remplaçant.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
record({ url: entry.name, type: entry.initiatorType,
duration: entry.duration, size: entry.transferSize });
}
}).observe({ type: 'resource', buffered: true });
Ce buffered: true est la partie importante : il vous livre les entrées antérieures à la création de l'observer, ce qui récupère une partie du début de la chronologie que vous auriez perdue autrement.
Les corps, c'est la partie dangereuse
Les corps de requête et de réponse sont la chose la plus utile que vous puissiez joindre à un rapport de bug, et celle qui risque le plus de transformer un rapport de bug en incident de sécurité. Ils contiennent des jetons d'authentification, des données personnelles, des liens de réinitialisation de mot de passe et des informations de paiement.
Le réglage par défaut défendable, c'est désactivé, avec une activation explicite par projet. Dans BugDump, cette activation s'appelle captureNetworkBodies, et elle n'est volontairement pas active par défaut, même si elle améliore les rapports. Si vous construisez cela vous-même, faites le même choix.
Quand les corps sont activés, il y a aussi des pièges mécaniques. Le corps d'une Response est un flux qui ne se lit qu'une fois : vous devez appeler clone() avant de le lire, sinon le await res.json() de l'application elle-même lèvera une exception sur un corps déjà consommé. Plafonnez la taille lue. Et lisez de façon asynchrone, pour ne pas bloquer le traitement de la réponse par l'application.
Le masquage se fait dans le navigateur
La meilleure garantie de confidentialité dont vous disposez, c'est que les données sensibles ne quittent jamais la machine. Cela veut dire filtrer au moment de la capture, côté client, et non sur le serveur après l'envoi.
Deux filtres couvrent la plupart des besoins. Un filtre de console écarte des entrées selon leur niveau ou un motif, ce qui sert pour la bibliothèque qui journalise un jeton de session en debug à chaque rendu. Un filtre réseau écarte ou réécrit des entrées selon l'URL : c'est ainsi que vous excluez votre endpoint d'authentification, votre prestataire de paiement et les beacons d'analytics qui, sinon, feraient 90 % du journal.
Bugdump.init({
apiKey: 'your-api-key',
networkFilter: (req) => !req.url.includes('/auth/'),
consoleFilter: (entry) => entry.level !== 'debug',
});
Ici, les prédicats valent mieux que la configuration. Toute liste d'autorisation à base de regex que vous concevrez sera fausse pour quelqu'un ; une fonction ne l'est pour personne.
Dimensionner, et savoir s'arrêter
Un rapport de bug n'est pas un pipeline d'observabilité. Vous capturez quelques minutes d'une session, pas une flotte de machines.
Des buffers circulaires avec des plafonds stricts, de l'ordre de quelques centaines d'entrées de console et de quelques centaines d'entrées réseau, suffisent presque toujours, parce que la fenêtre utile, ce sont les secondes qui précèdent le moment où l'utilisateur a abandonné. Gardez les entrées les plus récentes et supprimez les plus anciennes. Notez combien vous en avez supprimé, pour que le rapport puisse afficher « 200 dernières entrées sur 4 318 » au lieu de laisser entendre que le journal était court.
La même logique s'applique à ce qu'il vaut mieux ne pas construire. La résolution des source maps, la configuration des niveaux de log par module, l'échantillonnage, les politiques de rétention, l'agrégation : ce sont les problèmes d'un produit de monitoring d'erreurs. Un rapport de bug a un auteur, qui est juste là et vous dit ce qu'il essayait de faire. Ce contexte fait l'essentiel du travail que l'agrégation fait ailleurs.
Ce que je recommande
Tout ce qui précède se construit en quelques centaines de lignes. Même si vous ne le construisez pas, cela vaut la peine de le comprendre, parce que ce sont les propriétés à vérifier dans l'outil que vous utilisez : s'installe-t-il avant le bundle de l'application ? Bufferise-t-il dès le chargement ? Survit-il à un patch posé par quelqu'un d'autre ? Les corps sont-ils désactivés par défaut ? Pouvez-vous masquer les données avant l'envoi ?
BugDump fait tout cela derrière une seule balise script, affiche son interface dans un Shadow DOM pour que rien ne fuie dans un sens ni dans l'autre, et joint les logs de console et de réseau capturés à ce que l'utilisateur tape dans le champ du rapport. L'offre gratuite couvre 500 rapports par projet, sans carte bancaire.
Mais c'est la couche de capture qui compte, peu importe qui l'écrit. Le reste de l'outil sert seulement à vous faire parvenir ce qu'elle a capturé.
Pour aller plus loin
Voir les erreurs de console de vos utilisateurs, sans DevTools — buffers circulaires, hooks réseau et filtrage, et comment obtenir tout cela avec une seule balise script.
Anatomie d'un rapport de bug – ce que BugDump capture et pourquoi — chaque signal que collecte le widget BugDump, à quoi il sert et lesquels résolvent vraiment des bugs.
La relecture de session sans la surveillance — une relecture limitée à un rapport, que l'utilisateur peut flouter et que vous coupez en une ligne.