Herdei um legado sem documentação: 14 dias medindo antes de escrever qualquer linha
Peguei o acesso de um sistema que a fábrica anterior entregou e depois sumiu. README com três linhas, nenhum diagrama, ninguém do time original disponível para conversar. A primeira vontade de qualquer dev (a minha inclusive) é abrir o editor e começar a arrumar o que está feio. Nas duas primeiras semanas eu não escrevi feature nenhuma e não refatorei nada. Só medi.
A ordem que uso é sempre a mesma: subir o sistema do zero numa máquina limpa anotando cada coisa que falta, instrumentar o que já está em produção, e só então olhar o código. Estancar o sangramento é rápido. Descobrir de onde vem o sangue demora mais. Tratar a doença é remédio de meses. Quem inverte isso anuncia reescrita no dia 4 e some por um trimestre.
Medida 1: quanto tempo leva para subir isso do zero
Container vazio, cronômetro ligado, sem perguntar nada para ninguém.
$ git clone git@github.com:cliente/plataforma.git && cd plataforma
$ docker compose up -d
[+] Running 3/3
api-1 | Error: connect ECONNREFUSED 127.0.0.1:5432
api-1 | [nodemon] app crashed - waiting for file changes
$ docker compose logs worker | tail -3
worker-1 | Error: Missing env var STORAGE_BUCKET_KEY
worker-1 | Error: Missing env var LEGACY_SOAP_ENDPOINT
worker-1 | exited with code 1
Eu não tento adivinhar nada nessa hora. Abro um arquivo de texto e anoto o que faltou, na ordem em que quebrou:
- 11 variáveis de ambiente que só existiam no notebook do dev que saiu, e nenhuma no
.env.example - dump do banco que a operação manda por WhatsApp, com dado de cliente real dentro
- Node travado numa versão de 2021 por causa de uma lib nativa que não compila em versão nova
- integração com um serviço de terceiro que não tem sandbox, então local ninguém testa
- duas migrations que só rodam se você aplicar na ordem contrária à do timestamp
Esse arquivo virou o primeiro documento honesto do projeto. Em obra existe uma coisa chamada as-built: a planta do que foi construído de fato, não a que saiu do escritório. É isso que você está produzindo enquanto xinga o docker compose.
Do clone até o primeiro request respondendo 200 local foram uns dois dias e meio. Guardei o número, transformei o arquivo num script e passei a tratar o tempo de bootstrap como métrica:
$ time ./scripts/bootstrap.sh
==> checando dependências ok
==> subindo postgres e redis ok
==> aplicando 214 migrations ok
==> seed mínimo (3 usuários, 2 produtos) ok
==> GET /api/health -> 200
real 4m12.883s
De dois dias e meio para quatro minutos. É o único indicador do resgate que o time comercial entende sem tradução.
Medida 2: o /health que responde 200 com o banco caído
O painel do cliente estava verde havia meses. Fui ver o que ele media.
// src/routes/health.js
router.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', version: pkg.version });
});
Esse endpoint responde 200 com o Postgres desligado, com a fila parada e com o disco cheio. A métrica existe, a garantia não. Troquei por algo que ao menos toca nas dependências:
router.get('/health', async (req, res) => {
const checks = {};
const started = Date.now();
try {
await db.raw('select 1');
checks.db = 'ok';
} catch (err) {
checks.db = err.message;
}
try {
checks.queueDepth = await redis.llen('bull:faturamento:wait');
} catch (err) {
checks.queue = err.message;
}
const healthy = checks.db === 'ok' && checks.queueDepth < 5000;
res.status(healthy ? 200 : 503).json({ ...checks, ms: Date.now() - started });
});
Depois disso, o básico de observabilidade e para por aí: log estruturado com id de requisição, agrupamento de erro em qualquer ferramenta de captura, tempo de resposta por rota na borda. Nada encosta em regra de negócio, então o risco de quebrar alguma coisa é baixo.
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] || randomUUID();
const t0 = process.hrtime.bigint();
res.on('finish', () => {
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
logger.info({ id: req.id, route: req.route?.path, status: res.statusCode, ms });
});
next();
});
Uma semana de dado real de produção me ensinou mais sobre esse sistema do que um mês lendo código. Três rotas concentravam quase todo o tráfego. E o erro que a operação descrevia como "acontece de vez em quando" acontecia umas 400 vezes por dia sem ninguém olhando.
Medida 3: a lentidão estava dentro de um laço
Com log por requisição, a rota mais lenta apareceu sozinha. 143 queries numa única chamada de fechamento de pedido.
for (const item of pedido.itens) {
const produto = await db('produtos').where({ id: item.produto_id }).first();
const estoque = await db('estoques').where({ produto_id: produto.id }).first();
const preco = await db('precos').where({ produto_id: produto.id, tabela: pedido.tabela }).first();
total += calcular(item, produto, estoque, preco);
}
O EXPLAIN ANALYZE de uma dessas, sozinha, já entregava o resto da história:
Seq Scan on estoques (cost=0.00..18342.00 rows=1 width=48)
(actual time=12.431..12.433 rows=1 loops=1)
Filter: (produto_id = 88213)
Rows Removed by Filter: 412739
Planning Time: 0.184 ms
Execution Time: 12.470 ms
Seq scan varrendo 412 mil linhas para achar uma. Multiplica por 143 e você tem quase dois segundos gastos só em ida e volta ao banco. Índice e um whereIn fora do laço resolveram perto de 80% do tempo dessa rota, sem tocar em uma linha de regra de negócio.
Medida 4: 92% de cobertura, 41% de mutation score
A suíte tinha 92% de cobertura. Peguei o teste do módulo mais crítico:
it('calcula o total do pedido com desconto', async () => {
const pedido = await criarPedido({ itens: 3, cupom: 'BLACK10' });
await calcularTotal(pedido);
});
Roda, passa, conta como linha coberta e não afirma nada. Rodei mutation testing em um módulo só, para ter prova em vez de opinião:
$ npx stryker run --mutate "src/pedidos/**/*.js"
Ran 1.42 tests per mutant on average.
File | % score | # killed | # timeout | # survived | # no cov |
------------|---------|----------|-----------|------------|----------|
All files | 41.18 | 21 | 3 | 27 | 7 |
total.js | 28.57 | 4 | 0 | 10 | 4 |
desconto.js| 45.00 | 9 | 2 | 9 | 1 |
Trocar >= por > no cálculo de desconto não matou nenhum teste. Ou seja, dava para inverter a regra de faturamento e a suíte continuava verde.
Não tentei escrever a suíte inteira em duas semanas. Escrevi teste de caracterização nos caminhos por onde o dinheiro passa, fixando o comportamento de hoje do jeito que ele está, inclusive onde está errado:
it('congela o total atual do pedido 4471 (comportamento de produção)', async () => {
const total = await calcularTotal(fixtures.pedido4471);
expect(total).toBe(287.4); // valor que produção devolve hoje, com o arredondamento estranho
});
Isso é rede de proteção, e ainda não é qualidade. Qualidade vem depois, quando você já sabe o que pode mexer sem derrubar receita.
Onde esse plano falha
Ele falha quando o runtime não recebe mais patch de segurança, quando uma dependência crítica está parada há anos ou quando não existe gente no mercado para a stack. Aí a conta vira e a reconstrução por pedaço entra na mesa, com o sistema antigo rodando ao lado como referência de comportamento.
Também não sei se duas semanas fazem sentido para um sistema de 3 mil linhas com quarenta usuários. Para esse, uma tarde de leitura resolve, e gastar 14 dias medindo seria só teatro de método.
A pergunta que eu deixo, porque é a que mais me interessa: quanto tempo um dev novo leva para subir o sistema que você mantém hoje, numa máquina limpa, sem perguntar nada para ninguém? Alguém aqui já mediu esse número de verdade, ou ele ainda é aquele chute otimista que a gente fala em entrevista?
Publicado originalmente no blog da Revin: https://revin.com.br/pt/blog/herdei-sistema-legado-sem-documentacao-duas-semanas