Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,6 @@ public/sw.js

# Claude Code local settings / scratch
.claude/

# Playwright MCP session artifacts
.playwright-mcp/
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<div align="center">

![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)

</div>

Expand All @@ -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
Expand Down
57 changes: 57 additions & 0 deletions docs/adr/0006-observabilidade-sentry.md
Original file line number Diff line number Diff line change
@@ -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.
134 changes: 81 additions & 53 deletions docs/assets/architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading