Logs, feature flags e observabilidade que ajudam o QA

Neste guia, você vai ver o que um time de desenvolvimento pode colocar no código para que o QA encontre, reproduza e explique bugs muito mais rápido: logs estruturados em JSON, um correlation id devolvido no header da resposta, níveis de log bem usados, cuidado com dados sensíveis, mensagens de erro que ajudam de verdade, feature flags para testar caminhos e desligar risco, flags por usuário de teste e um endpoint de healthcheck que mostra a versão em execução. Tudo com exemplos de código curtos que você pode adaptar.

Trabalho com qualidade de software desde 2010, e boa parte do tempo perdido em investigação de bug não vem da falha em si, mas da falta de pistas. “Deu erro 500” sem mais nada vira uma tarde inteira de idas e vindas. Quando a aplicação foi pensada para ser observável, o mesmo bug vira um ticket com o id da requisição, a linha do log e a versão do serviço. Este texto é para devs que querem facilitar esse caminho.

Por que observabilidade é assunto de QA também

Testabilidade não é só ter seletores estáveis na tela ou uma API documentada. É também conseguir responder três perguntas depois que algo deu errado: o que aconteceu, em qual versão e com quais dados. Logs, métricas e traces respondem a primeira; um endpoint de versão responde a segunda; e um identificador de requisição liga as duas coisas ao relato do testador. Sem isso, o QA reporta sintomas e o dev precisa reproduzir tudo do zero.

Logs estruturados em JSON

Log em texto livre é fácil de escrever e difícil de pesquisar. Quando cada linha é um objeto JSON com campos previsíveis, dá para filtrar por request_id, por usuário de teste ou por rota sem expressões regulares frágeis. Ferramentas de agregação de log também indexam esses campos automaticamente.

Compare uma linha em texto livre com a mesma informação estruturada:

2026-10-13 14:02:11 ERRO falha ao criar pedido do usuario 42: estoque insuficiente
{"timestamp":"2026-10-13T14:02:11.482Z","level":"error","service":"pedidos-api","version":"1.8.3","request_id":"3f2b9c1e-7a44-4d0e-9a51-2c8e6b0f1d77","route":"POST /orders","user_id":"42","event":"order_create_failed","reason":"insufficient_stock","sku":"CAM-001"}

Alguns campos que valem a pena padronizar em todo serviço: timestamp em ISO 8601 com fuso UTC, level, service, version, request_id, event (um nome curto e estável para o que aconteceu) e o contexto específico da operação. Um exemplo em Node.js com uma biblioteca de log estruturado:

// logger.js
const pino = require('pino');

const logger = pino({
  base: { service: 'pedidos-api', version: process.env.APP_VERSION },
  timestamp: pino.stdTimeFunctions.isoTime,
  redact: ['req.headers.authorization', '*.password', '*.cpf', '*.cardNumber'],
});

module.exports = logger;

Quando o QA receber uma linha dessas copiada de um terminal ou de um painel, ela costuma vir em uma linha só e difícil de ler. Cole no Formatador de Log para separar níveis e campos, ou no Formatador JSON para indentar o objeto e anexar ao bug de forma legível.

Correlation id devolvido no header

O correlation id (ou request id) é um identificador único gerado na entrada da requisição e propagado para todos os serviços que ela toca. O detalhe que mais ajuda o QA é simples: devolver esse id em um header da resposta. Assim o testador vê o valor no DevTools, no Postman ou no relatório do teste automatizado e cola no ticket. O dev procura esse id nos logs e encontra a história completa.

// middleware Express
const { randomUUID } = require('crypto');
const logger = require('./logger');

function requestId(req, res, next) {
  const id = req.get('X-Request-Id') || randomUUID();
  req.id = id;
  req.log = logger.child({ request_id: id });
  res.set('X-Request-Id', id);
  next();
}

app.use(requestId);

