// observabilidade

Observabilidade em Produção

O desenvolvimento não termina no deploy. Depois que a aplicação entra no ar, começa outra disciplina: medir o comportamento real, entender como o software é usado e transformar dado em decisão. Esta demonstração percorre a camada de observabilidade deste ecossistema — da telemetria de front-end deste site aos logs estruturados das APIs em produção.

Observabilidade em Produçãofront-end · 3:33narrado

// o que esta demonstração mostra

  • A primeira semana do site em números: visitantes, sessões, origem, dispositivo e duração de visita.
  • Taxonomia de eventos tipados — nenhum evento genérico; cada interação vira informação com contexto.
  • Dado → informação: 44 falhas de vídeo no cliente que seriam invisíveis sem instrumentação.
  • Nas APIs: logs estruturados com correlação por requisição, trilha de auditoria imutável e cache-aside visível no log.
  • Diagnóstico real: um pico de 404 no primeiro dia revelando scanners em busca de arquivos de segredo.

// case study de engenharia

Problema, arquitetura, decisões e o que foi medido — tudo rastreável às fontes listadas no fim. Sem números inventados.

Problema

Antes da instrumentação, o site voava às cegas: nenhum analytics (zero visibilidade de visitantes, origem ou interesse), erros JavaScript só no ErrorBoundary visual — nenhum reportado para fora —, Web Vitals não medidos e nenhum log estruturado. Consequência prática: não saber qual projeto converte, erros silenciosos em produção e regressões de performance invisíveis.

Contexto

Restrições assumidas de saída: site estático (sem backend próprio) na CDN da Vercel, custo zero, sem banner de cookies e sem dependência nova de runtime. O modelo mental que guia tudo: em site estático não existe servidor nem banco — todo dado que importa precisa sair da página no momento em que acontece. Por outro lado, o navegador não expõe o IP ao JavaScript e o Umami o descarta no coletor; até então nenhuma camada sob controle do projeto registrava o IP de uma requisição — e scanners que nunca executam JS eram invisíveis ao analytics.

Arquitetura

Duas camadas separadas de propósito. No navegador, src/observability/: uma única fachada (telemetry.trackEvent / trackAttrs) sobre um catálogo tipado de eventos, fila em memória com flush em lote, providers intercambiáveis (Umami via Send API própria, sem script de terceiros; console em dev), Web Vitals nativos por PerformanceObserver e captura de erros. Na borda da Vercel, middleware.ts + api/session-link.ts + bibliotecas puras em src/server/: logs JSON http_request, security_event e session_link com IP da cadeia de confiança, requestId, geo e detecção de abuso. A ponte entre as duas é o sessionId anônimo por aba — o analytics nunca recebe IP; os logs nunca recebem dado de produto.

