Saber que quebrou antes do cliente: observabilidade
Existem duas formas de descobrir que seu sistema quebrou. Uma é o cliente te mandar mensagem. A outra é você já saber, ter olhado, e talvez até já ter arrumado quando a mensagem chega — se ela chegar. A diferença entre as duas não é tamanho de time nem orçamento de ferramenta. É uma decisão de tratar observabilidade como parte de construir a feature, não como algo que você adiciona depois que dói. No ClubPetro, com um monte de jobs assíncronos, crons e integrações externas, essa decisão é o que separa "operamos com calma" de "vivemos apagando incêndio". Este post é sobre como a gente tenta saber primeiro.
O ponto cego não é o código, é o assíncrono
Numa base fortemente tipada e com tRPC de ponta a ponta, o compilador pega uma classe enorme de erros antes de rodar. Mas tem uma fronteira onde o tipo não alcança: o que roda sozinho, longe de um usuário olhando. Os crons do Trigger.dev, os jobs de sync, os dispatchers. Se um deles falha silenciosamente, não tem tela girando, não tem ninguém pra reclamar na hora — o dado simplesmente para de chegar, e você descobre dias depois que um número está velho.
Foi exatamente aí que a gente mais se queimou. E o aprendizado não foi "escreva mais testes", foi mudar como os jobs são construídos:
- Isolar falha por item. Um job que processa mil redes não pode morrer inteiro porque a rede número 43 tem um dado ruim. Cada item roda no seu próprio try-catch; um item quebrado é logado e o job segue. Antes, uma falha isolada derrubava o lote todo e a gente nem sabia qual item era o culpado.
- Idempotência. Um cron que roda de novo não pode duplicar dado nem corromper estado. Isso é o que te dá coragem de simplesmente re-executar quando algo falhou, em vez de ter medo do "retry".
- Log com contexto suficiente. "Erro no sync" é inútil. "Erro no sync da rede X, item Y, motivo Z" é o que transforma um alerta em uma ação. O log tem que dizer o que quebrou e onde, não só que quebrou.
Observabilidade, aqui, começa no desenho do job. Um job que falha inteiro e silenciosamente é inobservável por construção.
Runs, health checks e a diferença entre 200 e "ok"
Ver o que os processos assíncronos estão fazendo virou uma superfície de produto, não só uma tela de devops.
- Central de processos. A gente expõe os runs do Trigger.dev no próprio admin — quais jobs rodaram, quais falharam, quando. Ver o histórico de execuções num lugar é o mínimo pra saber se o cron da meia-noite realmente rodou, ou se ele só devia ter rodado. (Aprendizado adjacente: schedule do Trigger.dev só sincroniza no deploy — um cron que "existe no código" mas não foi deployado nunca roda, e o painel de runs é onde você flagra isso.)
- Health checks de integração. As integrações externas — WhatsApp, ads, RD Station — têm checagens periódicas de saúde. Uma integração que parou de responder aparece como não-saudável antes de alguém notar que os dados pararam de entrar.
- Cuidado com o falso verde. Uma armadilha real que a gente documentou: o batch do tRPC devolve HTTP 200 mesmo quando um procedure interno falha. Monitorar "está retornando 200?" te dá um verde mentiroso. Saúde de verdade é olhar o resultado, não o envelope. Observabilidade ruim é pior que nenhuma, porque te dá confiança falsa.
O incidente que só existia porque ninguém observava
O caso mais didático que a gente tem: uma página do produto ficou travada, e a causa raiz era uma variável de ambiente que nunca tinha existido em produção. Não era um bug no código — o código estava certo. Era que a credencial que um cron precisava não estava provisionada nem no Trigger.dev nem no runtime de deploy, e ninguém sabia porque o cron falhava calado. Ele "rodava", falhava por falta de credencial, e nada gritava.
O conserto imediato foi provisionar a env var nos dois lugares. Mas a lição de observabilidade foi maior:
- Falha de configuração é falha de sistema. Um job que depende de uma credencial ausente tem que falhar alto, não engolir o erro e seguir como se nada fosse. Um erro de "faltou credencial" deveria ser tão visível quanto um crash.
- "Deployado" e "funcionando" são estados diferentes. Código na main, migration não aplicada, secret não provisionado, schedule não sincronizado — cada um desses é um jeito de o sistema estar "pronto" e quebrado ao mesmo tempo. Observabilidade é o que revela a diferença.
Depois desse, a gente passou a tratar "o cron ficou verde de novo" como parte da definição de pronto, não como algo que a gente assume.
Observabilidade barata, mas deliberada
A gente não tem um stack gigante de APM. O que a gente tem é um conjunto de hábitos baratos e uma recusa em construir coisa assíncrona cega:
- Todo job novo nasce com log de contexto e isolamento de falha. Não é opcional, é parte de escrever o job.
- Todo cron novo é conferido rodando de verdade após o deploy — porque schedule só sincroniza no deploy, e "escrevi o cron" não é "o cron roda".
- Toda integração externa ganha um health check. Se depende de um sistema que a gente não controla, a gente precisa de um sinal de quando ele nos abandonou.
Nada disso é caro. Tudo isso é decisão de fazer na hora certa — antes de doer — em vez de depois.
O que faríamos de novo
Construir observabilidade dentro dos jobs, e não por cima deles, foi o que mais nos poupou de noites ruins. Isolamento de falha por item e log com contexto renderam mais tranquilidade que qualquer dashboard bonito renderia. A materialized view, a fila paced, os health checks — tudo isso é observável por desenho.
O que a gente reforçaria mais cedo: tratar configuração e deploy como parte do sistema observável desde o começo. Metade dos nossos sustos não foram bugs de código — foram env var ausente, migration não aplicada, schedule não sincronizado. Coisas invisíveis até a gente decidir torná-las visíveis.
Se você é o tipo de engenheiro que, ao escrever um job, já pergunta "e como eu vou saber quando isso falhar às três da manhã?" — que acha que um erro engolido em silêncio é pior que um crash barulhento — você vai se sentir em casa aqui. A gente não persegue zero incidentes. A gente persegue saber primeiro, sempre antes do cliente. E isso a gente constrói, um job de cada vez.