Se o cliente já mandar um X-Request-Id, reaproveite-o: isso permite que o teste automatizado gere o próprio id e depois busque nos logs exatamente aquela execução. Para gerar ids válidos em testes manuais, o Gerador de UUID resolve. Em arquiteturas distribuídas, o padrão W3C Trace Context define o header traceparent para propagar o contexto entre serviços (especificação em https://www.w3.org/TR/trace-context/, consultado em 13/10/2026).

Do lado do QA, vale incluir o header na evidência. Um teste de API pode registrar o id sempre que falhar:

// Cypress
cy.request({ method: 'POST', url: '/orders', body: pedido, failOnStatusCode: false })
  .then((res) => {
    if (res.status >= 400) {
      cy.log(`Falhou. X-Request-Id: ${res.headers['x-request-id']}`);
    }
    expect(res.status).to.eq(201);
  });

Níveis de log com critério

Níveis existem para separar ruído de sinal. Quando tudo é error, nada é. Uma convenção prática:

  • debug: detalhes úteis só durante investigação (payloads resumidos, decisões internas). Normalmente desligado em produção.
  • info: eventos de negócio esperados, como “pedido criado” ou “login realizado”.
  • warn: algo fora do normal que o sistema contornou, como um retry, um fallback ou uma flag ausente usando valor padrão.
  • error: a operação falhou e alguém precisa olhar. Inclua o motivo e o contexto.

Uma ajuda extra para ambientes de teste: permitir mudar o nível de log por variável de ambiente, sem novo deploy. Em homologação, ligar debug por alguns minutos costuma encurtar bastante a investigação.

Não logue dados sensíveis

Log é lido por muita gente e costuma ficar guardado por bastante tempo. Senhas, tokens, números de cartão, CPF completo e dados de saúde não devem aparecer, nem em debug. Use a opção de redact da sua biblioteca, mascare antes de logar e prefira identificadores internos ao dado pessoal. No Brasil, a LGPD trata do uso de dados pessoais (texto da lei em https://www.planalto.gov.br/ccivil_03/_ato2015-2018/2018/lei/l13709.htm, consultado em 13/10/2026).

function mascararCpf(cpf) {
  const d = String(cpf).replace(/\D/g, '');
  return d.length === 11 ? `***.***.${d.slice(6, 9)}-**` : '[invalido]';
}

req.log.info({ event: 'cadastro_iniciado', cpf: mascararCpf(body.cpf) });

Uma dica para o QA: inclua no plano de testes um caso que verifica se a resposta de erro e os logs não vazam token ou senha. Se o sistema usa JWT, o Decodificador JWT ajuda a conferir o que vai dentro do token e se ele carrega dados que não deveria. Para cadastros de teste, use CPFs gerados e válidos no Validador de CPF ou no Gerador de Massa de Testes, nunca dados reais.

Mensagens de erro úteis

Uma boa resposta de erro diz o que deu errado, em qual campo e como corrigir, sem expor stack trace ao cliente. O formato Problem Details for HTTP APIs, da RFC 9457, é uma boa base (https://www.rfc-editor.org/rfc/rfc9457, consultado em 13/10/2026). Somar a ele um código estável e o request id facilita automação e suporte:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
X-Request-Id: 3f2b9c1e-7a44-4d0e-9a51-2c8e6b0f1d77

{
  "type": "https://api.exemplo.com/erros/estoque-insuficiente",
  "title": "Estoque insuficiente",
  "status": 422,
  "detail": "O SKU CAM-001 tem 2 unidades e foram pedidas 5.",
  "code": "INSUFFICIENT_STOCK",
  "request_id": "3f2b9c1e-7a44-4d0e-9a51-2c8e6b0f1d77"
}

O campo code é o que o teste automatizado deve verificar, porque o texto de detail pode mudar com tradução ou ajuste de redação. Para comparar respostas de erro entre versões, o Comparador JSON mostra rapidamente o que mudou.

Feature flags para testar caminhos e desligar risco

Feature flags separam o deploy da liberação. Para o QA, isso significa duas coisas: conseguir testar uma funcionalidade nova em produção ou homologação sem expô-la a todos, e ter um botão de emergência para desligar algo arriscado sem rollback. Algumas boas práticas:

  • Dê nomes descritivos e com prazo, como checkout_novo_frete, e remova a flag quando a funcionalidade estabilizar.
  • Defina um valor padrão seguro: se o serviço de flags cair, o sistema deve seguir o caminho antigo e testado.
  • Logue a avaliação da flag em nível debug ou junto do evento de negócio, para saber qual caminho cada requisição seguiu.
  • Teste os dois estados: flag ligada e desligada fazem parte da matriz de testes enquanto a flag existir.
async function calcularFrete(pedido, ctx) {
  const usarNovo = await flags.isEnabled('checkout_novo_frete', {
    userId: ctx.userId,
    default: false,
  });
  ctx.log.info({ event: 'frete_calculado', flag_checkout_novo_frete: usarNovo });
  return usarNovo ? freteV2(pedido) : freteV1(pedido);
}

Flags por usuário de teste

Uma variação muito útil é ativar flags para usuários ou grupos específicos. Com isso, o QA usa contas de teste conhecidas para exercitar o caminho novo enquanto o resto das pessoas continua no fluxo atual. Também dá para criar contas que forçam cenários difíceis, como um usuário que sempre recebe o fallback de pagamento.

{
  "checkout_novo_frete": {
    "default": false,
    "rules": [
      { "users": ["qa_frete_01", "qa_frete_02"], "value": true },
      { "group": "beta_testers", "value": true }
    ]
  }
}

Cuidado com um atalho comum: aceitar um header ou parâmetro de query para ligar flags. Se existir, ele deve funcionar só em ambientes de teste e exigir autenticação, senão vira uma porta para qualquer pessoa ativar funcionalidades incompletas. Para praticar cenários com perfis diferentes, a API de prática do Lab tem usuários como standard_user e locked_user.

Healthcheck e versão exposta

Uma pergunta que se repete em qualquer time: “esse ambiente já está com a correção?”. Um endpoint de healthcheck que devolva versão, commit e data do build responde isso em segundos e evita testar a versão errada.

app.get('/health', async (req, res) => {
  const db = await checarBanco();
  res.status(db ? 200 : 503).json({
    status: db ? 'ok' : 'degraded',
    service: 'pedidos-api',
    version: process.env.APP_VERSION,
    commit: process.env.GIT_SHA,
    build_time: process.env.BUILD_TIME,
    checks: { database: db ? 'ok' : 'fail' },
  });
});

Exponha só o necessário: versão e status das dependências ajudam; variáveis de ambiente, hosts internos e credenciais não. Se o build_time vier como timestamp numérico, o Conversor de Timestamp traduz para uma data legível. Do lado da automação, um passo inicial na pipeline que consulta /health e confere a versão esperada evita rodar a suíte inteira contra um deploy que ainda não terminou.

Checklist rápido para o time

  • Logs em JSON com timestamp, level, service, version, request_id e event.
  • Header X-Request-Id em todas as respostas, reaproveitando o enviado pelo cliente.
  • Níveis de log com critério e ajustáveis por ambiente.
  • Redação de senhas, tokens, cartões e documentos pessoais.
  • Erros com código estável, mensagem clara e request id, sem stack trace para o cliente.
  • Feature flags com padrão seguro, logadas e testadas nos dois estados.
  • Flags direcionadas a usuários de teste conhecidos.
  • Endpoint /health com versão, commit e status das dependências.

Próximos passos

Escolha um serviço e comece pelo par que mais rende: logs em JSON e request id devolvido no header. Depois, combine com o QA quais usuários de teste vão receber as próximas flags. Ferramentas que ajudam no dia a dia: