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:
@@ -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`).
|
||||
- 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
|
||||
|
||||
Reference in New Issue
Block a user