cloudbroker-sdk 0.5.0 → 0.7.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.
@@ -4,7 +4,16 @@ on:
4
4
  push:
5
5
  branches: [main]
6
6
  paths:
7
+ # Sem barra inicial: filtros de path do GitHub Actions são relativos à
8
+ # raiz do repo e '/package.json' não casa com nada — o workflow deixou de
9
+ # disparar por completo (o 0.6.0 foi mergeado e nunca publicado).
10
+ # 'package.json' já cobre só a raiz: sem '**', o glob não atravessa
11
+ # diretórios, que era a intenção original de ancorar.
7
12
  - 'package.json'
13
+ # Escape hatch: quando o filtro acima não dispara (foi o caso do 0.6.0, que
14
+ # entrou no main sem publicar), dá pra publicar a versão que está no main sem
15
+ # precisar de um bump artificial só pra acionar o gatilho.
16
+ workflow_dispatch:
8
17
 
9
18
  jobs:
10
19
  publish:
@@ -22,6 +31,15 @@ jobs:
22
31
  node-version: 20
23
32
  registry-url: 'https://registry.npmjs.org'
24
33
 
34
+ # Gate de teste antes do publish — este pacote roda DENTRO da aplicação
35
+ # de produção do cliente; publicar sem rodar a suíte já era um risco
36
+ # aberto (nenhum CI de teste existia neste repo).
37
+ - name: Instalar dependências de teste
38
+ run: npm ci || npm install
39
+
40
+ - name: Rodar testes
41
+ run: npm test
42
+
25
43
  - name: Publish to npm
26
44
  run: npm publish
27
45
  env:
package/README.md CHANGED
@@ -46,6 +46,62 @@ const res = await anthropic.messages.create({
46
46
  });
47
47
  ```
48
48
 
49
+ ## Uso com Gemini
50
+
51
+ ```js
52
+ import { GoogleGenAI } from "@google/genai";
53
+ import { CloudBrokerAI } from "cloudbroker-sdk";
54
+
55
+ const gemini = CloudBrokerAI.wrap(new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }), {
56
+ apiKey: "cb-key-xxxx",
57
+ product: "checkout",
58
+ team: "mobile",
59
+ });
60
+
61
+ const res = await gemini.models.generateContent({
62
+ model: "gemini-2.5-flash",
63
+ contents: "Olá!",
64
+ });
65
+ ```
66
+
67
+ O `cbLabel` pode ir no próprio params (a API do Gemini é de argumento único) ou
68
+ no segundo argumento, como nos outros wrappers. Nos dois casos ele é removido
69
+ antes de a chamada seguir pro Google:
70
+
71
+ ```js
72
+ await gemini.models.generateContent({
73
+ model: "gemini-2.5-flash", contents: "…", cbLabel: "checkout:resumo-pedido",
74
+ });
75
+ ```
76
+
77
+ ### Por que o custo do Gemini não é `input × preço + output × preço`
78
+
79
+ A conta do Gemini tem três particularidades. Ignorar qualquer uma delas erra o
80
+ valor de forma relevante, então o SDK trata as três:
81
+
82
+ | Particularidade | O que acontece se ignorar |
83
+ |---|---|
84
+ | `promptTokenCount` **já inclui** os tokens servidos de cache, que custam ~10x menos | superfatura a parcela cacheada (1,65x a mais num prompt 80% cacheado) |
85
+ | `thoughtsTokenCount` é separado de `candidatesTokenCount` e é cobrado como output | subfatura — 33x a menos num caso com 2000 thinking tokens |
86
+ | 2.5 Pro dobra de preço acima de 200k tokens de prompt; áudio na entrada custa mais que texto | subfatura prompts longos e entrada de áudio |
87
+
88
+ ### Limitações declaradas
89
+
90
+ - **O custo é estimado por tabela de preço local**, não lido de uma fatura. O
91
+ Gemini não expõe API de uso ou custo por chave — só o dashboard do AI Studio
92
+ e o console do Cloud Billing. Os preços são os do paid tier publicados em
93
+ `ai.google.dev/gemini-api/docs/pricing`, conferidos em 2026-08-02, e **não
94
+ foram calibrados contra fatura real**.
95
+ - **Free tier sai com custo > 0.** A tabela é a do paid tier; quem está no free
96
+ tier vê o valor que *pagaria*, não o que paga.
97
+ - **Modelo fora da tabela não recebe preço de outro modelo.** O evento vai com
98
+ `costUSD: null` e `unpriced: true`, preservando os tokens. Variante com sufixo
99
+ (`-preview-…`) é precificada pelo modelo base e marcada `approxPricing`.
100
+ - **Armazenamento de contexto em cache não é cobrado aqui.** O Google cobra por
101
+ hora de cache ativo (US$ 1,00/h na maioria dos modelos); isso não aparece no
102
+ `usageMetadata` de uma chamada e não é modelado.
103
+ - **Batch API não é modelada** — o Google cobra metade nesse modo.
104
+
49
105
  ## Streaming
50
106
 
51
107
  Chamadas com `stream: true` também são rastreadas — o SDK observa os chunks
@@ -106,14 +162,17 @@ CloudBrokerAI.trackDeploy("cb-key-xxxx", {
106
162
  });
107
163
  ```
108
164
 
109
- ## Custo de nuvem a nível de código (MVP)
165
+ ## Cost per Feature — Rastreie Custo por Feature
110
166
 
111
- Atribua custo de **AWS** a um trecho de código / produto. O SDK envolve um client
112
- do AWS SDK v3 e, a cada `.send()`, estima o custo (requests + transferência) e
167
+ Atribua custo de **AWS** e **Azure** a cada feature/módulo sua. O SDK envolve um
168
+ client do provedor e, a cada chamada, estima o custo (requests + transferência) e
113
169
  registra um evento — mesmo padrão fire-and-forget do wrapper de IA.
114
170
 
115
- > MVP: AWS apenas, serviços **S3** e **DynamoDB**, região `us-east-2`. Armazenamento
116
- > GB-mês, Azure e atribuição automática (stack-trace) estão fora do MVP.
171
+ **👉 [Guia Completo](./docs/COST_PER_FEATURE.md)** — Setup (5 min), exemplos, FAQ.
172
+
173
+ > Escopo atual — AWS: **S3** e **DynamoDB** (`us-east-2`); Azure: **Blob Storage**
174
+ > e **Cosmos DB** (`eastus2`). Armazenamento GB-mês e atribuição automática
175
+ > (stack-trace) estão fora do MVP.
117
176
 
