cloudbroker-sdk 0.1.0 → 0.2.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.
- package/README.md +40 -0
- package/package.json +1 -1
- package/src/client.js +76 -11
- package/src/cloud-wrapper.js +127 -0
- package/src/index.js +2 -0
- package/src/pricing-cloud.js +83 -0
- package/src/test.js +81 -0
package/README.md
CHANGED
|
@@ -106,6 +106,46 @@ CloudBrokerAI.trackDeploy("cb-key-xxxx", {
|
|
|
106
106
|
});
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
## Custo de nuvem a nível de código (MVP)
|
|
110
|
+
|
|
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
|
|
113
|
+
registra um evento — mesmo padrão fire-and-forget do wrapper de IA.
|
|
114
|
+
|
|
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.
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
120
|
+
import { CloudBrokerCloud } from "cloudbroker-sdk";
|
|
121
|
+
|
|
122
|
+
const s3 = CloudBrokerCloud.wrapAWS(new S3Client({ region: "us-east-2" }), {
|
|
123
|
+
apiKey: "cb-key-xxxx",
|
|
124
|
+
product: "checkout",
|
|
125
|
+
team: "mobile",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// label opcional do trecho, no 2º argumento do .send()
|
|
129
|
+
await s3.send(new PutObjectCommand({ Bucket: "b", Key: "nf.pdf", Body: buf }), {
|
|
130
|
+
cbLabel: "upload-nota-fiscal",
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Funciona igual com DynamoDB:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
|
|
138
|
+
|
|
139
|
+
const ddb = CloudBrokerCloud.wrapAWS(new DynamoDBClient({ region: "us-east-2" }), {
|
|
140
|
+
apiKey: "cb-key-xxxx", product: "checkout",
|
|
141
|
+
});
|
|
142
|
+
await ddb.send(new PutItemCommand({ ... }), { cbLabel: "gravar-pedido" });
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Cada chamada gera um evento `POST /api/cloud/events` com `service`, `operation`,
|
|
146
|
+
`product`, `team`, `label`, `requests`, `bytesIn`/`bytesOut`, `costUSD` estimado e
|
|
147
|
+
`latencyMs`. O `cbLabel` nunca é repassado ao AWS SDK.
|
|
148
|
+
|
|
109
149
|
## Garantias
|
|
110
150
|
|
|
111
151
|
- **Fire-and-forget**: se o CloudBroker estiver fora, sua aplicação não é afetada
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -3,25 +3,86 @@
|
|
|
3
3
|
* Fire-and-forget: nunca bloqueia a aplicação do cliente
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const DEFAULT_AI_ENDPOINT
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
6
|
+
const DEFAULT_AI_ENDPOINT = "https://api.cloudbroker.app.br/api/ai/events";
|
|
7
|
+
const DEFAULT_CLOUD_ENDPOINT = "https://api.cloudbroker.app.br/api/cloud/events";
|
|
8
|
+
const DEFAULT_API_BASE = "https://api.cloudbroker.app.br/api";
|
|
9
|
+
const RETRY_DELAY_MS = 30_000; // 30s entre retries
|
|
10
|
+
const MAX_QUEUE_SIZE = 500; // máximo de eventos em memória
|
|
10
11
|
|
|
11
12
|
export class CloudBrokerClient {
|
|
12
13
|
#apiKey;
|
|
13
14
|
#aiEndpoint;
|
|
15
|
+
#cloudEndpoint;
|
|
14
16
|
#apiBase;
|
|
15
17
|
#queue = [];
|
|
16
18
|
#flushing = false;
|
|
19
|
+
#cloudQueue = [];
|
|
20
|
+
#cloudFlushing = false;
|
|
17
21
|
#debug;
|
|
18
22
|
|
|
19
|
-
constructor({ apiKey, endpoint = DEFAULT_AI_ENDPOINT, apiBase = DEFAULT_API_BASE, debug = false } = {}) {
|
|
23
|
+
constructor({ apiKey, endpoint = DEFAULT_AI_ENDPOINT, cloudEndpoint = DEFAULT_CLOUD_ENDPOINT, apiBase = DEFAULT_API_BASE, debug = false } = {}) {
|
|
20
24
|
if (!apiKey) throw new Error("[CloudBroker] apiKey é obrigatório.");
|
|
21
|
-
this.#apiKey
|
|
22
|
-
this.#aiEndpoint
|
|
23
|
-
this.#
|
|
24
|
-
this.#
|
|
25
|
+
this.#apiKey = apiKey;
|
|
26
|
+
this.#aiEndpoint = endpoint;
|
|
27
|
+
this.#cloudEndpoint = cloudEndpoint;
|
|
28
|
+
this.#apiBase = apiBase;
|
|
29
|
+
this.#debug = debug;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Envia um evento de custo de nuvem (atribuição a nível de código)
|
|
34
|
+
* Fire-and-forget — nunca lança erro para o caller
|
|
35
|
+
*/
|
|
36
|
+
trackCloud(event) {
|
|
37
|
+
try {
|
|
38
|
+
const payload = {
|
|
39
|
+
...event,
|
|
40
|
+
sdkVersion: "0.2.0",
|
|
41
|
+
sentAt: new Date().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
if (this.#cloudQueue.length < MAX_QUEUE_SIZE) {
|
|
44
|
+
this.#cloudQueue.push(payload);
|
|
45
|
+
}
|
|
46
|
+
this.#flushCloud();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (this.#debug) console.warn("[CloudBroker] trackCloud error:", err.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async #flushCloud() {
|
|
53
|
+
if (this.#cloudFlushing || this.#cloudQueue.length === 0) return;
|
|
54
|
+
this.#cloudFlushing = true;
|
|
55
|
+
|
|
56
|
+
const batch = this.#cloudQueue.splice(0, 50);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(this.#cloudEndpoint, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
"x-cloudbroker-key": this.#apiKey,
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({ events: batch }),
|
|
66
|
+
signal: AbortSignal.timeout(5000),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
if (this.#debug) console.warn("[CloudBroker] cloud flush failed:", res.status);
|
|
71
|
+
this.#cloudQueue.unshift(...batch);
|
|
72
|
+
setTimeout(() => { this.#cloudFlushing = false; this.#flushCloud(); }, RETRY_DELAY_MS);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.#debug) console.log(`[CloudBroker] flushed ${batch.length} cloud events`);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
this.#cloudQueue.unshift(...batch);
|
|
79
|
+
if (this.#debug) console.warn("[CloudBroker] cloud flush error:", err.message);
|
|
80
|
+
setTimeout(() => { this.#cloudFlushing = false; this.#flushCloud(); }, RETRY_DELAY_MS);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.#cloudFlushing = false;
|
|
85
|
+
if (this.#cloudQueue.length > 0) this.#flushCloud();
|
|
25
86
|
}
|
|
26
87
|
|
|
27
88
|
/**
|
|
@@ -32,7 +93,7 @@ export class CloudBrokerClient {
|
|
|
32
93
|
try {
|
|
33
94
|
const payload = {
|
|
34
95
|
...event,
|
|
35
|
-
sdkVersion: "0.
|
|
96
|
+
sdkVersion: "0.2.0",
|
|
36
97
|
sentAt: new Date().toISOString(),
|
|
37
98
|
};
|
|
38
99
|
|
|
@@ -144,11 +205,15 @@ export class CloudBrokerClient {
|
|
|
144
205
|
if (this.#queue.length > 0) this.#flush();
|
|
145
206
|
}
|
|
146
207
|
|
|
147
|
-
/** Força flush imediato — útil em shutdown */
|
|
208
|
+
/** Força flush imediato (IA + nuvem) — útil em shutdown */
|
|
148
209
|
async flushSync() {
|
|
149
210
|
while (this.#queue.length > 0) {
|
|
150
211
|
await this.#flush();
|
|
151
212
|
await new Promise(r => setTimeout(r, 100));
|
|
152
213
|
}
|
|
214
|
+
while (this.#cloudQueue.length > 0) {
|
|
215
|
+
await this.#flushCloud();
|
|
216
|
+
await new Promise(r => setTimeout(r, 100));
|
|
217
|
+
}
|
|
153
218
|
}
|
|
154
219
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloudBrokerCloud — atribuição de custo de nuvem a nível de código (MVP).
|
|
3
|
+
*
|
|
4
|
+
* Envolve um client do AWS SDK v3 (S3 / DynamoDB) e, a cada chamada `.send()`,
|
|
5
|
+
* estima o custo de requests + transferência e envia um evento fire-and-forget
|
|
6
|
+
* ao backend. Não altera o comportamento do client original.
|
|
7
|
+
*
|
|
8
|
+
* Uso:
|
|
9
|
+
* import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
10
|
+
* import { CloudBrokerCloud } from "cloudbroker-sdk";
|
|
11
|
+
*
|
|
12
|
+
* const s3 = CloudBrokerCloud.wrapAWS(new S3Client({ region: "us-east-2" }), {
|
|
13
|
+
* apiKey: "cb-key-xxx", product: "checkout", team: "mobile",
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // label opcional do trecho, no 2º argumento do .send()
|
|
17
|
+
* await s3.send(new PutObjectCommand({ ... }), { cbLabel: "upload-nota-fiscal" });
|
|
18
|
+
*
|
|
19
|
+
* Nota de implementação: interceptamos via override de `client.send` (em vez do
|
|
20
|
+
* middlewareStack) porque o `cbLabel` passado no 2º argumento do `.send()` não é
|
|
21
|
+
* lido de forma confiável pelo context do middleware. O override captura tudo —
|
|
22
|
+
* nome da operação, bytes in/out, latência e label — de forma limpa e testável.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { CloudBrokerClient } from "./client.js";
|
|
26
|
+
import { estimateCloudCost } from "./pricing-cloud.js";
|
|
27
|
+
|
|
28
|
+
export class CloudBrokerCloud {
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} client — instância do AWS SDK v3 (ex: new S3Client())
|
|
31
|
+
* @param {object} options
|
|
32
|
+
* @param {string} options.apiKey — CloudBroker API key
|
|
33
|
+
* @param {string} [options.product] — nome do produto (ex: "checkout")
|
|
34
|
+
* @param {string} [options.team] — nome do squad (ex: "mobile")
|
|
35
|
+
* @param {string} [options.region="us-east-2"]
|
|
36
|
+
* @param {string} [options.endpoint] — endpoint de eventos de nuvem (opcional)
|
|
37
|
+
* @param {boolean} [options.debug=false]
|
|
38
|
+
*/
|
|
39
|
+
static wrapAWS(client, options = {}) {
|
|
40
|
+
const { apiKey, product, team, region = "us-east-2", endpoint, debug = false } = options;
|
|
41
|
+
if (!apiKey) throw new Error("[CloudBroker] apiKey é obrigatório.");
|
|
42
|
+
|
|
43
|
+
if (!client || typeof client.send !== "function") {
|
|
44
|
+
if (debug) console.warn("[CloudBroker] cliente AWS sem .send() — retornando original.");
|
|
45
|
+
return client;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const cb = new CloudBrokerClient({ apiKey, cloudEndpoint: endpoint, debug });
|
|
49
|
+
const service = CloudBrokerCloud.#detectService(client);
|
|
50
|
+
const clientRegion = CloudBrokerCloud.#resolveRegion(client) || region;
|
|
51
|
+
const originalSend = client.send.bind(client);
|
|
52
|
+
|
|
53
|
+
client.send = async (command, sendOptions) => {
|
|
54
|
+
const start = Date.now();
|
|
55
|
+
const operation = (command?.constructor?.name || "").replace(/Command$/, "");
|
|
56
|
+
const cbLabel = sendOptions?.cbLabel || null;
|
|
57
|
+
const bytesIn = CloudBrokerCloud.#inputBytes(command?.input);
|
|
58
|
+
|
|
59
|
+
// Passa adiante apenas as options nativas — nunca vaza cbLabel pro AWS SDK.
|
|
60
|
+
const { cbLabel: _omit, ...awsOptions } = sendOptions || {};
|
|
61
|
+
const result = await originalSend(command, Object.keys(awsOptions).length ? awsOptions : undefined);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const bytesOut = CloudBrokerCloud.#outputBytes(result);
|
|
65
|
+
const { costUSD, billingUnit } = estimateCloudCost({
|
|
66
|
+
service, operation, requests: 1, bytesOut, region: clientRegion,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
cb.trackCloud({
|
|
70
|
+
provider: "aws",
|
|
71
|
+
service,
|
|
72
|
+
operation,
|
|
73
|
+
product: product || null,
|
|
74
|
+
team: team || null,
|
|
75
|
+
label: cbLabel,
|
|
76
|
+
requests: 1,
|
|
77
|
+
bytesIn,
|
|
78
|
+
bytesOut,
|
|
79
|
+
costUSD,
|
|
80
|
+
billingUnit,
|
|
81
|
+
latencyMs: Date.now() - start,
|
|
82
|
+
region: clientRegion,
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (debug) console.warn("[CloudBroker] trackCloud error:", err.message);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return client;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Detecta o serviço AWS pelo nome do construtor do client. */
|
|
95
|
+
static #detectService(client) {
|
|
96
|
+
const name = client?.constructor?.name || "";
|
|
97
|
+
if (name.startsWith("S3")) return "s3";
|
|
98
|
+
if (name.startsWith("DynamoDB")) return "dynamodb"; // cobre DynamoDBClient e DynamoDBDocumentClient
|
|
99
|
+
return "unknown";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Tenta resolver a região configurada no client (pode ser função async ou string). */
|
|
103
|
+
static #resolveRegion(client) {
|
|
104
|
+
const r = client?.config?.region;
|
|
105
|
+
if (typeof r === "string") return r;
|
|
106
|
+
return null; // região via provider async não é resolvida no MVP — usa o fallback de options
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Estima bytes de entrada a partir do input do comando (Body do S3, etc). */
|
|
110
|
+
static #inputBytes(input) {
|
|
111
|
+
if (!input) return 0;
|
|
112
|
+
const body = input.Body;
|
|
113
|
+
if (body == null) return 0;
|
|
114
|
+
if (typeof body === "string") return Buffer.byteLength(body);
|
|
115
|
+
if (Buffer.isBuffer(body)) return body.length;
|
|
116
|
+
if (body instanceof Uint8Array) return body.byteLength;
|
|
117
|
+
if (typeof input.ContentLength === "number") return input.ContentLength;
|
|
118
|
+
return 0; // streams não são medidos no MVP
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Estima bytes de saída a partir do resultado (ContentLength de um GetObject, etc). */
|
|
122
|
+
static #outputBytes(result) {
|
|
123
|
+
if (!result) return 0;
|
|
124
|
+
if (typeof result.ContentLength === "number") return result.ContentLength;
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabela de preços de nuvem (curada, MVP) — atribuição de custo a nível de código.
|
|
3
|
+
* Escopo MVP: AWS, região us-east-2, serviços S3 e DynamoDB.
|
|
4
|
+
* Componentes: requests + transferência de dados (armazenamento GB-mês fora do MVP).
|
|
5
|
+
* Preços em USD. Atualizado: Jun/2026.
|
|
6
|
+
*/
|
|
7
|
+
export const CLOUD_PRICING = {
|
|
8
|
+
aws: {
|
|
9
|
+
"us-east-2": {
|
|
10
|
+
s3: {
|
|
11
|
+
// por request (preço por unidade — já dividido por 1.000)
|
|
12
|
+
put: 0.005 / 1000, // PUT, COPY, POST, LIST
|
|
13
|
+
get: 0.0004 / 1000, // GET, SELECT e demais
|
|
14
|
+
// transferência de dados de saída para a internet — 1ª faixa (simplificado), por GB
|
|
15
|
+
transferOutPerGB: 0.09,
|
|
16
|
+
},
|
|
17
|
+
dynamodb: {
|
|
18
|
+
// on-demand
|
|
19
|
+
wru: 1.25 / 1_000_000, // write request unit
|
|
20
|
+
rru: 0.25 / 1_000_000, // read request unit
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const GB = 1024 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
// Operações S3 cobradas como PUT-class (mais caras). O resto cai em GET-class.
|
|
29
|
+
const S3_PUT_CLASS = new Set([
|
|
30
|
+
"PutObject", "CopyObject", "PostObject", "ListObjects", "ListObjectsV2",
|
|
31
|
+
"ListBuckets", "CreateMultipartUpload", "UploadPart", "CompleteMultipartUpload",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
// Operações DynamoDB cobradas como escrita (WRU). O resto que gera custo cai em leitura (RRU).
|
|
35
|
+
const DDB_WRITE_OPS = new Set([
|
|
36
|
+
"PutItem", "UpdateItem", "DeleteItem", "BatchWriteItem", "TransactWriteItems",
|
|
37
|
+
]);
|
|
38
|
+
const DDB_READ_OPS = new Set([
|
|
39
|
+
"GetItem", "Query", "Scan", "BatchGetItem", "TransactGetItems",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Classifica a operação e estima o custo USD de uma única chamada.
|
|
44
|
+
* MVP: aproxima 1 unidade de capacidade por request (não conhece o tamanho do item
|
|
45
|
+
* com precisão); a reconciliação contra o Cost Explorer corrige o desvio agregado.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} p
|
|
48
|
+
* @param {string} p.service — "s3" | "dynamodb"
|
|
49
|
+
* @param {string} p.operation — nome da operação sem o sufixo "Command" (ex: "PutObject")
|
|
50
|
+
* @param {number} [p.requests=1]
|
|
51
|
+
* @param {number} [p.bytesOut=0] — bytes transferidos de saída (ex: ContentLength de um GetObject)
|
|
52
|
+
* @param {string} [p.region="us-east-2"]
|
|
53
|
+
* @returns {{ costUSD:number, billingUnit:string }}
|
|
54
|
+
*/
|
|
55
|
+
export function estimateCloudCost({ service, operation, requests = 1, bytesOut = 0, region = "us-east-2" }) {
|
|
56
|
+
const regionTable = CLOUD_PRICING.aws[region] || CLOUD_PRICING.aws["us-east-2"];
|
|
57
|
+
const svc = (service || "").toLowerCase();
|
|
58
|
+
let costUSD = 0;
|
|
59
|
+
let billingUnit = "unknown";
|
|
60
|
+
|
|
61
|
+
if (svc === "s3") {
|
|
62
|
+
const t = regionTable.s3;
|
|
63
|
+
const isPut = S3_PUT_CLASS.has(operation);
|
|
64
|
+
costUSD += requests * (isPut ? t.put : t.get);
|
|
65
|
+
billingUnit = isPut ? "s3-put" : "s3-get";
|
|
66
|
+
if (bytesOut > 0) {
|
|
67
|
+
costUSD += (bytesOut / GB) * t.transferOutPerGB;
|
|
68
|
+
}
|
|
69
|
+
} else if (svc === "dynamodb") {
|
|
70
|
+
const t = regionTable.dynamodb;
|
|
71
|
+
if (DDB_WRITE_OPS.has(operation)) {
|
|
72
|
+
costUSD += requests * t.wru;
|
|
73
|
+
billingUnit = "ddb-wru";
|
|
74
|
+
} else if (DDB_READ_OPS.has(operation)) {
|
|
75
|
+
costUSD += requests * t.rru;
|
|
76
|
+
billingUnit = "ddb-rru";
|
|
77
|
+
} else {
|
|
78
|
+
billingUnit = "ddb-none"; // control-plane (ex: DescribeTable) — sem custo de request
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { costUSD: parseFloat(costUSD.toFixed(8)), billingUnit };
|
|
83
|
+
}
|
package/src/test.js
CHANGED
|
@@ -74,3 +74,84 @@ wrapped.trackDeploy({
|
|
|
74
74
|
linesRemoved: 30,
|
|
75
75
|
});
|
|
76
76
|
console.log("✅ Deploy trackado (fire-and-forget)");
|
|
77
|
+
|
|
78
|
+
// ─── CLOUD COST ATTRIBUTION (MVP) ───────────────────────────────────────────────
|
|
79
|
+
import assert from "node:assert";
|
|
80
|
+
import { CloudBrokerCloud } from "./cloud-wrapper.js";
|
|
81
|
+
import { estimateCloudCost } from "./pricing-cloud.js";
|
|
82
|
+
|
|
83
|
+
console.log("\n☁️ Testando atribuição de custo de nuvem...");
|
|
84
|
+
|
|
85
|
+
// 1) Tabela de preços / estimativa
|
|
86
|
+
const s3put = estimateCloudCost({ service: "s3", operation: "PutObject", requests: 1 });
|
|
87
|
+
assert.strictEqual(s3put.billingUnit, "s3-put");
|
|
88
|
+
assert.ok(Math.abs(s3put.costUSD - 0.000005) < 1e-9, `s3 put esperado 0.000005, veio ${s3put.costUSD}`);
|
|
89
|
+
|
|
90
|
+
const s3get = estimateCloudCost({ service: "s3", operation: "GetObject", requests: 1, bytesOut: 1024 * 1024 * 1024 });
|
|
91
|
+
assert.strictEqual(s3get.billingUnit, "s3-get");
|
|
92
|
+
// 0.0000004 (request) + 0.09 (1 GB de transferência)
|
|
93
|
+
assert.ok(Math.abs(s3get.costUSD - 0.0900004) < 1e-6, `s3 get+transfer veio ${s3get.costUSD}`);
|
|
94
|
+
|
|
95
|
+
const ddbW = estimateCloudCost({ service: "dynamodb", operation: "PutItem", requests: 1 });
|
|
96
|
+
assert.strictEqual(ddbW.billingUnit, "ddb-wru");
|
|
97
|
+
assert.ok(Math.abs(ddbW.costUSD - 0.00000125) < 1e-12, `ddb write veio ${ddbW.costUSD}`);
|
|
98
|
+
|
|
99
|
+
const ddbR = estimateCloudCost({ service: "dynamodb", operation: "GetItem", requests: 1 });
|
|
100
|
+
assert.strictEqual(ddbR.billingUnit, "ddb-rru");
|
|
101
|
+
assert.ok(Math.abs(ddbR.costUSD - 0.00000025) < 1e-12, `ddb read veio ${ddbR.costUSD}`);
|
|
102
|
+
|
|
103
|
+
const ddbCtl = estimateCloudCost({ service: "dynamodb", operation: "DescribeTable", requests: 1 });
|
|
104
|
+
assert.strictEqual(ddbCtl.billingUnit, "ddb-none");
|
|
105
|
+
assert.strictEqual(ddbCtl.costUSD, 0);
|
|
106
|
+
console.log("✅ estimateCloudCost: S3 put/get+transfer e DynamoDB wru/rru/control-plane");
|
|
107
|
+
|
|
108
|
+
// 2) wrapAWS — captura evento, não vaza cbLabel pro AWS, preserva resultado
|
|
109
|
+
const captured = [];
|
|
110
|
+
const realFetch = globalThis.fetch;
|
|
111
|
+
globalThis.fetch = async (url, opts) => {
|
|
112
|
+
if (String(url).includes("/api/cloud/events")) {
|
|
113
|
+
captured.push(JSON.parse(opts.body));
|
|
114
|
+
return { ok: true, status: 200, json: async () => ({}) };
|
|
115
|
+
}
|
|
116
|
+
return { ok: true, status: 200, json: async () => ({}) };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
let sawAwsOptions; // o que o "AWS SDK" recebeu de options
|
|
120
|
+
class S3Client {
|
|
121
|
+
constructor(cfg) { this.config = cfg; }
|
|
122
|
+
async send(command, options) {
|
|
123
|
+
sawAwsOptions = options;
|
|
124
|
+
return { ContentLength: 2048, $metadata: { httpStatusCode: 200 } };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
class PutObjectCommand {
|
|
128
|
+
constructor(input) { this.input = input; }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const s3 = CloudBrokerCloud.wrapAWS(new S3Client({ region: "us-east-2" }), {
|
|
132
|
+
apiKey: "cb-test-key", product: "checkout", team: "mobile", debug: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const putRes = await s3.send(new PutObjectCommand({ Bucket: "b", Key: "k", Body: "hello world" }), { cbLabel: "upload-nf" });
|
|
136
|
+
assert.strictEqual(putRes.ContentLength, 2048, "resultado original deve ser preservado");
|
|
137
|
+
assert.strictEqual(sawAwsOptions, undefined, "cbLabel não pode vazar pro AWS SDK (options ficou só com cbLabel → vira undefined)");
|
|
138
|
+
|
|
139
|
+
await s3.flushSync?.(); // garante envio
|
|
140
|
+
// pequena espera pro fire-and-forget
|
|
141
|
+
await new Promise(r => setTimeout(r, 50));
|
|
142
|
+
|
|
143
|
+
assert.strictEqual(captured.length, 1, `esperado 1 evento de nuvem, veio ${captured.length}`);
|
|
144
|
+
const ev = captured[0].events[0];
|
|
145
|
+
assert.strictEqual(ev.provider, "aws");
|
|
146
|
+
assert.strictEqual(ev.service, "s3");
|
|
147
|
+
assert.strictEqual(ev.operation, "PutObject");
|
|
148
|
+
assert.strictEqual(ev.product, "checkout");
|
|
149
|
+
assert.strictEqual(ev.team, "mobile");
|
|
150
|
+
assert.strictEqual(ev.label, "upload-nf");
|
|
151
|
+
assert.strictEqual(ev.bytesIn, Buffer.byteLength("hello world"));
|
|
152
|
+
assert.strictEqual(ev.billingUnit, "s3-put");
|
|
153
|
+
assert.ok(ev.costUSD > 0, "deve ter custo estimado");
|
|
154
|
+
console.log("✅ wrapAWS: evento capturado, cbLabel não vazou, resultado preservado");
|
|
155
|
+
|
|
156
|
+
globalThis.fetch = realFetch;
|
|
157
|
+
console.log("\n🎉 Todos os testes passaram.");
|