brunocordeiro.dev

Link Charts — estudo de caso

Encurtador de URL com analytics de clique, que mantenho sozinho em produção desde 2025 — Laravel, Next.js e deploy blue/green por tag

Visitar linkcharts.com.brv2.20.0Release em produção · atualizado em ago 2026

Escopo do sistema

Cada clique é enriquecido com geografia, dispositivo, horário e qualidade de tráfego antes de ser gravado, e alimenta cinco dashboards de análise.

Além do encurtamento, o produto tem redirect com preview Open Graph para os bots do WhatsApp e do Telegram, quality score anti-fraude por clique, subdomínios personalizados com página link-in-bio, API pública, QR codes, relatórios com export CSV e senha, expiração e agendamento de links. A monetização vem de AdSense e Google Ads.

São cerca de 1.929 commits meus em 3 repositórios entre março de 2025 e agosto de 2026, em paralelo a um emprego em tempo integral, com 50 releases sob semver independente por repositório (backend em v2.16.0, frontend em v1.19.0).

Arquitetura

ArquiteturaO caminho de uma request, do navegador até o worker de fila.Navegadordashboards · link-in-bioBotsWhatsApp · TelegramCloudflareTLS · CDN · real-ip do clienteDigitalOcean · um dropletnginxupstream blue/green · cutover sem downtimeNext.js 15 · React 19App Router · TypeScript estritoISR com cache tags · TanStack QueryAuth0 · CSP/HSTS no middlewareApexCharts · LeafletLaravel 12 · PHP 8.2Controller → Service → Repositoryinjeção de dependência · API keys/r/{slug}responde antes de rastrearproxy por rewritesJWT httpOnly · sem CORS302 · humanoHTML + OG · botPostgreSQL 15links · cliques · agregaçõesRedis 7cache · fila de jobsWorker de cliquesdedup_key com índice únicograva o clique — retry não duplicaX-Request-Id — propagado do navegador até o worker de fila
síncronoassíncrono, depois da respostaO caminho de uma request, do navegador até o worker de fila.

O backend é Laravel 12 com PostgreSQL 15 e Redis 7, em camadas Controller, Service e Repository com injeção de dependência. A rota crítica /r/{slug} responde HTML com Open Graph para bots e um 302 para humanos; o tracking roda depois, num job assíncrono idempotente em que uma coluna dedup_key com índice único garante que retry não duplica clique.

O frontend é Next.js 15 com React 19 e TypeScript estrito, TanStack Query, ISR com cache tags, i18n em dois idiomas, Auth0 e CSP/HSTS em middleware. Os gráficos usam ApexCharts e os mapas, Leaflet.

A integração passa por proxy de rewrites, sem CORS, com JWT em cookie httpOnly e X-Request-Id propagado do navegador até o worker de fila para correlacionar logs de ponta a ponta.

Testes e análise estática

Testes PHPUnit
902 métodos em 133 arquivos (36 unit, 97 feature)
Matriz de banco no CI
a suíte roda 2× por push: SQLite e PostgreSQL 15 real
Migrations
56, zero destrutivas — MigrationSafetyTest reprova dropColumn no up()
Análise estática
PHPStan/Larastan nível 5 com baseline
E2E
Playwright, 6 projects (320/375/desktop × público/autenticado)
Abuso e resiliência
16 rate limiters nomeados + testes de IP spoofing e retry de fila

Verificações de CI antes do merge

Antes de integrar, cada push passa por um hook local e pelo CI, com a suíte rodando duas vezes em dois bancos diferentes. Merge não publica nada: deploy só nasce de tag.

Deploy blue/green por tag

Publicar depende de um push de tag. A imagem builda no runner do GitHub em 2m03s, nunca no servidor, e a troca de cor acontece sem derrubar requisição: o downtime medido caiu de ~5min para 0s.

Deploy blue/green por tagDo branch à troca de cor — e por que cada etapa é assim.branchcommitpushCImergedaqui para baixo é publicação — o gatilho é a tagtagv2.16.0, à mãobuildrunner GitHub · 2m03sghcrimagem versionadarsyncsó artefatos de deploynginxcutover graceful do upstreamblue · versão anteriordeixa de receber requisições novastermina as que já estavam em voodrain 30s → stopgreen · versão novawarm-up: migrate retrocompatível + caches/health em loop, até 30× a cada 2sfalhou? aborta e blue segue no ar1.035/1.035 amostras com HTTP 200 durante o cutover — downtime medido: 0sbuild no runner do GitHub: o droplet tem 2 vCPU e 3,8 GB — compilar ali tiraria CPU de quem está sendo atendidoo rsync leva só composes, scripts e config — código-fonte e toolchain não chegam ao servidora infra (postgres, redis, alloy) não é tocada: o deploy antigo derrubava o banco junto com a appmigrate online exige retrocompatibilidade; migration destrutiva vai em modo offline, ~20s, marcada por testeo health check bate no nginx e numa rota do Laravel — 200 do nginx não prova que o PHP-FPM subiurollback é a mesma esteira com a tag anterior — um caminho de emergência à parte seria o menos testado
no ardrenandoDo branch à troca de cor — e por que cada etapa é assim.

Observabilidade

