diff --git a/.gitignore b/.gitignore index b41d1c7..7e0a1c3 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ public/sw.js # Claude Code local settings / scratch .claude/ + +# Playwright MCP session artifacts +.playwright-mcp/ diff --git a/AGENTS.md b/AGENTS.md index 950cf48..ed9787d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,6 +104,24 @@ Regras duras: - Performance de navegação não pode degradar. - Para bibliotecas pesadas, prefira lazy-load/code-split em vez de reimplementar tudo custom por reflexo. +## Observabilidade + +- Error tracking via **Sentry** (`@sentry/nextjs`). Server inicializa no `src/instrumentation.ts` + (junto da validação do keyring, sem quebrá-la) e exporta `onRequestError`; client no + `src/instrumentation-client.ts`. Ligado **só em produção** (`NODE_ENV === 'production'`), com + `sendDefaultPii: false` — não envie dados da usuária. +- Os eventos vão pelo tunnel same-origin **`/monitoring`** (`tunnelRoute`). Se mexer no matcher do + `src/proxy.ts`, mantenha `api/health` e `monitoring` na allowlist — senão o tunnel e o health + check quebram (redirect para login). A CSP (`connect-src 'self'`) já cobre o tunnel. +- Catches que engolem erro de propósito (ex.: os `safeRecalculateHourBankFor*` do `hour-bank`) + devem reportar ao Sentry (`captureException`) sem deixar de engolir — falha silenciosa não pode + ficar invisível. +- **Health check:** `GET /api/health` (público, `SELECT 1`) alimenta o probe do App Service + (`healthCheckPath = /api/health`) e o uptime monitor do Sentry. Use `await connection()` em rotas + dinâmicas que dependem de request time (com `cacheComponents`, `force-dynamic` é proibido). +- Segredos: `NEXT_PUBLIC_SENTRY_DSN` é público (build-arg, inlinado no bundle); `SENTRY_AUTH_TOKEN` + é segredo, só docker build secret (upload de source maps). Ver ADR 0006. + ## Design System - Use OKLCH nos tokens de cor. diff --git a/README.md b/README.md index 7b438d1..dc7d189 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,13 @@ sem rede** — o registro entra numa fila local e sincroniza sozinho quando a co | 🗂️ | **Histórico auditável** | Edição de registros com trilha de auditoria; filtros por projeto e atividade sem deslocar a UI. | | 🔐 | **Integridade dos registros** | Cada sessão fechada carrega um **HMAC-SHA256** (versionado por `keyId` quando o keyring está configurado, como em produção); `/api/integrity` distingue formato inválido, adulteração e uma chave histórica indisponível. | | 📲 | **PWA instalável** | Service worker (Serwist), ícone/manifest e experiência de app nativo no celular. | +| 🔭 | **Observabilidade** | Erros de produção (server e client) capturados no **Sentry** com alerta; `/api/health` alimenta o health check do App Service e o uptime monitor. Ver [`docs/adr/0006`](docs/adr/). | ## Arquitetura
-![Arquitetura do ArchTime](docs/assets/architecture.svg) +![Arquitetura do ArchTime: cliente PWA offline-first (Serwist, IndexedDB) fala por HTTPS com o archtime.app na Azure App Service (Next.js 16 App Router, API Routes, Prisma 7 WASM), que acessa o Supabase Postgres com Auth Google OAuth em sa-east-1. Erros server e client são reportados ao Sentry via tunnel /monitoring, com /api/health e source maps por release. Entrega contínua: push na main dispara o GitHub Actions (build-image, source maps), publica a imagem no ghcr.io e um webhook aciona o pull e deploy na Azure.](docs/assets/architecture.svg)
@@ -56,7 +57,7 @@ sem rede** — o registro entra numa fila local e sincroniza sozinho quando a co | **Cliente** | PWA · React 19 · Serwist (service worker) · IndexedDB (`idb`) · Tailwind 4 + shadcn/ui + Radix | | **Servidor** | Next.js 16 App Router · `output: standalone` + PPR · API Routes · Prisma 7 (query compiler **WASM**) | | **Dados** | Supabase PostgreSQL (`sa-east-1`) · Supabase Auth (Google OAuth) | -| **Infra** | Azure App Service (container Linux, Brazil South) · `ghcr.io` · GitHub Actions | +| **Infra** | Azure App Service (container Linux, Brazil South) · `ghcr.io` · GitHub Actions · Sentry (observabilidade) | Princípio central: **toda escrita passa por uma API Route** — nenhum Client Component escreve direto no banco. O modelo de confiança de origem (CSRF via header `Host`/`Origin`) está detalhado diff --git a/docs/adr/0006-observabilidade-sentry.md b/docs/adr/0006-observabilidade-sentry.md new file mode 100644 index 0000000..2dd9ab0 --- /dev/null +++ b/docs/adr/0006-observabilidade-sentry.md @@ -0,0 +1,57 @@ +# ADR 0006 — Observabilidade com Sentry + health check + +**Data:** 2026-07-10 · **Status:** Aceito + +## Contexto + +Um HTTP 500 no clock-out em produção ficou invisível até a usuária principal reclamar que não +conseguia encerrar a sessão. O app não tinha **nenhuma** captura de erro (`grep` de +`console.error`/`captureException` nas rotas de API retornava zero) nem health check +(`healthCheckPath` do App Service estava vazio). A cegueira a falhas de produção era o risco +operacional #1 da auditoria: o próximo erro também chegaria por reclamação, não por alerta. + +## Decisão + +Integrar **`@sentry/nextjs`** (plano Team patrocinado via GitHub Student Pack) para error tracking +server e client, e expor um health check. + +- **Server:** `Sentry.init` no `src/instrumentation.ts` (`register()`), preservando a validação do + keyring de HMAC no boot. `onRequestError = captureRequestError` cobre route handlers, server + actions e RSC — inclui o 500 de clock-out que originou esta decisão. +- **Client:** `src/instrumentation-client.ts` (convenção do Next 16) + `onRouterTransitionStart`. +- **`enabled` só em produção** (`NODE_ENV === 'production'`) e **`sendDefaultPii: false`** — nenhum + dado da usuária (IP, headers, corpo) sai do app. +- **Tunnel:** `tunnelRoute: '/monitoring'` roteia os eventos pelo próprio domínio (same-origin) — + evita ad-blockers e mantém a CSP intacta (`connect-src 'self'`). Exige que `api/health` e + `monitoring` fiquem na allowlist do matcher do `src/proxy.ts`, senão são redirecionados ao login. +- **Falhas silenciosas por design viram visíveis:** os recálculos fail-safe do `hour-bank` + (`safeRecalculateHourBankFor*`) engolem o erro de propósito (o cache é derivado e roda após o + commit da mutação primária); agora reportam ao Sentry **sem** deixar de engolir. Era exatamente a + classe de falha que o `onRequestError` (que só captura erro que propaga) não pegaria. +- **Source maps:** subidos no build da imagem quando `SENTRY_AUTH_TOKEN` (build secret) está + presente; pulados no CI e no dev sem o token. O `release` é o SHA do commit (`SENTRY_RELEASE`), + já que o `.git` não entra no contexto do build Docker. +- **Segredos:** `NEXT_PUBLIC_SENTRY_DSN` é público (inlinado no bundle em build time, via build-arg); + `SENTRY_AUTH_TOKEN` é segredo, entra só como docker build secret (não persiste na imagem). +- **Health check:** `GET /api/health` (público, `SELECT 1` no banco) para o probe do App Service e o + uptime monitor do Sentry. Usa `await connection()` (não `force-dynamic`, incompatível com + `cacheComponents`). O App Service passa a apontar `healthCheckPath = /api/health`. + +## Consequências + +- **+** Erros de produção passam a gerar alerta em vez de esperar reclamação. +- **+** As falhas silenciosas do `hour-bank` ficam observáveis. +- **+** O App Service recicla a instância se ela travar (antes o `healthCheckPath` era vazio). +- **−** Com 1 instância B1, um banco fora do ar por tempo prolongado (`/api/health` = 503) pode levar + a Azure a reciclar a instância; trade-off aceitável para um app pessoal. +- O plano patrocinado (50k erros/mês, 5 TB logs, 1 uptime monitor) vale enquanto o mantenedor for + estudante; em **2027-07-10** rebaixa para o plano Developer grátis (5k erros/mês), suficiente para + um app de uma usuária. Nenhuma ação necessária na virada. + +## Alternativas rejeitadas + +- **Datadog** (Pro grátis por 2 anos no Student Pack): observabilidade completa, mas pesado demais + para um app de uma usuária e caro quando os 2 anos acabam. Guardado para se o produto crescer. +- **Azure Application Insights** (nativo do App Service, coberto por créditos): ótimo para APM/infra, + mas a DX de error tracking + alerta é mais fraca que a do Sentry para o problema em questão. +- **Self-host** (OpenTelemetry + coletor): custo operacional injustificável nesta fase. diff --git a/docs/assets/architecture.svg b/docs/assets/architecture.svg index 9e5782a..3509344 100644 --- a/docs/assets/architecture.svg +++ b/docs/assets/architecture.svg @@ -1,13 +1,13 @@ - + - + - + - + + - RUNTIME + RUNTIME - - Cliente · PWA - mobile-first, offline-first - - Serwist service worker - - IndexedDB · fila offline + + Cliente · PWA + mobile-first, offline-first + + Serwist service worker + + IndexedDB · fila offline - - archtime.app - Azure App Service · container · Brazil South - - Next.js 16 · App Router · standalone (PPR) - - API Routes - - Prisma 7 · WASM + + archtime.app + Azure App Service · container · Brazil South + + Next.js 16 · App Router · standalone (PPR) + + API Routes + + Prisma 7 · WASM - - Supabase - região sa-east-1 - - PostgreSQL - - Auth · Google OAuth + + Supabase + região sa-east-1 + + PostgreSQL + + Auth · Google OAuth - - - HTTPS - TLS + + + HTTPS + TLS + + + + erros + + Observabilidade + Sentry · error tracking (server + client) + + erros → /monitoring + + /api/health + + + + request + + telemetria + + CI/CD - ENTREGA CONTÍNUA - + ENTREGA CONTÍNUA + + + + + source maps · SHA - - - push → main - GitHub + + + push → main + GitHub - + - - GitHub Actions - build-image · docker + + GitHub Actions + build-image · source maps - + - - ghcr.io - imagem :latest (pública) + + ghcr.io + imagem :latest (pública) - + - - webhook → Azure - pull & deploy + + webhook → Azure + pull & deploy diff --git a/docs/supabase-security-checklist.md b/docs/supabase-security-checklist.md index 65119b8..26d7cae 100644 --- a/docs/supabase-security-checklist.md +++ b/docs/supabase-security-checklist.md @@ -30,6 +30,9 @@ chave ativa e armazenar um segredo aleatório por `keyId`. `ENTRY_HASH_SECRET` permanece apenas durante a janela de rollout/rollback compatível. - Manter `ALLOWED_EMAILS` como lista explícita de usuários permitidos. +- Observabilidade (ADR 0006): `NEXT_PUBLIC_SENTRY_DSN` é público (build-arg, inlinado no bundle); + `SENTRY_AUTH_TOKEN` é segredo e entra só como docker build secret (upload de source maps) — nunca + na imagem nem em runtime. ## Regime de escrita (RLS)