docs(api): add Feature Control section to API_Mobile_App.md

Full contract for iOS integration:
- POST /feature-control/bootstrap: request/response, context fields,
  flags camelCase mapping table, source/fallback strategy
- POST /feature-control/telemetry/exposure: batch event format
- Available flags reference table (fc.ios, fc.android, fc.promo, etc.)
- Recommended session flow with cache guidance (15 min client-side)
This commit is contained in:
Daniel Arantes Loverde
2026-06-05 16:19:26 -03:00
parent ff1ad1254d
commit 09396ebcf3

View File

@@ -950,4 +950,229 @@ Para dashboard da loja e análise de produto, usar:
- O painel `/store/reviews` segue funcional com os campos legados (`rate`, `message`, `itemFeedback`, `improvementFeedback`, `deliveryFeedback`). - O painel `/store/reviews` segue funcional com os campos legados (`rate`, `message`, `itemFeedback`, `improvementFeedback`, `deliveryFeedback`).
- Campos novos coexistem para evolução gradual sem quebrar o fluxo atual. - Campos novos coexistem para evolução gradual sem quebrar o fluxo atual.
---
## Feature Control (Feature Flags)
O Feature Control permite ativar/desativar funcionalidades do app remotamente sem publicar uma nova versão. O app nunca chama o Atomenta diretamente — usa um **BFF (Backend for Frontend)** que protege o `Atomenta-Token` do cliente.
### Arquitetura
```
App iOS → POST /feature-control/bootstrap → BFF → Atomenta /api/feature-control/evaluate
```
O BFF mantém cache de 60 segundos. Em caso de timeout ou erro upstream, retorna flags de fallback estáticas definidas nas variáveis de ambiente do BFF.
---
### 1. Bootstrap — Avaliar flags na inicialização
**`POST /feature-control/bootstrap`**
Deve ser chamado uma vez por sessão do app (ao abrir ou ao autenticar). Retorna os valores de todos os flags solicitados para o contexto do usuário.
**Headers:**
```
Content-Type: application/json
Authorization: Bearer <JWT_Token> ← opcional, mas recomendado
```
> Se o `Authorization` for enviado, o BFF extrai `subjectId` automaticamente do JWT (`sub` / `customerId` / `id`). Se não for enviado, `context.subjectId` é obrigatório.
**Request body:**
```json
{
"environment": "production",
"keys": [
"fc.ios",
"fc.android",
"fc.promo",
"fc.promo-codes",
"fc.city-aguai"
],
"context": {
"subjectType": "customer",
"subjectId": "e30188cf-cf69-471e-bc59-4c92e0f9ad7b",
"platform": "ios",
"appVersion": "2.3.1",
"attributes": {
"city": "Aguaí"
}
}
}
```
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `environment` | string | não | `"production"` (default) ou `"sandbox"` |
| `keys` | string[] | sim | Lista de flags a avaliar (máx 100) |
| `context.subjectType` | string | sim | `"customer"` \| `"user"` \| `"store"` \| `"anonymous"` |
| `context.subjectId` | string | sim* | ID do usuário. *Dispensável se JWT enviado no header |
| `context.platform` | string | não | `"ios"` ou `"android"` |
| `context.appVersion` | string | não | Versão semântica, ex: `"2.3.1"` |
| `context.storeId` | string | não | Para flags segmentadas por loja |
| `context.attributes` | object | não | Atributos extras (ex: `city`, `tier`) |
**Response 200 — fonte live:**
```json
{
"ok": true,
"source": "live",
"configVersion": 7,
"evaluatedAt": "2026-06-05T12:00:00.000Z",
"flags": {
"ios": true,
"android": true,
"promo": true,
"promoCodes": false,
"cityAguai": true
},
"raw": {
"fc.ios": {
"enabled": true,
"variant": "on",
"payload": null,
"reason": "rollout"
},
"fc.promo-codes": {
"enabled": false,
"variant": "off",
"payload": null,
"reason": "default"
}
}
}
```
**Response 200 — fonte fallback** (quando BFF não consegue atingir Atomenta):
```json
{
"ok": true,
"source": "fallback",
"configVersion": 0,
"evaluatedAt": "2026-06-05T12:00:00.000Z",
"flags": { "ios": true, "promo": true },
"raw": { ... },
"upstreamError": {
"status": 504,
"code": "FEATURE_CONTROL_UPSTREAM_TIMEOUT",
"message": "Timeout no upstream"
}
}
```
#### Como usar `flags` no app
Os flags são retornados em `flags` com chaves **camelCase** derivadas das chaves originais (prefixo `fc.` removido, hífen/underscore viram camelCase):
| Chave original | Chave em `flags` |
|---|---|
| `fc.ios` | `flags.ios` |
| `fc.android` | `flags.android` |
| `fc.promo` | `flags.promo` |
| `fc.promo-codes` | `flags.promoCodes` |
| `fc.city-aguai` | `flags.cityAguai` |
**Regra de leitura:**
- `true` → flag habilitado
- `false` ou `"off"` → flag desabilitado
- Qualquer outro string → variante multivariate (ex: `"variant_a"`, `"variant_b"`)
**Exemplo Swift:**
```swift
if flags["ios"] as? Bool == true {
// mostrar funcionalidade exclusiva iOS
}
```
#### `source` e estratégia de fallback
| `source` | Significado | Ação recomendada |
|---|---|---|
| `"live"` | Dados frescos do Atomenta | Usar normalmente |
| `"fallback"` | BFF usou defaults estáticos (timeout/erro) | Usar com cautela; re-tentar na próxima sessão |
---
### 2. Telemetria de Exposição
**`POST /feature-control/telemetry/exposure`**
Registra quais flags o usuário visualizou (para análise de rollout). Envio em batch, **fire-and-forget** — não bloquear UX aguardando resposta.
**Headers:**
```
Content-Type: application/json
Authorization: Bearer <JWT_Token>
```
**Request body:**
```json
{
"events": [
{
"featureKey": "fc.promo",
"variant": "on",
"subjectType": "customer",
"storeId": "store_1777215327582_akum6"
},
{
"featureKey": "fc.ios",
"variant": "on",
"subjectType": "customer"
}
]
}
```
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `featureKey` | string | sim | Chave original com prefixo `fc.` |
| `variant` | string | não | Variante exposta (default `"off"`) |
| `subjectType` | string | não | Default `"customer"` |
| `storeId` | string | não | Quando relevante |
Máximo de **100 eventos por request**.
**Response 200:**
```json
{
"ok": true,
"code": "FEATURE_CONTROL_EXPOSURE_ACCEPTED",
"result": { "count": 2 }
}
```
---
### 3. Flags disponíveis (referência)
| Flag | Descrição | Default |
|---|---|---|
| `fc.ios` | Funcionalidades exclusivas iOS habilitadas | `true` |
| `fc.android` | Funcionalidades exclusivas Android habilitadas | `true` |
| `fc.promo` | Módulo de promoções ativo | `true` |
| `fc.promo-codes` | Cupons de desconto habilitados | `true` |
| `fc.city-aguai` | Expansão para cidade de Aguaí/SP | `true` |
> Novos flags são criados pelo time Pedi Foods no painel `/feature-control`. Consulte o time antes de codificar um flag inexistente.
---
### 4. Fluxo recomendado no app
```
1. App abre / usuário autentica
2. POST /feature-control/bootstrap (com JWT no header)
3. Salvar flags em memória/UserDefaults para a sessão
4. Renderizar UI baseada nos flags
5. POST /feature-control/telemetry/exposure (fire-and-forget, por flag visualizado)
6. Ao fechar sessão ou após 15 min, repetir passo 2 na próxima abertura
```
> **Não** chamar bootstrap a cada tela — apenas na inicialização da sessão. O BFF tem cache de 60s no servidor; o app deve ter cache local mínimo de 15 minutos para evitar latência desnecessária.
---
Autor: Daniel Arantes Loverde Autor: Daniel Arantes Loverde