Toda sessão de depuração começa no mesmo lugar: você abre o DevTools. O console tem o erro, a aba de rede tem a requisição que falhou e, em um minuto, você sabe o que deu errado.
O problema é que o DevTools está aberto na sua máquina, e o bug está acontecendo na do usuário.
Este é um guia para encurtar essa distância — instrumentar uma página para que, quando algo quebrar no navegador de um usuário, você receba os mesmos três painéis que teria aberto por conta própria. Ele cobre o que interceptar, em que ordem, e as armadilhas específicas que transformam uma implementação ingênua numa fonte de novos bugs.
Comece pelo problema da ordem
Quase todo guia sobre capturar a saída do console começa pelo monkey patch. É o lugar errado para começar, porque a parte difícil não é como fazer o patch — é quando.
A sua instrumentação só vê o que acontece depois que ela carrega. Tudo o que foi registrado antes disso se perdeu. E os erros que mais interessam costumam ser justamente os do início: um fetch de configuração que falhou, um script de terceiros que lançou um erro na inicialização, um hydration mismatch. Quando o seu bundle de relatório de bugs termina de baixar, de ser parseado e de inicializar, a parte interessante da linha do tempo já é história.
Há duas formas de lidar com isso, e você precisa das duas.
Carregue a camada de captura primeiro. Coloque-a antes do bundle da aplicação, de forma síncrona, no <head>. Mantenha-a pequena — é esse o argumento a favor de um stub de captura que instala os hooks e mais nada, com a interface mais pesada carregando sob demanda depois.
Guarde em buffer antes de ter onde colocar as coisas. Os hooks devem escrever num buffer circular em memória desde o primeiro milissegundo, muito antes de alguém clicar em «Reportar um bug». Quando um relatório finalmente é aberto, o buffer já está cheio de histórico. É isso que permite capturar logs de antes de o widget ser aberto — o widget nunca foi o responsável pela captura.
Fazendo patch do console sem quebrá-lo
A mecânica é simples:
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);
};
}
Na prática, quatro coisas dão errado aqui.
Você quebra a atribuição de origem do DevTools. Agora o console mostra cada linha como vinda do seu wrapper, e não do ponto de chamada. Chrome e Firefox respeitam um comentário //# sourceURL e regras de blackboxing, mas a correção confiável é capturar um stack trace no momento da chamada e guardar o frame de quem chamou junto com a entrada. Você vai querer esse frame no relatório de qualquer jeito.
Você entra em recursão. Se serialize lança um erro, o seu handler de erros escreve no log e o log está com patch, você tem um loop infinito que trava a aba de vez. Proteja-se com uma flag de reentrância e faça o corpo inteiro do hook virar um no-op se qualquer coisa dentro dele falhar. Instrumentação que derruba a página é pior do que nenhuma instrumentação.
Você mantém objetos vivos. Colocar os argumentos crus num buffer mantém uma referência forte a todo objeto já registrado — nós do DOM, corpos de resposta, árvores inteiras de componentes. Numa single-page app que fica aberta por muito tempo, isso é um vazamento de memória que cresce enquanto a aba estiver aberta. Serialize na hora para uma representação em string de tamanho limitado e solte a referência.
Você não é o único fazendo patch. Scripts de analytics, rastreadores de erros e as dev tools dos frameworks também fazem patch do console. Se você captura o original no carregamento e alguém faz patch depois de você, o wrapper dele fica acima do seu e você continua vendo tudo. Se ele fez o patch antes, você vê a saída do wrapper dele. As duas situações funcionam; o que não funciona é supor que você está sozinho. Nunca restaure console[level] para o original no teardown — você vai atropelar quem o envolveu depois de você. Restaure só se você ainda estiver no topo da cadeia.
Serialização é a maior parte do trabalho
JSON.stringify falha com referências circulares, descarta funções e undefined em silêncio, lança erro com BigInt e transforma objetos Error em {} — que é justamente o caso que mais importa.
Um serializador bom o bastante para relatórios de bug precisa lidar com: referências circulares (troque por um marcador), instâncias de Error (name, message e stack, explicitamente), nós do DOM (tag, id, classes — não a subárvore), Map e Set, e limites de profundidade e de tamanho para que um objeto enorme não consuma o buffer inteiro. Trunque strings longas com um marcador explícito, não em silêncio. Uma linha de log que diz "[truncated at 2000 chars]" informa algo ao desenvolvedor; uma linha que simplesmente para no meio, não.
Erros que você nunca registrou
A maioria dos erros em produção nunca passa pelo console.error no seu código. Eles são lançados, e outra coisa os trata. Dois hooks globais cobrem essa lacuna:
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) });
});
Use a fase de captura para error, assim você vê os eventos mesmo quando algo mais abaixo interrompe a propagação. E repare que o evento error também dispara quando um recurso falha ao carregar — uma imagem quebrada ou um script que deu 404 —, caso em que o target é um elemento, e não um objeto de erro. Vale registrá-los à parte: um bundle ausente explica muitos relatórios de «a página está simplesmente em branco».
Capturando a rede
Duas APIs para envolver e uma para usar como rede de segurança.
fetch é a mais direta. Envolva-a, registre método, URL e horário de início e, quando a promise for concluída, o status e a duração. Registre também as rejeições — uma falha de rede gera uma promise rejeitada, não uma resposta, e «a requisição nunca terminou» é um relatório bem diferente de «a requisição retornou 500».
XMLHttpRequest ainda importa. Muitas dependências o usam internamente, e se você envolver só o fetch vai receber relatórios em que a requisição que falhou é invisível. Faça patch de open para guardar o método e a URL na instância, depois faça patch de send e escute loadend.
PerformanceObserver com o tipo de entrada resource é a rede de segurança. Ele vê todas as requisições que o navegador fez — incluindo imagens, folhas de estilo, fontes, sendBeacon e qualquer coisa disparada por um script que pegou uma referência ao fetch antes do seu patch. Ele dá o tempo e o tamanho transferido, mas não os códigos de status nem os corpos, o que faz dele um complemento aos wrappers, e não um substituto.
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 });
O buffered: true é a parte importante: ele entrega as entradas de antes de o observer existir, o que recupera parte do início da linha do tempo que você teria perdido.
Os corpos são a parte perigosa
Os corpos de requisição e de resposta são a coisa mais útil que você pode anexar a um relatório de bug, e a que tem mais chance de transformar um relatório de bug num incidente de segurança. Corpos contêm tokens de autenticação, dados pessoais, links de redefinição de senha e dados de pagamento.
O padrão defensável é desligado, com ativação explícita por projeto. No BugDump, essa ativação é captureNetworkBodies, e de propósito ela não vem ligada por padrão, mesmo deixando os relatórios melhores. Se você for construir isso por conta própria, tome a mesma decisão.
Com os corpos ligados, também há armadilhas mecânicas. O corpo de um Response é um stream que só pode ser consumido uma vez — você precisa chamar clone() antes de ler, ou o próprio await res.json() da aplicação vai lançar erro por causa de um corpo já consumido. Limite o tamanho que você lê. E leia de forma assíncrona, para não bloquear o tratamento da resposta pela própria aplicação.
O mascaramento deve acontecer no navegador
A propriedade de privacidade mais forte que você tem é a de que os dados sensíveis nunca saem da máquina. Isso significa filtrar no momento da captura, no cliente, e não no servidor depois do upload.
Dois filtros cobrem a maioria das necessidades. Um filtro de console descarta entradas por nível ou por padrão — útil para aquela biblioteca que registra um token de sessão em debug a cada render. Um filtro de rede descarta ou reescreve entradas por URL, e é assim que você exclui o seu endpoint de autenticação, o seu provedor de pagamento e os beacons de analytics que, de outra forma, seriam 90% do log.
Bugdump.init({
apiKey: 'your-api-key',
networkFilter: (req) => !req.url.includes('/auth/'),
consoleFilter: (entry) => entry.level !== 'debug',
});
Aqui, predicados ganham de configuração. Qualquer allowlist baseada em regex que você desenhar vai estar errada para alguém; uma função não está errada para ninguém.
Tamanho, e saber quando parar
Um relatório de bug não é um pipeline de observabilidade. Você está capturando alguns minutos de uma sessão, não uma frota.
Buffers circulares com limites rígidos — na casa de algumas centenas de entradas de console e algumas centenas de entradas de rede — quase sempre bastam, porque a janela útil são os segundos antes de o usuário desistir. Guarde as entradas mais recentes e descarte do início. Registre quantas você descartou, para que o relatório possa dizer «mostrando as últimas 200 de 4.318 entradas», em vez de sugerir em silêncio que o log era curto.
A mesma lógica vale para o que você não constrói. Resolução de source maps, configuração de nível de log por módulo, amostragem, políticas de retenção, agregação — esses são problemas de um produto de monitoramento de erros. Um relatório de bug tem um autor que está ali, contando o que estava tentando fazer. Esse contexto faz a maior parte do trabalho que a agregação faz em outros lugares.
A recomendação honesta
Tudo isso dá para construir em algumas centenas de linhas. Vale entender mesmo que você não vá construir, porque essas são as propriedades a verificar em qualquer ferramenta que você usar: ela é instalada antes do bundle da aplicação? Faz buffer desde o carregamento? Sobrevive a um patch feito por outra pessoa? Os corpos vêm desligados por padrão? Dá para mascarar os dados antes do upload?
O BugDump faz tudo isso com uma tag de script, renderiza a interface dentro de um Shadow DOM para que nada vaze em nenhuma direção e anexa os logs de console e de rede capturados ao que o usuário digitar na caixa do relatório. O plano Grátis cobre 500 relatórios por projeto, sem cartão.
Mas a camada de captura é a parte que importa, seja quem for que a escreva. O resto da ferramenta é só um jeito de fazer isso chegar até você.
Leia também
Veja os erros de console que seus usuários veem, sem DevTools — como a captura no cliente funciona, com buffers circulares, hooks de rede e filtros, e como ligá-la com uma tag de script.
Rastreamento de erros JavaScript em apps web pequenos: o que você precisa de verdade — pegar o que quebra, agrupar as repetições, ler o stack no seu próprio código e guardar o minuto antes da falha.
Três quartos do meu SDK eram código que quase ninguém executava — como medi os 450 KB que o widget mandava para cada visitante e o que fiz para cortá-los.