Pipeline de observabilidade: navegador e borda da VercelDuas faixas. No navegador: componentes emitem eventos pela fachada telemetry, tipados por um catálogo; uma fila com flush em lote e beacon envia ao Umami pela Send API, sem script de terceiros; Web Vitals e erros entram pelo mesmo caminho; um POST por sessão envia o sessionId a /api/session-link. Na borda da Vercel: o middleware gera requestId, resolve o IP pela cadeia de confiança, avalia sinais de abuso e emite logs JSON http_request e security_event, respondendo 404 a probes, 429 a rajadas extremas ou deixando passar; api/session-link grava o vínculo sessionId com IP. Analytics e logs ficam separados; o sessionId é a ponte de correlação.// navegador · src/observabilitycomponentestelemetry.trackEvent(...)trackAttrs → 1 listenercatálogo tipadoevents.ts · EventPayloadsdedupe · once-per-sessionfila + flushlote 800 ms · beaconboot no idle (2 s)Umami CloudSend API · sem scriptsem IP · cookielessWeb Vitals · errosvitals nativos · erros JSsession-link.ts1 POST por sessãosó o sessionIdsessionId (ponte)// borda Vercel · middleware.ts + api/session-link.ts + src/servertoda requisiçãoinclusive scannersque não rodam JSmiddleware.tsrequestId · IP confiávelsinais: probe · UA · rajadaapi/session-linksessionId ⇄ IPteto 10/min/IPveredito do middlewarepass · segue para a CDN404 · probe de path429 · ≥ 180 req/10 slog-only: UA · rajada · enum.Runtime Logs (Vercel) — 1 linha JSON por registro, campos enumeradoshttp_request (toda requisição) · security_event (motivo concreto, cooldown 30 s/IP) · session_link (1×/sessão)investigação: Umami → data.sessionId → session_link → IP → http_request / security_event → requestIdfail-safe: exceção no middleware = deixar passar · analytics (sem IP) e logs (com IP) nunca se misturam
diagrama SVG inline — sem biblioteca, herda o tema da página
  • Fachada + catálogo

    telemetry.ts e events.ts: um único ponto de emissão, tipado pelo catálogo (snake_case, verbo no passado); fila, dedupe (1,5 s), once-per-session, teto de 300 eventos e 10 erros por sessão, beacon na descarga da página.

  • Sinais nativos

    performance.ts (LCP/CLS/INP/FCP/TTFB via PerformanceObserver, limiares do web.dev) e error-tracking.ts (window.error, unhandledrejection, falha de recurso, ErrorBoundary).

  • Providers

    providers/umami-provider.ts (Send API + sendBeacon) e console-provider.ts; contrato TelemetryProvider que nunca lança exceção.

  • Correlação

    session-link.ts: 1 POST por sessão com o sessionId (o mesmo que o Umami recebe) para /api/session-link, respeitando opt-out, DNT e kill switch.

  • Borda (Vercel Edge)

    middleware.ts gera requestId, resolve IP (src/server/ip.ts), avalia sinais (src/server/security.ts) e emite registros (src/server/security-log.ts); api/session-link.ts grava o vínculo sessionId ⇄ IP.

Decisões e trade-offs

  1. decisão 01

    Umami Cloud como única ferramenta, consumido pela Send API própria (fetch keepalive / sendBeacon) — 0 KB de script de terceiros.

    em vez de
    GA4 (~50 KB + cookies → exigiria banner), Vercel Analytics (eventos custom só no plano Pro), Speed Insights (redundante), Clarity (replay invasivo), PostHog (poder demais), Plausible (sem plano gratuito), OpenTelemetry (sem backend para receber), Sentry (adiado: erros viram eventos no Umami; reavaliar se o volume crescer).
    trade-off
    O endpoint do coletor ainda pode ser bloqueado por ad-blocker — perda aceitável e comum a qualquer analytics. Trocar de ferramenta = escrever um provider novo; nenhum componente muda.
    evidência
    docs/OBSERVABILIDADE.md §4 (tabela de ferramentas avaliadas)
  2. decisão 02

    Catálogo tipado de eventos com fachada única: telemetry.trackEvent(nome, payload) só aceita nomes do catálogo com o payload correspondente; cliques via trackAttrs e um listener delegado no document.

    em vez de
    Strings soltas de evento espalhadas pelos componentes.
    trade-off
    Todo evento novo exige entrada no catálogo (tipo + descrição + origem, validado por teste) — fricção deliberada que impede taxonomia genérica.
    evidência
    src/observability/events.ts · src/observability/__tests__/events.test.ts
  3. decisão 03

    Web Vitals medidos nativamente com PerformanceObserver, sem biblioteca, e enviados ao mesmo coletor como web_vital_measured.

    em vez de
    Vercel Speed Insights / biblioteca de vitals.
    trade-off
    INP usa aproximação do percentil alto das interações (Event Timing API) — suficiente para um portfólio; limitação documentada.
    evidência
    src/observability/performance.ts · docs/OBSERVABILIDADE.md §4
  4. decisão 04

    Boot da telemetria no idle do navegador (requestIdleCallback, timeout 2 s), fila com flush em lote (800 ms), flush imediato para erros/engajamento e sendBeacon em visibilitychange/pagehide.

    trade-off
    Um trackEvent antes do boot seria descartado — por isso initObservability(onReady) entrega um callback para eventos de carregamento (é como a DemoPage emite demo_page_viewed). Custo medido: +6,6 kB gzip no bundle.
    evidência
    src/observability/index.ts · docs/OBSERVABILIDADE.md §9
  5. decisão 05

    IP registrado exclusivamente na borda (Edge Middleware + /api/session-link), nunca no frontend nem no analytics, com cadeia de confiança x-vercel-forwarded-forx-real-ip → primeiro item de x-forwarded-for.

    em vez de
    Confiar em cf-connecting-ip/Forwarded (excluídos: sem Cloudflare/proxy próprio na frente seriam headers forjáveis); enviar IP ao Umami (rejeitado por design).
    trade-off
    Retenção curta dos Runtime Logs da Vercel — tratada como feature de minimização (LGPD: finalidade específica, campos enumerados). A janela deslizante de contagem vive na memória do isolate edge e zera em cold start; contagem global exata exigiria um KV — anotado como evolução, não pendência.
    evidência
    src/server/ip.ts · docs/SEGURANCA-LOGS.md §2 e §6
  6. decisão 06

    Bloqueio na borda só nos dois casos sem falso positivo plausível — probe de path sensível (/.env, /.git, wp-admin, dumps, traversal…) → 404 imediato; ≥ 180 req/10 s do mesmo IP → 429. Todo o resto (scanner UA, UA ausente, ≥ 60 req/10 s, ≥ 30 paths distintos/10 s) é log-only.

    trade-off
    Anti-ruído: 1 security_event por IP a cada 30 s e teto de 2 000 IPs rastreados por isolate. O middleware não enxerga o status de arquivos servidos pela CDN (roda antes da resposta); 404 de rota SPA continua sendo reportado pelo cliente (page_not_found_viewed).
    evidência
    middleware.ts · src/server/security.ts (DEFAULT_THRESHOLDS) · docs/SEGURANCA-LOGS.md §5
  7. decisão 07

    Correlação Umami ⇄ logs pelo sessionId aleatório por aba: o frontend envia 1× por sessão apenas esse id; o servidor grava session_link (sessionId ⇄ IP).

    trade-off
    O pageview nativo do Umami não carrega sessionId — sessões que só geraram pageview não são correlacionáveis; sessões sem JS (scanners) não têm session_link, mas são exatamente as que o middleware enxerga por inteiro. Visitante com opt-out/DNT não envia o vínculo: coerência de consentimento vale mais que cobertura.
    evidência
    src/observability/session-link.ts · api/session-link.ts · docs/SEGURANCA-LOGS.md §4
  8. decisão 08

    Fail-safe total: provider fora do ar, storage bloqueado ou API ausente são engolidos com try/catch; no middleware, qualquer exceção resulta em "deixar passar".

    trade-off
    Observabilidade jamais derruba o site — ao custo de falhas da própria telemetria serem silenciosas (logger em nível debug).
    evidência
    src/observability/providers/types.ts · middleware.ts (bloco catch)

Implementação

  • Atribuição de origem das páginas de demonstração sem identificar ninguém: resume (sem referrer — cliques em PDF não enviam referrer e o currículo é quem publica as URLs), campaign (whitelist utm_*/ref), internal/external.
  • Sanitização: strings truncadas (200–300 ch), stacks a 2 000 ch, URLs de recurso sem query string, qualquer parâmetro de query fora da whitelist descartado antes do envio.
  • Do Not Track respeitado, opt-out persistido (telemetry.optOut()), kill switch por variável de ambiente; sem Website ID nada é enviado (padrão seguro).
  • Segurança de borda: headers nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy, HSTS em tudo; CSP restritiva (script-src 'self'; connect-src 'self' + coletor) nas rotas da SPA.
  • Registros de borda construídos por builders que só copiam campos enumerados — Authorization, Cookie, tokens, corpo e valores de query nunca entram nos logs (apenas as chaves).
  • Matcher do middleware exclui assets/, videos-v4/ e favicon* para não gastar invocação de edge com tráfego sem valor investigativo.

