@luanpdd/kit-mcp 1.8.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +97 -1
  3. package/gates/golden-signals-coverage.md +133 -0
  4. package/gates/obs-agents-mcp-supabase.md +86 -0
  5. package/gates/obs-skills-frontmatter.md +76 -0
  6. package/gates/omm-no-regression.md +83 -0
  7. package/gates/postmortem-template-required.md +127 -0
  8. package/gates/prr-checklist-coverage.md +128 -0
  9. package/gates/skill-must-include.md +21 -19
  10. package/kit/agents/burn-rate-forecaster.md +160 -0
  11. package/kit/agents/golden-signals-instrumenter.md +241 -0
  12. package/kit/agents/incident-investigator.md +245 -0
  13. package/kit/agents/observability-instrumenter.md +200 -0
  14. package/kit/agents/omm-auditor.md +251 -0
  15. package/kit/agents/postmortem-writer.md +282 -0
  16. package/kit/agents/prr-conductor.md +288 -0
  17. package/kit/agents/slo-engineer.md +224 -0
  18. package/kit/agents/supabase-architect.md +62 -0
  19. package/kit/agents/supabase-auth-bootstrapper.md +17 -0
  20. package/kit/agents/supabase-edge-fn-writer.md +124 -0
  21. package/kit/agents/supabase-migration-writer.md +98 -0
  22. package/kit/agents/supabase-realtime-implementer.md +23 -0
  23. package/kit/agents/supabase-rls-writer.md +17 -0
  24. package/kit/agents/supabase-storage-implementer.md +174 -0
  25. package/kit/agents/toil-auditor.md +277 -0
  26. package/kit/commands/auditar-marco.md +102 -1
  27. package/kit/commands/auditar-observabilidade.md +103 -0
  28. package/kit/commands/auditar-toil.md +129 -0
  29. package/kit/commands/burn-rate-status.md +140 -0
  30. package/kit/commands/concluir-marco.md +73 -1
  31. package/kit/commands/definir-slo.md +108 -0
  32. package/kit/commands/discutir-fase.md +26 -0
  33. package/kit/commands/forense.md +83 -1
  34. package/kit/commands/golden-signals.md +142 -0
  35. package/kit/commands/instrumentar-fase.md +200 -0
  36. package/kit/commands/investigar-producao.md +162 -0
  37. package/kit/commands/observabilidade.md +116 -0
  38. package/kit/commands/planejar-fase.md +20 -0
  39. package/kit/commands/postmortem.md +179 -0
  40. package/kit/commands/prr.md +205 -0
  41. package/kit/commands/risk-budget.md +220 -0
  42. package/kit/commands/sre.md +227 -0
  43. package/kit/commands/verificar-trabalho.md +26 -0
  44. package/kit/skills/_shared-observability/glossary.md +396 -0
  45. package/kit/skills/_shared-sre/glossary.md +573 -0
  46. package/kit/skills/blameless-postmortems/SKILL.md +340 -0
  47. package/kit/skills/burn-rate-alerting/SKILL.md +258 -0
  48. package/kit/skills/core-analysis-loop/SKILL.md +352 -0
  49. package/kit/skills/distributed-tracing/SKILL.md +362 -0
  50. package/kit/skills/eliminating-toil/SKILL.md +243 -0
  51. package/kit/skills/event-based-slos/SKILL.md +296 -0
  52. package/kit/skills/four-golden-signals/SKILL.md +297 -0
  53. package/kit/skills/observability-driven-development/SKILL.md +315 -0
  54. package/kit/skills/observability-maturity-model/SKILL.md +222 -0
  55. package/kit/skills/opentelemetry-standard/SKILL.md +351 -0
  56. package/kit/skills/production-readiness-review/SKILL.md +305 -0
  57. package/kit/skills/sre-risk-management/SKILL.md +221 -0
  58. package/kit/skills/structured-events/SKILL.md +265 -0
  59. package/kit/skills/telemetry-pipelines/SKILL.md +259 -0
  60. package/kit/skills/telemetry-sampling/SKILL.md +256 -0
  61. package/package.json +1 -1
@@ -178,8 +178,132 @@ Test local:
178
178
  - Função existente que precisa de pequeno ajuste → use Edit direto
179
179
  - Lógica que pode rodar em DB function (`security definer`) → considera `supabase-database-functions` (mais barato que Edge)
180
180
 
181
+ ## Observabilidade integrada
182
+
183
+ Edge Function nasce instrumentada com OTel — não é addon. Beneficia mais que qualquer outro agent dado que é entry-point externo.
184
+
185
+ 1. **OTel SDK no topo do `index.ts`** (skill [`opentelemetry-standard`](../skills/opentelemetry-standard/SKILL.md)):
186
+ ```ts
187
+ import { trace } from 'npm:@opentelemetry/api@1.9.0'
188
+ import { NodeSDK } from 'npm:@opentelemetry/sdk-node@0.55.0'
189
+ import { OTLPTraceExporter } from 'npm:@opentelemetry/exporter-trace-otlp-http@0.55.0'
190
+ const sdk = new NodeSDK({ /* service.name, OTLP endpoint */ })
191
+ sdk.start()
192
+ ```
193
+ 2. **Span por handler** com kind `SERVER` envolvendo `Deno.serve`. Atributos canônicos: `request.id`, `user.id`, `tenant_id`, `endpoint`, `result.success`, `error.type`, `build_id` (`Deno.env.get('SUPABASE_GIT_SHA')`) — skill [`structured-events`](../skills/structured-events/SKILL.md).
194
+ 3. **Context propagation** via header `traceparent` para outbound calls a Postgres/PostgREST/external (skill [`distributed-tracing`](../skills/distributed-tracing/SKILL.md)).
195
+ 4. **Sampling head-based** baseado em `customer.tier` ou `feature_flag.<name>` (skill [`telemetry-sampling`](../skills/telemetry-sampling/SKILL.md) *Phase 34*) — 100% errors, 100% enterprise, 10% baseline.
196
+
197
+ **Output adicionado:** template completo de Edge Function inclui SDK setup + span wrapper + propagação outbound + classificador de error.type. ODD-compliant (4 perguntas pré-PR endereçadas).
198
+
199
+ ## Four Golden Signals
200
+
201
+ > Cross-ref canônico: [four-golden-signals](../skills/four-golden-signals/SKILL.md) (cap 6 do livro Google SRE — Monitoring Distributed Systems). Para retro-instrumentar Edge Function existente, delegar para [golden-signals-instrumenter](./golden-signals-instrumenter.md).
202
+
203
+ Edge Function user-facing nasce com os 4 sinais dourados — não é addon. O bloco `## Observabilidade integrada` acima cobre OTel SDK + spans + propagation; este bloco especifica os **4 instrumentos canônicos** que o template gerado SEMPRE inclui:
204
+
205
+ | Signal | Instrumento | Dimensão | Valor padrão |
206
+ |---|---|---|---|
207
+ | **Latency** | `meter.createHistogram('http_request_duration_ms')` com `explicitBucketBoundaries: [1,2,5,10,25,50,100,250,500,1000,2500,5000,10000,30000]` | `result=success\|error` (separar success de erro) | Bucketing exponencial captura long tail sem cardinality explosion |
208
+ | **Traffic** | `meter.createCounter('http_requests_total')` | `endpoint`, `http_method` | Incrementado antes de processar request |
209
+ | **Errors** | `meter.createCounter('http_errors_total')` | `error.type` enum (5-15 valores: `timeout\|validation\|auth\|rate_limit\|db\|provider_down\|...`) — **nunca** `error.message` (cardinalidade explode) | Incrementado em catch + path 4xx/5xx |
210
+ | **Saturation** | `meter.createObservableGauge('saturation_pct')` com callback que lê estado real | resource-specific: `connection_pool` (pg) / `concurrency_limit` (Edge runtime) / `egress_bandwidth` / `cache_memory` | % do recurso mais escasso identificado ANTES de instrumentar |
211
+
212
+ ### Snippet canônico — adicionado ao topo do `index.ts` gerado
213
+
214
+ ```ts
215
+ // PT-BR: 4 golden signals — instrumentação mínima universal
216
+ import { metrics } from 'npm:@opentelemetry/api@1.9.0'
217
+ const meter = metrics.getMeter('<function_name>')
218
+
219
+ // 1. LATENCY — histogram bucketed exponencial
220
+ const latencyHistogram = meter.createHistogram('http_request_duration_ms', {
221
+ description: 'Edge function latency split by result (success vs error)',
222
+ unit: 'ms',
223
+ advice: { explicitBucketBoundaries: [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000] }
224
+ })
225
+
226
+ // 2. TRAFFIC — counter de requests recebidos
227
+ const trafficCounter = meter.createCounter('http_requests_total', {
228
+ description: 'Total HTTP requests received by edge function'
229
+ })
230
+
231
+ // 3. ERRORS — counter por error.type (NUNCA error.message — cardinalidade)
232
+ const errorsCounter = meter.createCounter('http_errors_total', {
233
+ description: 'Edge function errors by error.type enum'
234
+ })
235
+
236
+ // 4. SATURATION — gauge do recurso mais escasso (callback lê estado real)
237
+ // PT-BR: para Edge Function default, saturation = concurrency_limit_used %
238
+ // Substituir callback conforme recurso identificado (db pool, queue, cache)
239
+ meter.createObservableGauge('saturation_pct', {
240
+ description: 'Saturation of scarcest resource — function-specific'
241
+ }).addCallback((result) => {
242
+ // PT-BR: callback canônico — ler estado real (ex: SELECT count(*) FROM pg_stat_activity)
243
+ // Aqui placeholder: 0 < value < 1
244
+ result.observe(getSaturationPct()) // implementar conforme resource
245
+ })
246
+ ```
247
+
248
+ ### Wrapping no handler
249
+
250
+ ```ts
251
+ Deno.serve(async (req: Request) => {
252
+ const start = performance.now()
253
+ const endpoint = new URL(req.url).pathname
254
+ trafficCounter.add(1, { endpoint, http_method: req.method })
255
+
256
+ try {
257
+ const response = await handle(req)
258
+ latencyHistogram.record(performance.now() - start, {
259
+ endpoint,
260
+ result: response.ok ? 'success' : 'error',
261
+ })
262
+ if (!response.ok) {
263
+ errorsCounter.add(1, { endpoint, 'error.type': classifyError(response) })
264
+ }
265
+ return response
266
+ } catch (err) {
267
+ latencyHistogram.record(performance.now() - start, { endpoint, result: 'error' })
268
+ errorsCounter.add(1, { endpoint, 'error.type': classifyError(err) })
269
+ throw err
270
+ }
271
+ })
272
+
273
+ // PT-BR: classifyError DEVE retornar enum fechado, não err.message
274
+ function classifyError(e: unknown): string {
275
+ if (e instanceof TimeoutError) return 'timeout'
276
+ if (e instanceof ValidationError) return 'validation'
277
+ if (e instanceof AuthError) return 'auth'
278
+ // ... 5-15 valores no total
279
+ return 'unknown'
280
+ }
281
+ ```
282
+
283
+ ### Saturation por tipo de Edge Function
284
+
285
+ | Tipo de função | Recurso mais escasso | Implementação típica |
286
+ |---|---|---|
287
+ | API simples (GET/POST com leitura DB) | `pg_pool` connections used | `select count(*) from pg_stat_activity where state = 'active'` |
288
+ | RAG / embeddings | `concurrency_limit` (provider externo) | counter de requests in-flight |
289
+ | Email / queue consumer (cron → pgmq) | `pgmq.queue_length` | `select msg_count from pgmq.metrics_<queue>` |
290
+ | Storage I/O heavy (uploads grandes) | `egress_bandwidth` | bytes-out tracker em window |
291
+
292
+ ### Anti-patterns prevenidos
293
+
294
+ - Errors counter usando `error.type = err.message` → SEMPRE enum fechado (5-15 valores)
295
+ - Latency mistura success + error → SEMPRE `result` dimension separa
296
+ - Mean latency em vez de histogram → SEMPRE histogram com percentis derivados em backend
297
+ - Saturation genérico (CPU%) sem identificar recurso real → SEMPRE escolher recurso scarcest da função
298
+
181
299
  ## Ver também
182
300
 
183
301
  - [supabase-edge-functions](../skills/supabase-edge-functions/SKILL.md) — base de conhecimento canônica
184
302
  - [supabase-cron-queues](../skills/supabase-cron-queues/SKILL.md) — pattern `cron → pgmq → Edge Function`
185
303
  - [supabase-auth-ssr](../skills/supabase-auth-ssr/SKILL.md) — clients Supabase
304
+ - [opentelemetry-standard](../skills/opentelemetry-standard/SKILL.md) — SDK setup para Deno
305
+ - [distributed-tracing](../skills/distributed-tracing/SKILL.md) — context propagation
306
+ - [structured-events](../skills/structured-events/SKILL.md) — campos canônicos
307
+ - [observability-driven-development](../skills/observability-driven-development/SKILL.md) — 4 perguntas pré-PR
308
+ - [four-golden-signals](../skills/four-golden-signals/SKILL.md) — 4 sinais canônicos (Latency, Traffic, Errors, Saturation) cap 6 livro Google SRE
309
+ - [golden-signals-instrumenter](./golden-signals-instrumenter.md) — agent que retro-instrumenta Edge Functions existentes com os 4 signals
@@ -154,3 +154,101 @@ Próximos passos:
154
154
  - `auth.uid()` sem `(select)` → SEMPRE wrapper
155
155
  - Schema-qualifier ausente em DB functions → SEMPRE `public.<name>`
156
156
  - Comandos destrutivos sem comentário → BLOQUEIA até user adicionar Risk/Validation/Rollback
157
+
158
+ ## Observabilidade integrada
159
+
160
+ Toda migration emite evento estruturado e cria audit hooks por default — não é addon, é parte do contrato (skill [`observability-driven-development`](../skills/observability-driven-development/SKILL.md)).
161
+
162
+ 1. **Migration event** (auto-gerado no fim da migration):
163
+ ```sql
164
+ -- PT-BR: emite linha em observability.migration_events
165
+ insert into observability.migration_events (
166
+ migration_id, sql_hash, applied_at, build_id, result_success, duration_ms
167
+ ) values (
168
+ '20260506120000_create_orders', md5(...), now(), '{{BUILD_ID}}', true, {{ELAPSED_MS}}
169
+ );
170
+ ```
171
+ 2. **Audit triggers em tabelas sensíveis** (pagamentos, auth, dados pessoais): trigger `after insert/update/delete` que insere `audit_log` com `tenant_id`, `user_id`, `op`, `old_row`, `new_row`, `actor`, `timestamp`.
172
+ 3. **Atributos canônicos** em qualquer função criada: `set search_path = ''` + comments com `result.success`, `error.type` enum esperado (skill [`structured-events`](../skills/structured-events/SKILL.md)).
173
+
174
+ **Output adicionado:** seção "## Audit hooks" + "## Migration event emit" no SQL gerado, comentadas em PT-BR.
175
+
176
+ ## Alerta toil — automação via pg_cron
177
+
178
+ > Cross-ref canônico: [eliminating-toil](../skills/eliminating-toil/SKILL.md) (cap 5 do livro Google SRE — Eliminating Toil). Para auditoria sistemática de toil em todo o repo, delegar para [toil-auditor](./toil-auditor.md).
179
+
180
+ Migrations SQL executadas **manualmente em cadência regular** (rebuild índice, VACUUM, REFRESH MATERIALIZED VIEW, ANALYZE) são toil canônico — passam todos os 6 critérios: manual, repetitivo, automatizável, tático, sem valor durável, escala linear. Este agent **detecta padrões de toil** ao escrever migration e **alerta proativamente** sugerindo automação via `pg_cron`.
181
+
182
+ ### 6 critérios — quando uma migration é toil-prone
183
+
184
+ Migration descreve operação que será re-executada > 1× = toil-prone. Aplicar 6 critérios da skill `eliminating-toil`:
185
+
186
+ | Critério | Pergunta | Sinal de toil |
187
+ |---|---|---|
188
+ | 1. Manual | Operador roda `psql` ou aplica migration "quando lembra"? | Sim |
189
+ | 2. Repetitivo | Já foi executada 3+ vezes em milestones diferentes? | Sim |
190
+ | 3. Automatizável | `pg_cron` consegue agendar sem julgamento humano? | Sim |
191
+ | 4. Tático | Reage a sintoma (lentidão, bloat, stale view) sem planejar? | Sim |
192
+ | 5. Sem valor durável | Não cria asset permanente — só "limpa" estado | Sim |
193
+ | 6. Escala linear | Mais users / mais dados = mais frequência manual | Sim |
194
+
195
+ Se TODOS os 6 = sim → **toil**. Bloquear migration manual recorrente; oferecer alternativa via `pg_cron`.
196
+
197
+ ### Padrões SQL canônicos que SEMPRE disparam alerta toil
198
+
199
+ | Operação manual | Por quê é toil | Automação canônica |
200
+ |---|---|---|
201
+ | `REINDEX TABLE x` recorrente (a cada N semanas) | Rebuild de bloat de índice é tático, sem valor durável, repetitivo | `select cron.schedule('reindex_x', '0 3 * * 0', $$reindex table x$$);` (semanal 3am) |
202
+ | `VACUUM ANALYZE x` manual | autovacuum não está acompanhando — sintoma de tuning, não fix manual | Tunar `autovacuum_vacuum_scale_factor` para tabela específica + `pg_cron` se necessário |
203
+ | `REFRESH MATERIALIZED VIEW x` manual | Stale view detectada por user reclamação ou alert | `select cron.schedule('refresh_x', '*/30 * * * * *', $$refresh materialized view concurrently x$$);` |
204
+ | `ANALYZE` em tabela após bulk insert manual | Estatísticas desatualizadas após ETL — bem conhecido | Trigger AFTER INSERT/COPY com `analyze` no fim do batch, ou `pg_cron` pós-ETL |
205
+ | `delete from logs where created_at < now() - interval '90d'` manual recorrente | Retention manual = toil clássico | `select cron.schedule('purge_logs', '0 4 * * *', $$delete from logs where ...$$);` |
206
+ | `dump + restore` periódico para estatísticas / planos cache | Operação repetitiva sem valor permanente | `pg_cron` job ou `pg_stat_reset_*()` calls automatizadas |
207
+
208
+ ### Snippet canônico — converter manual em pg_cron
209
+
210
+ ```sql
211
+ -- PT-BR: ANTES — toil (operador roda manualmente)
212
+ -- $ psql -c 'reindex table heavy_table;' ← repetir a cada 2 semanas
213
+
214
+ -- PT-BR: DEPOIS — automação via pg_cron (necessita extension pg_cron habilitada)
215
+ create extension if not exists pg_cron;
216
+
217
+ select cron.schedule(
218
+ 'reindex_heavy_table_biweekly',
219
+ '0 3 1,15 * *', -- 3am dias 1 e 15
220
+ $$ reindex table public.heavy_table $$
221
+ );
222
+
223
+ -- PT-BR: monitor — falha em job pg_cron emite linha em cron.job_run_details
224
+ -- alimentar alerta SLO se job falha 3+ vezes seguidas
225
+ ```
226
+
227
+ ### Quando NÃO automatizar (não é toil)
228
+
229
+ - **Migration de schema (DDL one-shot)** — `create table`, `alter table add column` são project work, não toil. Não recorrentes.
230
+ - **Backfill data único** — `update orders set status = ...` aplicado 1× para corrigir bug é grungy work, não toil.
231
+ - **Rebuild que requer julgamento** — `reindex` que requer escolher hora baseada em load patterns variáveis, ou que precisa coordenação com release. Mantém manual mas documenta runbook.
232
+
233
+ ### Output do agent — adicionado ao SQL gerado
234
+
235
+ Quando o agent detecta que a migration descreve operação toil-prone (regex em DDL: `reindex|vacuum|refresh materialized|delete from .* interval`), adiciona comentário-alerta no header do arquivo SQL gerado:
236
+
237
+ ```sql
238
+ /*
239
+ ⚠ TOIL ALERT — esta operação parece recorrente.
240
+
241
+ Se será executada em cadência regular, considere automação via pg_cron:
242
+ select cron.schedule('<job_name>', '<schedule>', $$ <sql> $$);
243
+
244
+ Cross-ref: kit/skills/eliminating-toil/SKILL.md (6 critérios canônicos)
245
+ kit/agents/toil-auditor.md (audit sistemático para repo todo)
246
+ */
247
+ ```
248
+
249
+ ### Anti-patterns prevenidos
250
+
251
+ - "Roda quando der" runbook → SEMPRE pg_cron + monitoring de falha do job
252
+ - `pg_cron` schedule mas sem alerta de falha → SEMPRE incluir SLO em `cron.job_run_details` (% sucesso 30d)
253
+ - Automação parcial (script humano-iniciado) → ainda é toil (humano pressiona botão); preferir cron.schedule completo
254
+ - Migration manual recorrente "porque é só uma vez por mês" → 12×/ano = toil, regra ≤ 50% se acumular vários "só um por mês"
@@ -245,8 +245,31 @@ PRÓXIMOS PASSOS
245
245
  - Presence para listas de objetos → ALERTA explícito (use queries normais)
246
246
  - Naming inconsistente → SEMPRE `scope:entity:id`
247
247
 
248
+ ## Observabilidade integrada
249
+
250
+ Realtime é tipicamente fora-de-trace porque WebSocket não usa header `traceparent` por default. Patches:
251
+
252
+ 1. **Trace context no payload do broadcast** (skill [`distributed-tracing`](../skills/distributed-tracing/SKILL.md)):
253
+ ```ts
254
+ // PT-BR: producer — anexa traceparent ao payload do broadcast
255
+ const carrier: Record<string, string> = {}
256
+ propagation.inject(context.active(), carrier)
257
+ await channel.send({
258
+ type: 'broadcast',
259
+ event: 'message_inserted',
260
+ payload: { ...originalPayload, _trace_context: carrier }
261
+ })
262
+ ```
263
+ 2. **Consumer extrai contexto** ao receber broadcast e abre span filho — stitching cross-WebSocket fica completo.
264
+ 3. **Atributos canônicos** em todo span de subscribe/unsubscribe (skill [`structured-events`](../skills/structured-events/SKILL.md)): `channel.name`, `channel.private`, `subscribe.status` (`SUBSCRIBED` | `CHANNEL_ERROR` | `TIMED_OUT`), `user.id`, `tenant_id`.
265
+ 4. **Trigger DB** (`realtime.broadcast_changes`) emite evento estruturado em `observability.events` com `event_name = 'realtime_broadcast'`, `result_success`, `tenant_id`.
266
+
267
+ **Output adicionado:** template inclui propagation.inject no payload + span wrapper em subscribe + atributos canônicos no callback.
268
+
248
269
  ## Ver também
249
270
 
250
271
  - [supabase-realtime](../skills/supabase-realtime/SKILL.md) — base de conhecimento canônica
251
272
  - [supabase-rls-writer](./supabase-rls-writer.md) — invocar para policies adicionais em tabelas do app
252
273
  - [supabase-database-functions](../skills/supabase-database-functions/SKILL.md) — trigger function pattern
274
+ - [distributed-tracing](../skills/distributed-tracing/SKILL.md) — context propagation cross-WebSocket
275
+ - [structured-events](../skills/structured-events/SKILL.md) — atributos canônicos para channels
@@ -212,7 +212,24 @@ NOTAS
212
212
  - Tabela já tem policies estabelecidas e user só quer 1 ajuste pequeno → use Edit direto
213
213
  - Tabela é puramente read-only para `anon` (ex: catalog público) → policy trivial, overhead
214
214
 
215
+ ## Observabilidade integrada
216
+
217
+ RLS denials são sinal de segurança e debug — emite evento estruturado SEMPRE.
218
+
219
+ 1. **RLS deny logging**: no entry-point do app (Edge Function ou backend), capturar `42501 insufficient_privilege` errors e emitir span com:
220
+ - `policy.name` (qual policy negou)
221
+ - `attempted_op` (`select` | `insert` | `update` | `delete`)
222
+ - `user.id` (de `auth.uid()` na sessão)
223
+ - `tenant_id` (de `app_metadata` quando aplicável)
224
+ - `resource.table` (qual tabela/view tentada)
225
+ - `error.type = 'authz'` (skill [`structured-events`](../skills/structured-events/SKILL.md))
226
+ 2. **Investigação via Core Analysis Loop** (skill [`core-analysis-loop`](../skills/core-analysis-loop/SKILL.md)): pergunta canônica "qual policy + qual tenant + qual op + quando começou?" → query agrupando por essas 4 dimensões para identificar pattern.
227
+
228
+ **Output adicionado:** seção "## Observability hooks" com snippet de error handler que classifica RLS denial e emite span.
229
+
215
230
  ## Ver também
216
231
 
217
232
  - [supabase-rls-policies](../skills/supabase-rls-policies/SKILL.md) — base de conhecimento canônica das regras
218
233
  - [supabase-migration-writer](./supabase-migration-writer.md) — invocar quando user quer policies dentro de migration nova
234
+ - [structured-events](../skills/structured-events/SKILL.md) — campos canônicos para RLS denial logging
235
+ - [core-analysis-loop](../skills/core-analysis-loop/SKILL.md) — investigar denial patterns
@@ -233,8 +233,182 @@ ALERTAS
233
233
  - **Vector Buckets / Analytics Buckets** ainda alpha em 2026-05-06 — não detalhar
234
234
  - **Smart CDN** para egress optimization — fora deste agent (config no Dashboard)
235
235
 