118
177
  ```js
119
178
  import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
@@ -146,6 +205,81 @@ Cada chamada gera um evento `POST /api/cloud/events` com `service`, `operation`,
146
205
  `product`, `team`, `label`, `requests`, `bytesIn`/`bytesOut`, `costUSD` estimado e
147
206
  `latencyMs`. O `cbLabel` nunca é repassado ao AWS SDK.
148
207
 
208
+ ### Azure — Blob Storage e Cosmos DB
209
+
210
+ O Azure SDK não tem um `.send()` único como o AWS SDK v3: cada sub-client tem
211
+ métodos próprios. Por isso o `wrapAzure` intercepta a **cadeia de sub-clients** —
212
+ as fábricas (`getContainerClient`, `database`/`container`) devolvem sub-clients já
213
+ embrulhados, e só os métodos de data-plane conhecidos são rastreados. Qualquer
214
+ método fora dessa lista passa intocado.
215
+
216
+ ```js
217
+ import { BlobServiceClient } from "@azure/storage-blob";
218
+ import { CloudBrokerCloud } from "cloudbroker-sdk";
219
+
220
+ const blobSvc = CloudBrokerCloud.wrapAzure(
221
+ BlobServiceClient.fromConnectionString(process.env.AZURE_STORAGE_CONNECTION),
222
+ { apiKey: "cb-key-xxxx", product: "checkout", team: "mobile" },
223
+ );
224
+
225
+ // label opcional dentro do objeto de options nativo do método
226
+ await blobSvc
227
+ .getContainerClient("docs")
228
+ .getBlockBlobClient("nf.pdf")
229
+ .uploadData(buf, { cbLabel: "upload-nota-fiscal" });
230
+ ```
231
+
232
+ Cosmos DB usa o **`requestCharge` (RUs) real** que o servidor devolve em cada
233
+ resposta — custo por unidade cobrada de verdade, não aproximação por request:
234
+
235
+ ```js
236
+ import { CosmosClient } from "@azure/cosmos";
237
+
238
+ const cosmos = CloudBrokerCloud.wrapAzure(new CosmosClient(conn), {
239
+ apiKey: "cb-key-xxxx", product: "checkout",
240
+ });
241
+ const orders = cosmos.database("loja").container("pedidos");
242
+
243
+ await orders.items.create({ id: "123" }, { cbLabel: "criar-pedido" });
244
+ await orders.items.query("SELECT * FROM c").fetchAll(); // RUs do query também contam
245
+ ```
246
+
247
+ O `expressMiddleware` (abaixo) funciona igual pros dois provedores — mesma
248
+ prioridade de `label`/`product`/`team`, sem precisar de `cbLabel` manual.
249
+
250
+ > **Precisão e premissas — leia antes de tratar o número como fechado:**
251
+ >
252
+ > - **Cosmos:** as RUs são as **reais** devolvidas pelo servidor em cada resposta
253
+ > (`requestCharge`). O **preço por RU** assume o modelo **serverless**. Em
254
+ > *throughput provisionado* (manual ou autoscale) a Azure cobra RU/s reservada
255
+ > por hora, independentemente do consumo — esse modelo ainda não é representado,
256
+ > então o custo calculado não corresponde à fatura nesse caso.
257
+ > - **Blob:** estimado por classe de operação (write/read/list), assumindo tier
258
+ > **Hot** e redundância **LRS**. Cool/Archive e GRS/ZRS têm outro preço.
259
+ > - **Transferência de saída:** cobrada sobre todo `bytesOut`. A Azure não cobra
260
+ > saída intra-região e concede franquia mensal — para um app rodando na mesma
261
+ > região do Storage, este termo **superestima**.
262
+ > - **Região:** só `eastus2` tem tabela de preço. Em outra região o evento é
263
+ > registrado com `costUSD: null` e `billingUnit: "*-region-unpriced"` — a
264
+ > operação aparece no relatório, mas sem custo inventado.
265
+ > - **Reconciliação:** a tela compara o estimado contra o custo real do Cost
266
+ > Management e mostra o **desvio**; o ajuste da tabela é manual (não há
267
+ > recalibração automática).
268
+ > - **Listagem:** `listBlobsFlat`/`listContainers` devolvem iterator preguiçoso —
269
+ > o evento só é emitido quando uma página é de fato buscada. Percorrendo com
270
+ > `for await`, contamos **1 operação** mesmo que o SDK pagine internamente;
271
+ > use `.byPage()` para contagem por página.
272
+ > - **Cosmos `for await` direto** (`getAsyncIterator`) não é instrumentado —
273
+ > use `fetchAll()`/`fetchNext()`.
274
+ > - **Delete** no tier Hot é grátis: sai com custo `0` e
275
+ > `billingUnit: "blob-free"`, sem sumir do relatório.
276
+ >
277
+ > ⚠️ **Ainda não calibrado contra uma fatura Azure real.** A estrutura é
278
+ > exercitada contra os SDKs oficiais (`@azure/storage-blob`, `@azure/cosmos`) na
279
+ > suíte de testes, mas os valores da tabela de preço Azure não foram conferidos
280
+ > contra a página oficial nem contra uma fatura. Trate o custo Azure como ordem
281
+ > de grandeza até rodar a reconciliação na sua conta.
282
+
149
283
  ### Atribuição automática por requisição (Express)
150
284
 
151
285
  Em vez de passar `cbLabel` em cada `.send()`, use o middleware: todas as chamadas
@@ -0,0 +1,296 @@
1
+ # CloudBroker SDK — Rastreie Custo por Feature
2
+
3
+ ## O Que É?
4
+
5
+ Instrumentar sua aplicação com o CloudBroker SDK permite rastrear **quanto cada feature/módulo seu custa em AWS e Azure**.
6
+
7
+ ```javascript
8
+ // Seu código
9
+ await s3.putObject({ ... }, { cbLabel: 'invoice-generation' });
10
+ // CloudBroker registra: feature 'invoice-generation' custou $0.00042
11
+ ```
12
+
13
+ **Result:** Dashboard mostra "invoice-generation: $X/mês".
14
+
15
+ ---
16
+
17
+ ## Setup Rápido (5 minutos)
18
+
19
+ ### 1. Instalar
20
+
21
+ ```bash
22
+ npm install cloudbroker-sdk
23
+ ```
24
+
25
+ ### 2. Gerar API Key
26
+
27
+ 1. Acesse https://app.cloudbroker.app.br
28
+ 2. Configurações → API Keys
29
+ 3. Criar nova key do tipo **SDK** (só Admin pode criar keys SDK)
30
+ 4. Copiar: `cb_live_...` — **o valor é exibido uma única vez**; depois só o
31
+ prefixo mascarado fica visível. Se perder, gere outra.
32
+
33
+ ### 3. Instrumentar Backend
34
+
35
+ ```javascript
36
+ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
37
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
38
+ import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
39
+ import { CloudBrokerCloud } from "cloudbroker-sdk";
40
+
41
+ // S3
42
+ const s3 = CloudBrokerCloud.wrapAWS(new S3Client({ region: "us-east-2" }), {
43
+ apiKey: process.env.CLOUDBROKER_API_KEY,
44
+ product: "checkout-service", // seu app
45
+ team: "payments", // seu squad
46
+ });
47
+
48
+ // DynamoDB
49
+ const dynamodb = CloudBrokerCloud.wrapAWS(
50
+ DynamoDBDocumentClient.from(new DynamoDBClient({ region: "us-east-2" })),
51
+ { apiKey: process.env.CLOUDBROKER_API_KEY, product: "checkout-service", team: "payments" }
52
+ );
53
+ ```
54
+
55
+ ### 4. Adicionar Labels
56
+
57
+ ```javascript
58
+ // S3 upload com label
59
+ await s3.send(new PutObjectCommand({
60
+ Bucket: 'invoices',
61
+ Key: `${orderId}.pdf`,
62
+ Body: pdfBuffer,
63
+ }), {
64
+ cbLabel: 'invoice-generation', // ← Identifica a feature
65
+ });
66
+
67
+ // DynamoDB insert com label
68
+ await dynamodb.send(new PutCommand({
69
+ TableName: 'Orders',
70
+ Item: { orderId, status: 'pending' },
71
+ }), {
72
+ cbLabel: 'order-processing', // ← Identifica a feature
73
+ });
74
+ ```
75
+
76
+ ### 5. Ver Custos
77
+
78
+ Dashboard: **CloudBroker App → Custo por Código**
79
+
80
+ ```
81
+ Feature: order-processing
82
+ Requests: 3,456
83
+ Total: $87.50
84
+ Per Request: $0.0253 ✅
85
+
86
+ Feature: invoice-generation
87
+ Requests: 1,234
88
+ Total: $10.20
89
+ Per Request: $0.00828 ✅
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Azure — Blob Storage e Cosmos DB
95
+
96
+ O SDK da Azure não tem um `.send()` único como o AWS SDK v3, então o `wrapAzure`
97
+ intercepta a **cadeia de sub-clients**: as fábricas (`getContainerClient`,
98
+ `database`/`container`) devolvem sub-clients já instrumentados.
99
+
100
+ ```javascript
101
+ import { BlobServiceClient } from "@azure/storage-blob";
102
+ import { CloudBrokerCloud } from "cloudbroker-sdk";
103
+
104
+ const blobSvc = CloudBrokerCloud.wrapAzure(
105
+ BlobServiceClient.fromConnectionString(process.env.AZURE_STORAGE_CONNECTION),
106
+ { apiKey: "cb_live_...", product: "checkout", team: "mobile" },
107
+ );
108
+
109
+ await blobSvc
110
+ .getContainerClient("docs")
111
+ .getBlockBlobClient("nf.pdf")
112
+ .uploadData(buf, { cbLabel: "upload-nota-fiscal" });
113
+ ```
114
+
115
+ Cosmos DB usa o `requestCharge` (RUs) **real** devolvido pelo servidor:
116
+
117
+ ```javascript
118
+ import { CosmosClient } from "@azure/cosmos";
119
+
120
+ const cosmos = CloudBrokerCloud.wrapAzure(new CosmosClient(conn), {
121
+ apiKey: "cb_live_...", product: "checkout",
122
+ });
123
+ const pedidos = cosmos.database("loja").container("pedidos");
124
+
125
+ await pedidos.items.create({ id: "123" }, { cbLabel: "criar-pedido" });
126
+ await pedidos.items.query("SELECT * FROM c").fetchAll();
127
+ ```
128
+
129
+ O `expressMiddleware` funciona igual para AWS e Azure.
130
+
131
+ **Premissas que mudam o número** (detalhe completo no
132
+ [README](../README.md#cost-per-feature--rastreie-custo-por-feature)):
133
+
134
+ | Item | Situação |
135
+ |---|---|
136
+ | Cosmos — RUs | Reais (`requestCharge` do servidor) |
137
+ | Cosmos — preço por RU | Assume **serverless**. Throughput provisionado não é modelado |
138
+ | Blob | Estimado por classe de operação; assume tier **Hot** + **LRS** |
139
+ | Egress | Cobrado sobre todo `bytesOut` (a Azure não cobra intra-região) |
140
+ | Região | Só `eastus2` tem preço; outras saem com `costUSD: null` |
141
+ | `for await` no Cosmos | Não instrumentado — use `fetchAll()`/`fetchNext()` |
142
+ | Calibração | ⚠️ **Não conferido contra fatura Azure real ainda** |
143
+
144
+ ---
145
+
146
+ ## Convenção de Labels
147
+
148
+ Use nomes **descritivos** que refletem features/módulos:
149
+
150
+ | Feature | Label | Exemplos de Operações |
151
+ |---------|-------|----------------------|
152
+ | Checkout | `order-processing` | Criar pedido, persistir dados |
153
+ | Geração de Invoice | `invoice-generation` | Upload de PDF, armazenamento |
154
+ | Detecção de Fraude | `fraud-detection` | Análise de padrões, lookup histórico |
155
+ | Analytics | `analytics-tracking` | Coleta de eventos, agregação |
156
+ | Admin Panel | `admin-reports` | Geração de relatórios, queries |
157
+
158
+ **Regras:**
159
+ - ✅ Use labels **finitos** (não criar um label por request)
160
+ - ✅ Labels **descritivos** (não `x1`, `x2`, etc)
161
+ - ✅ Use **kebab-case** (`order-processing`, não `orderProcessing`)
162
+
163
+ ❌ **EVITAR:**
164
+ ```javascript
165
+ // Ruim: label infinito (1 por request)
166
+ cbLabel: `order-${order.id}`
167
+
168
+ // Ruim: não descritivo
169
+ cbLabel: 'x1'
170
+
171
+ // Bom: finito e descritivo
172
+ cbLabel: 'order-processing'
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Exemplo Completo
178
+
179
+ ```javascript
180
+ import express from 'express';
181
+ import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
182
+ import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
183
+ import { CloudBrokerCloud } from 'cloudbroker-sdk';
184
+
185
+ const app = express();
186
+
187
+ // Setup
188
+ const s3 = CloudBrokerCloud.wrapAWS(new S3Client(), {
189
+ apiKey: process.env.CLOUDBROKER_API_KEY,
190
+ product: 'checkout-service',
191
+ team: 'payments',
192
+ });
193
+
194
+ const dynamodb = CloudBrokerCloud.wrapAWS(
195
+ DynamoDBDocumentClient.from(new DynamoDBClient()),
196
+ { apiKey: process.env.CLOUDBROKER_API_KEY, product: 'checkout-service', team: 'payments' }
197
+ );
198
+
199
+ // POST /orders — cria pedido com 2 features diferentes
200
+ app.post('/orders', async (req, res) => {
201
+ const order = { orderId: `order-${Date.now()}`, ...req.body };
202
+
203
+ // Feature 1: Persistir pedido
204
+ await dynamodb.send(new PutCommand({
205
+ TableName: 'Orders',
206
+ Item: order,
207
+ }), {
208
+ cbLabel: 'order-processing',
209
+ });
210
+
211
+ // Feature 2: Gerar invoice
212
+ await s3.send(new PutObjectCommand({
213
+ Bucket: 'invoices',
214
+ Key: `${order.orderId}.json`,
215
+ Body: JSON.stringify(order),
216
+ }), {
217
+ cbLabel: 'invoice-generation',
218
+ });
219
+
220
+ res.json({ ok: true, orderId: order.orderId });
221
+ });
222
+
223
+ app.listen(3000);
224
+ ```
225
+
226
+ **Result no Dashboard:**
227
+ - `order-processing`: $X/mês
228
+ - `invoice-generation`: $Y/mês
229
+
230
+ ---
231
+
232
+ ## FAQ
233
+
234
+ **P: Quanto tempo leva pra aparecer no dashboard?**
235
+ A: ~5 minutos (batch em DynamoDB).
236
+
237
+ **P: E se não colocar label?**
238
+ A: Ainda rastreia como `null` (agrupa sem label).
239
+
240
+ **P: Posso mudar o label depois?**
241
+ A: Não — é histórico. Use um novo label a partir de agora.
242
+
243
+ **P: Qual é o overhead?**
244
+ A: <1% CPU (fire-and-forget, não bloqueia requests).
245
+
246
+ **P: Funciona em todas as regiões?**
247
+ A: Hoje há tabela de preço para `us-east-2` (AWS) e `eastus2` (Azure). Em outra
248
+ região a operação é registrada com `costUSD: null` e `billingUnit`
249
+ `*-region-unpriced` — aparece no relatório, mas sem custo estimado (preferimos
250
+ não exibir o preço de outra região como se fosse o seu).
251
+
252
+ **P: E se der erro no rastreamento?**
253
+ A: Silencioso — seu request continua normal (fail-soft).
254
+
255
+ ---
256
+
257
+ ## Troubleshooting
258
+
259
+ ### Nenhum dado aparecendo no dashboard?
260
+
261
+ 1. ✅ API Key configurada?
262
+ ```bash
263
+ echo $CLOUDBROKER_API_KEY
264
+ ```
265
+
266
+ 2. ✅ CloudBrokerCloud.wrapAWS() chamado?
267
+ ```javascript
268
+ const s3 = CloudBrokerCloud.wrapAWS(new S3Client(), { apiKey, product, team });
269
+ ```
270
+
271
+ 3. ✅ cbLabel adicionado?
272
+ ```javascript
273
+ await s3.send(cmd, { cbLabel: 'seu-label' });
274
+ ```
275
+
276
+ 4. ✅ Esperou 5+ minutos?
277
+ (CloudBroker faz batch, não é real-time)
278
+
279
+ ### Custo aparecendo como $0?
280
+
281
+ - Normal no MVP (estimativa simplificada, reconciliação depois)
282
+ - Operações muito baratas podem ficar abaixo do centavo
283
+
284
+ ---
285
+
286
+ ## Próximos Passos
287
+
288
+ - **Exemplo funcional:** Veja `example-checkout-app` no repo
289
+ - **Mais Serviços:** Lambda, SQS, SNS; Azure Functions, Service Bus (roadmap)
290
+ - **OpenTelemetry:** Rastrear custom operations (futuro)
291
+
292
+ ---
293
+
294
+ ## Suporte
295
+
296
+ Dúvidas? Abra issue no [GitHub](https://github.com/cloudbroker-app/cloudbroker-sdk).
@@ -0,0 +1,13 @@
1
+ # AWS Configuration
2
+ AWS_REGION=us-east-2
3
+ AWS_ACCESS_KEY_ID=your-access-key
4
+ AWS_SECRET_ACCESS_KEY=your-secret-key
5
+
6
+ # CloudBroker Configuration
7
+ CLOUDBROKER_API_KEY=sk_1234...
8
+ CLOUDBROKER_PRODUCT=checkout-example
9
+ CLOUDBROKER_TEAM=platform
10
+
11
+ # App Configuration
12
+ PORT=3000
13
+ NODE_ENV=development