Testes

Suíte Vitest (jsdom) que cobre a camada de telemetria e as bibliotecas puras da borda; tsc -b garante a tipagem forte do catálogo inteiro.

  • Catálogo/convenção de nomes, enriquecimento de contexto, dedupe, once-per-session, teto de erros, provider quebrado (fail-safe), listener delegado, opt-out, serialização de erros, sanitização de URL, achatamento para o Umami e limiares de Web Vitals (src/observability/__tests__/).
  • Resolução de IP pela cadeia de confiança, normalização IPv4/IPv6 (incl. IPv4 mapeado em IPv6) e detectores/limiares de segurança (src/server/__tests__/).
  • Roteamento das páginas de demonstração com .html, barra final e forma limpa (src/__tests__/routing.test.ts).

Observabilidade

Este é o case study da própria observabilidade — o que ela já entregou como informação, não só como dado.

  • Primeira semana do site em números (visitantes, sessões, origem, dispositivo, duração) e funil do currículo: demo_page_viewedproject_video_started → progresso/quartis → project_video_completedproject_deploy_clicked/project_repository_clicked.
  • 44 falhas de vídeo no cliente que, sem instrumentação, seriam invisíveis (project_video_failed / resource_load_failed).
  • Pico de 404 no primeiro dia revelando scanners em busca de arquivos de segredo — hoje respondido na borda com 404 e security_event.
  • Fluxo de investigação: sessão suspeita no Umami → data.sessionIdsession_link → IP → http_request/security_event por IP → requestId amarrando cada linha.
  • Métricas de negócio definidas com regra de decisão (taxa de conversão em deploy, interesse por projeto, conclusão de vídeo, funil de contato, vitals por dispositivo).

Performance

Números medidos — custo da própria telemetria e baseline de produção do site registrado em 2026-08-19 (antes das etapas de otimização do plano de auditoria).

  • Telemetria: +6,6 kB gzip no bundle (70,8 → 77,4 kB), zero scripts de terceiros, zero requisições até o idle.
  • Lighthouse 12 em produção (2026-08-19): Performance 95 (mobile) / 96 (desktop); Best Practices 100; SEO 100; CLS 0; TBT 21 ms (mobile); TTFB 18–22 ms.

Resultados e aprendizados

  • Decisão de produto sustentada por dado: o site mede qual projeto converte, onde o visitante desiste e como o vídeo performa — e o currículo ganhou um funil mensurável.
  • Duas fronteiras honestas documentadas: analytics de produto (Umami, sem IP) e observabilidade de segurança (borda, com IP e finalidade específica) seguem separados de propósito — um mede interesse, o outro protege.
  • Scanners que nunca executam JavaScript saíram da invisibilidade: probes conhecidos são bloqueados na borda e registrados com IP confiável, requestId e geo.
  • Aprendizado: privacidade por construção (builders com campos enumerados, whitelist de query, sem identify) custa menos do que remediar depois.

Fontes

Tudo o que está escrito acima é rastreável a estes arquivos e repositórios.

  • docs/OBSERVABILIDADE.md
  • docs/SEGURANCA-LOGS.md
  • docs/BASELINE.md
  • middleware.ts
  • api/session-link.ts
  • src/observability/*.ts
  • src/server/*.ts
  • src/data/projects.ts (descrições dos clipes site-obs / agenda-obs)

eduardo.valente © 2026 // engenharia em produção