O bug que a gente debugou viajando no tempo
skail · 2026-07-29 · 7 min
TL;DR
- A cena — um probe que consulta um provedor externo falhou 15 de 16 tentativas em produção, 9min41s de ponta a ponta. O log dizia "Response status code does not indicate success" quinze vezes seguidas.
- O que a gente fez — em vez de grepar log, abrimos o trace, copiamos UM JSON do Monitor pro
launchSettings.jsone rodamosdotnet runcom o estado exato que a produção tinha. Isso é time travel debug em produção.- O que ele revelou — um
400 Bad Requestsendo retentado como se fosse falha passageira. A correção da política de retry é um arquivo; a capacidade de debug que expôs isso é a plataforma.
Um stakeholder me perguntou no chat: "o probe tá saudável?". Eu abri o Monitor e respondi em cinco segundos — sem abrir um único arquivo de log.
O que estava na tela: cinco execuções recentes do mesmo probe, todas concluídas, com a contagem de tentativas visível ao lado de cada uma — 2, 0, 3, 0, 1. Provedor instável, retentado sozinho, e o produto nunca ficou sabendo. E uma sexta execução ainda Running, com 14 tentativas acumuladas. Uma falha sendo absorvida ao vivo, legível num relance.
As imagens neste post são recreações fiéis da UI do Monitor / Debug Execution, com os identificadores trocados. O caso é uma execução de produção nossa (2026-07-18); a validação continua interna.
A execução durável grava cada tentativa como um evento enquanto a história acontece, não depois. A contagem de retry vem do histórico da própria execução, não de uma correlação de log que eu tive que montar. Foi por isso que eu consegui responder em cinco segundos. Agora eu queria a execução que estava travada.
O trace conta a história, não o log
Abrir aquela execução Running não me levou a um arquivo de log: me levou à árvore da tarefa. Raiz [SkailFunction] ProbeLiveFeed, seu comando filho, e um painel por passo com as tentativas empilhadas — 15 de 16, cada erro na sequência em que aconteceu.
Uma execução, a história inteira em ordem. Sem arqueologia de correlação — sem juntar dez linhas de log de três serviços pra descobrir a ordem dos fatos. A execução durável grava o histórico completo de eventos, então o trace é a história. Você não a remonta.
Mas ver a falha em ordem ainda não é reproduzi-la. O erro dizia sempre a mesma frase genérica. Eu precisava do estado — a requisição exata, os parâmetros, o que voltou. É aí que o botão Debug this step muda o jogo.
Um JSON, um dotnet run e o estado real de produção
Esse botão não abre mais um painel de log. Ele abre um modal com um bloco de JSON pra copiar. Um só passo:
Você cola isso no launchSettings.json do projeto:
E roda:
O SKAIL_DEBUG_FILE aponta pro envelope de estado capturado daquele task específico: a RPC que o iniciou, os eventos duráveis, os números de sequência, os timestamps e a exceção serializada. Nada de mock, nada de caso de teste montado tentando adivinhar a entrada — é a execução de produção, rodando na minha IDE, com o estado que a produção tinha. Zero chute de reprodução.
Isso é time travel debug: você não observa o que aconteceu de fora, você volta pro exato instante e ponto de execução e roda de novo, localmente, com o estado real. A documentação de Debug Execution descreve o fluxo por inteiro.
Por que retry cego num 4xx é uma armadilha
Com esse task rodando na minha máquina, o breakpoint parou onde o log jamais me levaria. O envelope decodificado apontava para uma linha só:
Um 400. E o [SkailFunction] pai vinha retentando aquilo até 15 de 16 tentativas. Aí está a armadilha inteira num número: um 400 não é falha passageira — é a forma da requisição que está errada. Retentar uma requisição malformada não conserta nada; só queima tentativa, tempo e cota do provedor até o teto de retry.
O código que gerou aquele trace tratava toda falha igual — qualquer exceção subia e o runtime redisparava a tarefa:
A correção é classificar o erro antes de deixá-lo subir. 4xx é permanente: falha rápido e entrega pro caminho de fallback. 5xx e timeout continuam retentáveis, com backoff:
E o pai passa a saber o que fazer com o erro permanente — em vez de insistir, cai no caminho alternativo:
| Código do provedor | O que fazer | |---|---| | 4xx — requisição malformada | falha rápido → caminho de fallback | | 5xx · timeout (408/429) — falha passageira | retry com backoff (do runtime) |
Vale separar as duas coisas, porque elas moram em camadas diferentes. A política de retry é código de aplicação: essa correção coube num arquivo. Já a capacidade que me deixou ver o 400: abrir o trace, replicar o task com o estado de produção, parar no breakpoint certo — essa é da plataforma. O tem que fazer retry na trigger da documentação de boas práticas mora do lado do runtime; a classificação do erro mora do seu.
O que isso tem a ver com agentes de IA
Essa mesma postura é o que você vai querer no dia em que a ação de um agente falhar. Um agente que chama uma ferramenta e recebe um 400 silencioso quinze vezes é exatamente o probe desta história — só que decidindo o próximo passo com um modelo em cima.
Quando a chamada de ferramenta de um agente falha em produção, a pergunta não pode ser "cadê o log?". Tem que ser: consigo inspecionar essa execução de ponta a ponta e reproduzi-la localmente, com o estado real? Não "adiciona mais log e torce", e sim "abre o trace, replica o task, olha o estado". Observabilidade de verdade pra código que move dinheiro ou dados (agente ou não) se parece com isto agora. É a mesma propriedade que sustenta agentes de IA em produção.
O que fica
Nada disso começou como demonstração. Foi um probe de produção que caiu, um stakeholder perguntando se estava tudo bem, e um 400 escondido atrás de quinze linhas de erro idênticas. E é justamente por isso que vale: a plataforma diagnosticando o nosso próprio código é, ela mesma, a demonstração.
Cada camada me deu uma coisa:
- O trace — a história, sem correlação de log.
- O Debug Execution — o estado de produção na minha máquina.
- O breakpoint — a única coisa que o log nunca daria: o motivo.
Ajuste a política de retry num arquivo. Mas o que transformou uma tarde de grep numa investigação de dez minutos foi poder voltar no tempo até o task exato.