Apps
This commit is contained in:
@@ -8,6 +8,11 @@ BFF mínimo para expor flags ao app sem expor `Atomenta-Token` no cliente.
|
|||||||
- `POST /feature-control/telemetry/exposure`
|
- `POST /feature-control/telemetry/exposure`
|
||||||
- `GET /health`
|
- `GET /health`
|
||||||
|
|
||||||
|
## Notificações
|
||||||
|
|
||||||
|
No momento este BFF cobre apenas Feature Control (`bootstrap` + `exposure`).
|
||||||
|
Para notificações segmentadas por audiência (`todos`, `feature:fc.ios`), precisamos adicionar o contrato de endpoint de notificações do Atomenta para integrar no app.
|
||||||
|
|
||||||
## Variáveis de ambiente
|
## Variáveis de ambiente
|
||||||
|
|
||||||
- `PORT` (default: `8787`)
|
- `PORT` (default: `8787`)
|
||||||
@@ -22,8 +27,11 @@ Exemplo de `FEATURE_CONTROL_DEFAULTS_JSON`:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"fc.checkout_v2": { "enabled": false, "variant": "off" },
|
"fc.promo": { "enabled": true, "variant": "on" },
|
||||||
"fc.search_ranking_v3": { "enabled": true, "variant": "on", "payload": { "model": "v3" } }
|
"fc.promo-codes": { "enabled": true, "variant": "on" },
|
||||||
|
"fc.android": { "enabled": true, "variant": "on" },
|
||||||
|
"fc.city-aguai": { "enabled": true, "variant": "on" },
|
||||||
|
"fc.ios": { "enabled": true, "variant": "on" }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -32,7 +40,7 @@ Exemplo de `FEATURE_CONTROL_DEFAULTS_JSON`:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"environment": "production",
|
"environment": "production",
|
||||||
"keys": ["fc.checkout_v2", "fc.search_ranking_v3"],
|
"keys": ["fc.promo", "fc.promo-codes", "fc.android", "fc.city-aguai", "fc.ios"],
|
||||||
"context": {
|
"context": {
|
||||||
"subjectType": "customer",
|
"subjectType": "customer",
|
||||||
"subjectId": "cust_123",
|
"subjectId": "cust_123",
|
||||||
@@ -58,11 +66,14 @@ Se o app enviar `Authorization: Bearer <JWT>`, o BFF tenta extrair `subjectId` d
|
|||||||
"configVersion": 7,
|
"configVersion": 7,
|
||||||
"evaluatedAt": "2026-04-16T12:00:00.000Z",
|
"evaluatedAt": "2026-04-16T12:00:00.000Z",
|
||||||
"flags": {
|
"flags": {
|
||||||
"checkoutV2": true,
|
"promo": true,
|
||||||
"searchRankingV3": "off"
|
"promoCodes": true,
|
||||||
|
"android": true,
|
||||||
|
"cityAguai": true,
|
||||||
|
"ios": true
|
||||||
},
|
},
|
||||||
"raw": {
|
"raw": {
|
||||||
"fc.checkout_v2": {
|
"fc.promo": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"variant": "on",
|
"variant": "on",
|
||||||
"payload": null,
|
"payload": null,
|
||||||
|
|||||||
@@ -91,8 +91,9 @@ function createUpstreamStub(options = {}) {
|
|||||||
evaluatedAt: "2026-04-16T12:00:00.000Z",
|
evaluatedAt: "2026-04-16T12:00:00.000Z",
|
||||||
configVersion: 7,
|
configVersion: 7,
|
||||||
flags: {
|
flags: {
|
||||||
"fc.checkout_v2": { enabled: true, variant: "on", payload: null, reason: "rollout" },
|
"fc.promo": { enabled: true, variant: "on", payload: null, reason: "rollout" },
|
||||||
"fc.search_ranking_v3": { enabled: false, variant: "off", payload: null, reason: "default" }
|
"fc.promo-codes": { enabled: true, variant: "on", payload: null, reason: "rollout" },
|
||||||
|
"fc.ios": { enabled: true, variant: "on", payload: null, reason: "audience" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -118,7 +119,7 @@ function createUpstreamStub(options = {}) {
|
|||||||
function bootstrapBody() {
|
function bootstrapBody() {
|
||||||
return {
|
return {
|
||||||
environment: "production",
|
environment: "production",
|
||||||
keys: ["fc.checkout_v2", "fc.search_ranking_v3"],
|
keys: ["fc.promo", "fc.promo-codes", "fc.ios"],
|
||||||
context: {
|
context: {
|
||||||
subjectType: "customer",
|
subjectType: "customer",
|
||||||
subjectId: "cust_123",
|
subjectId: "cust_123",
|
||||||
@@ -151,9 +152,10 @@ test("bootstrap sucesso retorna flags simplificadas e raw", async () => {
|
|||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
assert.equal(json.ok, true);
|
assert.equal(json.ok, true);
|
||||||
assert.equal(json.configVersion, 7);
|
assert.equal(json.configVersion, 7);
|
||||||
assert.equal(json.flags.checkoutV2, true);
|
assert.equal(json.flags.promo, true);
|
||||||
assert.equal(json.flags.searchRankingV3, "off");
|
assert.equal(json.flags.promoCodes, true);
|
||||||
assert.ok(json.raw["fc.checkout_v2"]);
|
assert.equal(json.flags.ios, true);
|
||||||
|
assert.ok(json.raw["fc.promo"]);
|
||||||
assert.equal(upstream.state.evaluatePayload.context.subjectId, "cust_123");
|
assert.equal(upstream.state.evaluatePayload.context.subjectId, "cust_123");
|
||||||
} finally {
|
} finally {
|
||||||
await bff.stop();
|
await bff.stop();
|
||||||
@@ -170,7 +172,7 @@ test("bootstrap com 429 devolve fallback sem quebrar cliente", async () => {
|
|||||||
ATOMENTA_ORIGIN: `http://127.0.0.1:${upstreamPort}`,
|
ATOMENTA_ORIGIN: `http://127.0.0.1:${upstreamPort}`,
|
||||||
FEATURE_CONTROL_MODULE_TOKEN: "token",
|
FEATURE_CONTROL_MODULE_TOKEN: "token",
|
||||||
FEATURE_CONTROL_DEFAULTS_JSON: JSON.stringify({
|
FEATURE_CONTROL_DEFAULTS_JSON: JSON.stringify({
|
||||||
"fc.checkout_v2": { enabled: false, variant: "off" }
|
"fc.promo": { enabled: false, variant: "off" }
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -184,7 +186,7 @@ test("bootstrap com 429 devolve fallback sem quebrar cliente", async () => {
|
|||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
assert.equal(json.source, "fallback");
|
assert.equal(json.source, "fallback");
|
||||||
assert.equal(json.upstreamError.status, 429);
|
assert.equal(json.upstreamError.status, 429);
|
||||||
assert.equal(json.flags.checkoutV2, "off");
|
assert.equal(json.flags.promo, "off");
|
||||||
} finally {
|
} finally {
|
||||||
await bff.stop();
|
await bff.stop();
|
||||||
await closeServer(upstream.server);
|
await closeServer(upstream.server);
|
||||||
@@ -234,14 +236,14 @@ test("telemetria de exposure encaminha lote para upstream", async () => {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
events: [
|
events: [
|
||||||
{ featureKey: "fc.checkout_v2", variant: "on", subjectType: "customer", storeId: "store_1" }
|
{ featureKey: "fc.promo", variant: "on", subjectType: "customer", storeId: "store_1" }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
assert.equal(response.status, 200);
|
assert.equal(response.status, 200);
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
assert.equal(json.ok, true);
|
assert.equal(json.ok, true);
|
||||||
assert.equal(upstream.state.exposurePayload.events[0].featureKey, "fc.checkout_v2");
|
assert.equal(upstream.state.exposurePayload.events[0].featureKey, "fc.promo");
|
||||||
} finally {
|
} finally {
|
||||||
await bff.stop();
|
await bff.stop();
|
||||||
await closeServer(upstream.server);
|
await closeServer(upstream.server);
|
||||||
|
|||||||
@@ -258,12 +258,12 @@ final class FeatureControlService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func featureKeys() -> [String] {
|
private func featureKeys() -> [String] {
|
||||||
let raw = ProcessInfo.processInfo.environment["FEATURE_CONTROL_KEYS"] ?? "fc.checkout_v2,fc.search_ranking_v3"
|
let raw = ProcessInfo.processInfo.environment["FEATURE_CONTROL_KEYS"] ?? "fc.promo,fc.promo-codes,fc.android,fc.city-aguai,fc.ios"
|
||||||
let items = raw
|
let items = raw
|
||||||
.split(separator: ",")
|
.split(separator: ",")
|
||||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
.filter { $0.isEmpty == false }
|
.filter { $0.isEmpty == false }
|
||||||
return items.isEmpty ? ["fc.checkout_v2"] : items
|
return items.isEmpty ? ["fc.ios"] : items
|
||||||
}
|
}
|
||||||
|
|
||||||
private func platformName() -> String {
|
private func platformName() -> String {
|
||||||
|
|||||||
Reference in New Issue
Block a user