O OpenTelemetry exporta traces, métricas e logs para o Grafana Cloud via Grafana Alloy, com tail sampling que guarda 100% dos erros e das requisições lentas. Cada trace carrega o SHA do deploy, então uma regressão aponta direto para a release que a introduziu.

Os logs saem em 8 canais por domínio, com redação automática de PII. Faro RUM cobre o frontend e o Pyroscope faz profiling contínuo do PHP. Os 4 dashboards e as 9 alert rules vivem como JSON no repositório; nada é configurado à mão na UI.

Fora da infra, um probe roda a cada 5 minutos e abre sozinho uma issue de incidente se o serviço cair — é o que cobre a queda total, em que os alertas internos não teriam de onde reportar.

Métricas de produção

Os painéis abaixo consomem a API Prometheus do meu workspace no Grafana Cloud, o mesmo que monitora o Link Charts em produção. Quando a API não responde, o painel degrada para um snapshot versionado e indica isso no badge. O uptime vem do probe externo no GitHub Actions.

Grafana · linkcharts · produçãoÚltimos 30 diasatualizado 01/08/2026, 00:00 UTCdados via Grafana Cloud · Prometheus
uptime 30dGitHub Actions
99,0%probe externo a cada 5 min — abre issue de incidente sozinho
p95 · redirectsnapshot
180 msrota crítica /r/{slug}, últimas 24h
erros 5xxsnapshot
0,4%percentual das requisições, últimas 24h
requisições/minsnapshot
12,0média das últimas 24h
commits por mêsgit log
git real dos repositórios do Link Charts, mar/2025–ago/2026
1.035/1.035 amostras de deploy com HTTP 200 · 9 alert rules e 4 dashboards versionados como JSON no repositório — zero config na UI

Postmortems

Em julho de 2026, um deploy do modelo antigo, com build no próprio servidor, devolveu 918 respostas HTTP 502 — uma delas para um visitante real. Foi o gatilho para reescrever o release como blue/green por tag, com warm-up, health check em loop e abort automático em falha.

Um build-arg ausente no Dockerfile compilou as conversões do Google Ads como string vazia, e campanhas rodaram semanas sem registrar conversão. A correção virou gate de CI: um script compara os NEXT_PUBLIC_* usados no código com os ARG do Dockerfile e bloqueia o build se faltar algum.

O IP do cliente era forjável nos logs e nos rate limiters. Corrigi com o real-ip do Cloudflare e deixei um teste automatizado para a falha não voltar despercebida.

Um deploy quebrou no meio da esteira enquanto um medidor externo batia no site a cada 2 segundos: 156 de 156 amostras responderam 200 — o release falho aborta antes do cutover, então a versão no ar não é tocada.

Desenvolvimento com agentes de IA

Construí o Link Charts num fluxo guiado por spec — brainstorm, design doc, plano, execução — usando agentes de IA para escrever a maior parte do código. Um arquivo de contexto de arquitetura versionado no repositório orienta os agentes, e ADRs e postmortems realimentam esse contexto ao longo do tempo.

O comando /ship leva do commit ao deploy com no máximo duas tentativas de autocorreção por etapa; se não resolver, ele para e devolve o controle para mim. Regra que precisa valer vira teste de CI, não página de wiki.

São cerca de 1.929 commits solo em 17 meses sob as mesmas travas: os 902 testes, o PHPStan e o limite de zero warnings bloquearam merges o tempo todo.

Os repositórios em números

Os dois repositórios do produto são públicos. Os números abaixo vêm direto da API do GitHub — commits e tags contados pelo header de paginação, linguagens pela proporção de bytes no código.

linkchart-frontend ↗Next.js 15 · React 19 · TypeScript
live
commits
1.039
tags no git
30
última tag
v1.21.0
último push
31/08/2026
linguagens
  • TypeScript 96%
  • CSS 2%
  • JavaScript 2%
  • Shell <1%
  • Dockerfile <1%
linkchart-backend ↗Laravel 12 · PHP 8.2
live
commits
731
tags no git
28
última tag
v2.20.0
último push
31/08/2026
linguagens
  • PHP 96%
  • Blade 2%
  • Shell <1%
  • HTML <1%
  • Dockerfile <1%
  • PLpgSQL <1%

Stack

Frontend

  • Next.js 15 (App Router)
  • React 19
  • TypeScript estrito
  • MUI 6
  • TanStack Query 5
  • ApexCharts
  • Leaflet
  • Auth0

Backend

  • Laravel 12
  • PHP 8.2
  • PostgreSQL 15
  • Redis 7
  • filas assíncronas
  • API pública com API keys

Infra

  • Docker multi-stage
  • GHCR
  • nginx
  • DigitalOcean
  • Cloudflare

Observabilidade

  • OpenTelemetry
  • Grafana Cloud
  • Grafana Alloy
  • Faro RUM
  • Pyroscope
  • alertas como código

CI/CD

  • GitHub Actions
  • deploy blue/green por tag
  • rollback pela mesma esteira

Qualidade

  • PHPUnit (902 testes)
  • PHPStan nível 5
  • Laravel Pint
  • ESLint 0 warnings
  • Playwright (6 projects)
← Voltar ao contato