236
+ ## Observabilidade integrada
237
+
238
+ Upload events são quentes em custo (egress + storage) e em UX (lentidão de upload = abandono). Instrumentar SEMPRE.
239
+
240
+ 1. **Span por upload/download** (skill [`structured-events`](../skills/structured-events/SKILL.md)) com atributos:
241
+ - `bucket.name`, `bucket.public` (bool)
242
+ - `file.size_bytes`, `file.mime_type`, `file.path`
243
+ - `operation`: `upload` | `download` | `signed_url` | `delete`
244
+ - `result.success`, `error.type` (enum: `quota_exceeded`, `unauthorized`, `mime_blocked`, `size_exceeded`, `network`)
245
+ - `duration_ms`, `transfer.bytes_per_second` (calculado)
246
+ - `user.id`, `tenant_id` (do `auth.uid()`)
247
+ 2. **Sampling** (skill [`telemetry-sampling`](../skills/telemetry-sampling/SKILL.md) *Phase 34*): 100% errors, 100% uploads > 10 MB (cardinalidade baixa, valor alto), 5% baseline para downloads pequenos (alto volume).
248
+ 3. **Audit log** para uploads em buckets sensíveis (`audit_log` table com `actor`, `op`, `resource`, `geo`, `user_agent`).
249
+
250
+ **Output adicionado:** seção "## Observability hooks" com snippet de upload/download wrapper.
251
+
252
+ ## Saturation signal — bucket size + quota
253
+
254
+ > Cross-ref canônico: [four-golden-signals](../skills/four-golden-signals/SKILL.md) (cap 6 do livro Google SRE — Monitoring Distributed Systems). Para retro-instrumentar storage existente com os 4 signals, delegar para [golden-signals-instrumenter](./golden-signals-instrumenter.md).
255
+
256
+ Storage tem o **recurso mais escasso explícito**: o quota do plano (Free 1 GB, Pro 100 GB, Team 1 TB, etc.). Sem signal de saturation, time descobre quota exhaustion via incident (uploads falham silenciosamente em UX) — **anti-pattern clássico** de white-box monitoring sem detecção precoce. O bloco `## Observabilidade integrada` acima cobre Latency / Traffic / Errors (3 signals); este bloco completa com **Saturation** — o 4º signal canônico.
257
+
258
+ ### Saturation = bucket size ÷ quota plan
259
+
260
+ | Plano | Quota total | Threshold ALERT (yellow) | Threshold PAGE (red) |
261
+ |---|---|---|---|
262
+ | Free | 1 GB | 80% (800 MB) | 95% (950 MB) |
263
+ | Pro | 100 GB | 80% (80 GB) | 95% (95 GB) |
264
+ | Team | 1 TB | 80% (800 GB) | 95% (950 GB) |
265
+ | Enterprise | custom | custom | custom |
266
+
267
+ ### Signal 1 — Gauge: bucket size atual (bytes)
268
+
269
+ `ObservableGauge` (push periódico via callback) mede tamanho real de cada bucket. Callback consulta `storage.objects` agregado:
270
+
271
+ ```ts
272
+ // PT-BR: 4º signal — saturation (gauge de bucket size em bytes)
273
+ import { metrics } from 'npm:@opentelemetry/api@1.9.0'
274
+ const meter = metrics.getMeter('supabase-storage')
275
+
276
+ meter.createObservableGauge('storage_bucket_bytes', {
277
+ description: 'Tamanho atual em bytes por bucket — saturation signal',
278
+ unit: 'bytes',
279
+ }).addCallback(async (result) => {
280
+ // PT-BR: query agregada (rodar via service-role client em cron)
281
+ const sizes = await supabaseAdmin.rpc('storage_bucket_sizes_bytes')
282
+ // expected: [{ bucket_id: 'avatars', total_bytes: 12345678 }, ...]
283
+ for (const row of sizes ?? []) {
284
+ result.observe(row.total_bytes, { 'bucket.id': row.bucket_id })
285
+ }
286
+ })
287
+
288
+ meter.createObservableGauge('storage_saturation_pct', {
289
+ description: 'Saturation = bucket size / quota plan — % do quota usado',
290
+ unit: '1', // ratio (0..1)
291
+ }).addCallback(async (result) => {
292
+ const sizes = await supabaseAdmin.rpc('storage_bucket_sizes_bytes')
293
+ const QUOTA_BYTES = Number(Deno.env.get('SUPABASE_PLAN_QUOTA_BYTES') ?? 1_000_000_000) // default Free
294
+ for (const row of sizes ?? []) {
295
+ result.observe(row.total_bytes / QUOTA_BYTES, { 'bucket.id': row.bucket_id })
296
+ }
297
+ })
298
+ ```
299
+
300
+ SQL helper para o callback:
301
+
302
+ ```sql
303
+ -- PT-BR: function que retorna bytes por bucket — chamada por callback OTel
304
+ create or replace function public.storage_bucket_sizes_bytes()
305
+ returns table (bucket_id text, total_bytes bigint)
306
+ language sql
307
+ security definer
308
+ set search_path = ''
309
+ as $$
310
+ select bucket_id, coalesce(sum((metadata->>'size')::bigint), 0) as total_bytes
311
+ from storage.objects
312
+ group by bucket_id;
313
+ $$;
314
+ ```
315
+
316
+ ### Signal 2 — Counter: quota near-exhaustion events
317
+
318
+ `Counter` incrementa a cada upload que **detecta** approach a quota threshold (80%, 95%). Permite contar eventos críticos para alerting:
319
+
320
+ ```ts
321
+ // PT-BR: counter incrementado em cada upload
322
+ const quotaWarnings = meter.createCounter('storage_quota_warnings_total', {
323
+ description: 'Counter de eventos onde upload aproxima quota — alimentar alert SLO',
324
+ })
325
+
326
+ export async function uploadInstrumented(file: File, filename: string) {
327
+ const supabase = createClient()
328
+ const { data: { user } } = await supabase.auth.getUser()
329
+ if (!user) throw new Error('not authenticated')
330
+
331
+ const path = `${user.id}/${filename}`
332
+
333
+ // PT-BR: pre-check — saturation atual antes de upload
334
+ const sizes = await supabaseAdmin.rpc('storage_bucket_sizes_bytes')
335
+ const bucketSize = sizes?.find(s => s.bucket_id === '<bucket_name>')?.total_bytes ?? 0
336
+ const QUOTA = Number(Deno.env.get('SUPABASE_PLAN_QUOTA_BYTES') ?? 1_000_000_000)
337
+ const saturation = bucketSize / QUOTA
338
+
339
+ if (saturation >= 0.95) {
340
+ quotaWarnings.add(1, { 'bucket.id': '<bucket_name>', threshold: '95pct' })
341
+ } else if (saturation >= 0.80) {
342
+ quotaWarnings.add(1, { 'bucket.id': '<bucket_name>', threshold: '80pct' })
343
+ }
344
+
345
+ const { data, error } = await supabase.storage
346
+ .from('<bucket_name>')
347
+ .upload(path, file, { upsert: true })
348
+
349
+ if (error) throw error
350
+ return data.path
351
+ }
352
+ ```
353
+
354
+ ### Cron schedule sugerido
355
+
356
+ Saturation gauge não precisa rodar em cada request — agendar leitura via `pg_cron` (ou OTel SDK polling interval = 60s) é suficiente:
357
+
358
+ ```sql
359
+ -- PT-BR: refresh saturation cache a cada 60s para gauge OTel
360
+ create materialized view if not exists obs.storage_saturation as
361
+ select bucket_id, sum((metadata->>'size')::bigint) as total_bytes, now() as captured_at
362
+ from storage.objects
363
+ group by bucket_id;
364
+
365
+ select cron.schedule(
366
+ 'refresh_storage_saturation',
367
+ '* * * * *', -- a cada 1 min
368
+ $$ refresh materialized view concurrently obs.storage_saturation $$
369
+ );
370
+ ```
371
+
372
+ ### Alert SLO sobre saturation
373
+
374
+ Saturation alimenta SLO event-based — não threshold direto:
375
+
376
+ ```yaml
377
+ # PT-BR: SLO sobre quota — % de tempo em yellow ou worse
378
+ slo:
379
+ name: storage_quota_healthy
380
+ target: 0.99 # 99% do tempo em < 80% quota
381
+ window: 30d_sliding
382
+ sli:
383
+ type: event_based
384
+ good_event:
385
+ saturation_pct: { lt: 0.80 }
386
+ bad_event:
387
+ saturation_pct: { gte: 0.80 }
388
+ ```
389
+
390
+ ### Output do agent — adicionado ao SQL/código gerado
391
+
392
+ Quando agent gera bucket privado novo, **sempre inclui**:
393
+ 1. Function SQL `storage_bucket_sizes_bytes()` (uma vez por projeto)
394
+ 2. Materialized view `obs.storage_saturation` + pg_cron refresh job
395
+ 3. Snippet OTel ObservableGauge no código client wrapper
396
+ 4. Counter `storage_quota_warnings_total` no upload wrapper
397
+ 5. SLO `storage_quota_healthy` em `.planning/slos/<bucket>.yaml`
398
+
399
+ ### Anti-patterns prevenidos
400
+
401
+ - Saturation = "% disco do servidor" → SEMPRE saturation = % quota plan (recurso correto)
402
+ - Threshold direto em alerta CPU/memory para capacity → SEMPRE SLO event-based sobre saturation_pct
403
+ - Polling de bucket size em cada request → SEMPRE materialized view + pg_cron refresh + OTel polling 60s
404
+ - Plan quota hardcoded → SEMPRE env var `SUPABASE_PLAN_QUOTA_BYTES` (varia por plano, pode ser sobrescrita em test)
405
+
236
406
  ## Ver também
237
407
 
238
408
  - [supabase-storage](../skills/supabase-storage/SKILL.md) — base de conhecimento canônica
239
409
  - [supabase-rls-writer](./supabase-rls-writer.md) — invocar para policies adicionais
240
410
  - [supabase-auth-ssr](../skills/supabase-auth-ssr/SKILL.md) — usuário autenticado obtém `auth.uid()`
411
+ - [structured-events](../skills/structured-events/SKILL.md) — campos canônicos para upload/download events
412
+ - [telemetry-sampling](../skills/telemetry-sampling/SKILL.md) *(Phase 34)* — head-based sampling por size_bytes
413
+ - [four-golden-signals](../skills/four-golden-signals/SKILL.md) — 4 sinais canônicos (Latency, Traffic, Errors, Saturation) cap 6 livro Google SRE — saturation = bucket size / quota plan
414
+ - [golden-signals-instrumenter](./golden-signals-instrumenter.md) — agent que retro-instrumenta storage existente com os 4 signals