DEV Community

Marcus Dorbação
Marcus Dorbação

Posted on • Edited on

Feature Flags - Thinking

Sistema de Feature Flags — Documentação Técnica

Documentação em construção. Público-alvo: devs do time (uso técnico/API).
Sistema avançado: rollout percentual, segmentação de usuários e A/B test.

Índice

  1. Visão geral
  2. Core de flags
  3. Targeting e segmentação
  4. Rollout progressivo
  5. A/B testing / Experimentação
  6. SDKs e integração técnica
  7. Autenticação (SDK Key)
  8. Sincronização em tempo real
  9. Eventos de conexão
  10. Webhooks (integração externa)
  11. Fluxo completo de evaluate()
  12. Decisões de arquitetura
  13. Modelo de dados
  14. Em aberto

Visão geral

Sistema de feature flags construído do zero, com suporte a:

  • Flags booleanas e multivariáveis
  • Segmentação por atributos de usuário
  • Rollout percentual com bucketing consistente
  • A/B testing com múltiplas variantes
  • SDKs client-side e server-side

Core de flags

  • Tipos de flag: boolean (on/off) e multivariável (string/número/JSON), permitindo retornar variantes diferentes, não só ligar/desligar
  • Ambientes: dev, staging, produção — cada um com seu próprio estado de flag
  • Kill switch: desligar uma flag instantaneamente em produção, sem deploy

Targeting e segmentação

  • Regras de segmentação: por atributo do usuário (país, plano, versão do app, device, etc.)
  • Targeting individual: ligar a flag para usuários específicos (por ID, email)
  • Segments: listas nomeadas de usuários (ex: beta testers, funcionários internos). Terminologia alinhada ao padrão de mercado (LaunchDarkly usa "segments"; "cohort" é o termo equivalente do lado de ferramentas de analytics como Amplitude, e os dois conceitos costumam ser sincronizados entre as duas categorias de ferramenta)

Rollout progressivo

  • Rollout percentual: liberar gradualmente para uma fatia da base (5%, 10%, 50%...)
  • Sticky bucketing: garante que o mesmo usuário sempre caia na mesma variante entre sessões
  • Hash consistente: normalmente hash do user ID + flag key para decidir o bucket
  • Bucketing, definição: mecanismo de dividir a base de usuários em fatias numeradas (0-99) de forma determinística e recalculada a cada requisição — não há cadastro prévio de "usuário está no grupo X" guardado em banco. A mesma entrada (userId + flagKey) sempre produz a mesma saída, o que gera o efeito "sticky" sem precisar guardar estado por usuário. Incluir a flagKey no hash (e não só o userId) evita correlação indesejada entre o bucket de um usuário em flags diferentes

A/B testing / Experimentação

  • Múltiplas variantes com pesos configuráveis (ex: A 33%, B 33%, C 34%)
  • Integração com analytics: disparar eventos de exposição (quem viu qual variante)
  • Significância estatística geralmente delegada a uma ferramenta de análise externa, mas o sistema de flags precisa expor os dados de exposição

SDKs e integração técnica

SDK client-side vs server-side

A diferença não é só "onde roda o código" — é uma questão de segurança e vazamento de informação.

Server-side SDK

  • Roda em ambiente confiável (backend)
  • Pode ter acesso a todas as flags, incluindo as não lançadas, regras de segmentação completas e segredos usados no targeting
  • Comunica-se direto com a API central ou via um Relay Proxy (serviço intermediário que reduz chamadas repetidas)

Client-side SDK (browser, mobile, apps)

  • Roda em ambiente não confiável — qualquer um pode inspecionar o payload
  • Nunca deve receber a lista completa de flags. O backend avalia a flag para aquele usuário específico e envia só o payload já resolvido (ex: flag X = true, flag Y = variante B)
  • Isso é feito via endpoint do tipo /sdk/eval?context={user}, retornando um payload minimalista
  • Regra crítica: nunca usar a SDK key server-side (completa) dentro de um app client — isso vazaria todas as flags e regras internas

Avaliação local vs remota

Abordagem Latência Frescor dos dados
Remota (thin client) Alta — chamada de rede a cada checagem Sempre atualizado
Local (thick client, recomendada) Baixa — avaliação em memória Depende da estratégia de sync

Estratégias de atualização do cache local:

  • Polling: SDK pergunta "tem mudança?" a cada N segundos
  • Streaming: servidor empurra updates em tempo real assim que uma flag muda

Muitos SDKs combinam os dois: streaming como canal principal + polling como fallback caso a conexão caia.

Fallback / default values

  • Todo flag check exige um valor default obrigatório na chamada (nunca deixar o SDK "adivinhar")
  • Hierarquia de fallback:
    1. Valor em cache local (último snapshot conhecido)
    2. Se nunca conectou → default fornecido no código de chamada
  • Falha do serviço de flags nunca deve travar a aplicação — deve ser silenciosa e logada
  • Timeout de inicialização: tempo máximo de espera no boot para o primeiro fetch (ex: 5s); depois disso, segue com defaults e atualiza quando possível
  • Emitir métrica/log quando o SDK cai em modo fallback, para sinalizar degradação do serviço

Autenticação (SDK Key)

A SDK key funciona como um bearer token:

Authorization: Bearer sdk-a1b2c3d4...
Enter fullscreen mode Exit fullscreen mode
Elemento Papel
SDK key (bearer token) Autentica + resolve escopo (projeto/ambiente) — não participa da decisão de segmentação
Context (key, plan, country...) Fornece os dados para a decisão de segmentação e rollout

Exemplo de request:

POST /evaluate
Headers: { "Authorization": "Bearer sdk-a1b2c3d4..." }

Body: {
  "flagKey": "cancelar-nota-fiscal",
  "context": {
    "key": "user-12345",
    "plan": "enterprise",
    "country": "BR"
  }
}
Enter fullscreen mode Exit fullscreen mode

Por que não expor projectId/environment como parâmetros públicos

Alternativa descartada: passar ?project=app-mobile&env=production abertamente, sem token.

Problemas dessa abordagem:

  1. Enumeração — projectId previsível permite testar/descobrir outros projetos/ambientes
  2. Sem autenticação — impossível distinguir chamada legítima de bisbilhotagem
  3. Vazamento de lógica de negócio — testar contexts publicamente permite reconstruir regras de segmentação por engenharia reversa
  4. Sem revogação/rotação — sem token não há "chave" para cortar acesso; seria preciso mudar a própria estrutura da API
  5. Sem rate limiting/billing por cliente — impossível medir uso ou isolar consumidores
  6. DoS mais fácil — sem identificação, não dá para aplicar throttling seletivo
  7. Mistura acidental de ambientes — sem credencial vinculante, é mais fácil um client de produção acessar staging por engano

Sincronização em tempo real

Por que webhook não serve como mecanismo de sync do SDK

Webhook exige que o servidor inicie uma conexão de saída até o cliente — o que não funciona para a maioria dos consumidores reais de SDK:

  • Frontend/browser: sem endereço público
  • App mobile: sem IP público, sem porta aberta
  • Backend atrás de NAT/firewall corporativo: porta não exposta
  • Serverless/Lambda: função só existe durante a execução
  • Múltiplas réplicas atrás de load balancer: ambíguo para qual réplica enviar

Por isso, o padrão adotado (como na maioria dos sistemas de feature flag do mercado) é o inverso: o cliente inicia a conexão de saída (streaming ou polling) e mantém ela aberta.

SSE como alternativa ao WebSocket

WebSocket é bidirecional e mais pesado que o necessário — o caso de uso aqui é apenas server → client.

SSE (Server-Sent Events) cobre esse caso:

  • Conexão HTTP simples, unidirecional
  • Reconexão automática nativa
  • Funciona sobre HTTP/1.1 comum, sem upgrade de protocolo
  • Mais simples de implementar mantendo tempo real

Fluxo de autenticação SSE:

SDK Client → API (Auth): POST /auth (org, app, ambiente)
API (Auth) → SDK Client: SDK Key (bearer token)
SDK Client → Sync Service (SSE): GET /stream (Authorization: Bearer ...)
Sync Service → SDK Client: snapshot inicial de flags
[flag alterada no painel]
Sync Service → SDK Client: evento SSE (update de flag)
SDK Client: atualiza cache local
Enter fullscreen mode Exit fullscreen mode

Eventos de conexão

SdkConnected

Dispara quando o SDK abre a conexão de streaming (SSE/WebSocket) e o token é validado com sucesso. Não é só "TCP conectou" — é "conexão autenticada e pronta para receber updates daquele escopo (org/app/ambiente)".

SdkDisconnected

Dispara quando a conexão cai, por um de três motivos (importante diferenciar no payload):

  1. Fechamento gracioso — shutdown, deploy, scale-down
  2. Timeout/heartbeat perdido — SDK parou de responder ao keep-alive
  3. Erro de rede — queda abrupta

Diferenciar o motivo importa para observabilidade: uma queda maciça de conexões pode ser "todo mundo fez deploy ao mesmo tempo" (normal) ou "o serviço de streaming caiu" (crítico).

Pontos de atenção:

  • Reconexões automáticas com backoff podem gerar ruído (SdkDisconnected → SdkConnected repetidos) — considerar debounce/agregação nas métricas
  • Incluir um connectionId único por sessão de streaming para correlacionar conexão/desconexão e calcular duração

Usos práticos:

  • Observabilidade — quantos SDKs ativos por ambiente
  • Detecção de degradação — queda anormal indica problema no Sync service
  • Billing/capacity planning — se cobrança for por conexões simultâneas

Payload — SdkConnected

{
  "eventType": "SdkConnected",
  "connectionId": "conn_9f8a3b2c",
  "organizationId": "org_123",
  "applicationId": "app_456",
  "environment": "production",
  "sdkKeyId": "sdkkey_789",
  "sdkVersion": "js-server@2.4.1",
  "transport": "sse",
  "connectedAt": "2026-07-16T14:32:10Z"
}
Enter fullscreen mode Exit fullscreen mode
  • connectionId: chave de correlação entre o evento de conexão e o de desconexão — sem ele não dá pra calcular duração nem saber a qual conexão um disconnect se refere
  • sdkKeyId: identificador do token, nunca a SDK key crua — logar o token completo num evento de observabilidade seria vazamento de credencial em qualquer pipeline de logs/analytics
  • sdkVersion: linguagem + versão do SDK — essencial pra saber quais versões estão em uso ao investigar bugs ou depreciar releases antigas
  • transport: canal usado (sse, websocket, polling-fallback) — ajuda a identificar se alguma fatia de clients caiu pra polling por problema de rede

Payload — SdkDisconnected

{
  "eventType": "SdkDisconnected",
  "connectionId": "conn_9f8a3b2c",
  "organizationId": "org_123",
  "applicationId": "app_456",
  "environment": "production",
  "sdkKeyId": "sdkkey_789",
  "disconnectedAt": "2026-07-16T15:10:42Z",
  "durationSeconds": 2312,
  "reason": "heartbeat_timeout",
  "reasonDetail": "no keep-alive response in 30s"
}
Enter fullscreen mode Exit fullscreen mode
  • durationSeconds: calculado no momento do evento (disconnectedAt - connectedAt) — poupa quem consome o evento de cruzar os dois eventos manualmente
  • reason: enum fechado (graceful, heartbeat_timeout, network_error) — nunca texto livre, pra permitir agregação confiável em dashboards
  • reasonDetail: campo livre opcional, só pra debug humano — nunca usado em lógica/agregação

Agregação de reconexões: se um novo SdkConnected chegar com o mesmo sdkKeyId dentro de uma janela curta (ex: 5s) após um SdkDisconnected, o pipeline de métricas pode agregar os dois como "reconexão" em vez de "queda + nova sessão" — evita inflar o contador de desconexões em cenários normais de instabilidade de rede.

Decisão — sem IP nos eventos: os eventos de conexão não carregam IP do client. Segmentação por IP, se necessária no futuro, é responsabilidade do client incluir como atributo no context da chamada de evaluate() — igual qualquer outro atributo (país, plano, device). O sistema de flags não infere nem captura dado de rede por conta própria; só decide com base no que recebe explicitamente.


Webhooks (integração externa)

Webhook não é o mecanismo de sincronização do SDK, mas é útil como feature de integração opcional, quando o receptor é um serviço backend do próprio cliente, controlado por eles, com endpoint público de propósito.

Exemplos de uso:

  • Notificar o sistema de CI/CD do cliente quando uma flag mudar em produção
  • Sincronizar com um sistema de config interno do cliente

Modelo proposto: URL de webhook registrada no momento da autenticação (junto com organização/aplicação/ambiente), salva em uma tabela de webhooks associada à chave/token.

Escopo do webhook

O webhook usado aqui serve exclusivamente para notificar que uma flag mudou — não carrega o novo valor no payload, apenas o sinal de que algo mudou naquela flag/ambiente. O cliente, ao receber o evento, decide se e quando revalidar o estado real via evaluate()/sdk/eval.

Escopo do registro: Application + Environment (não atrelado a uma SdkKey específica, já que a chave de SDK tem ciclo de vida de rotação/revogação próprio, diferente da vida útil de uma integração de webhook).

Webhook {
  id
  applicationId
  environmentId
  url
  status          // active | disabled
  createdAt
}
Enter fullscreen mode Exit fullscreen mode

Payload

{
  "eventType": "FLAG_CHANGED",
  "applicationId": "app_456",
  "environmentId": "env_789",
  "flagKey": "cancelar-nota-fiscal",
  "changedAt": "2026-07-17T10:15:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Sinal genérico, sem o valor da flag — o cliente sempre revalida o estado real depois de receber o evento; o webhook nunca é a fonte de verdade do novo valor.

Por que não usar assinatura (HMAC/secret)

Como o payload não carrega o valor da flag, forjar essa notificação não tem efeito real além de fazer o cliente revalidar seu estado à toa (uma chamada extra desnecessária, não uma injeção de dado falso). Diferente de um webhook que carregasse o valor diretamente — nesse caso a assinatura seria obrigatória. Aqui, o cliente é livre para descartar/ignorar disparos que considerar excessivos.

Rate limiting por webhook — com colapso de eventos

Rate limit aqui não é sobre validade temporal da requisição — é um teto de frequência de disparo, para impedir que uma rajada de edições na mesma flag vire uma rajada de chamadas HTTP contra a url cadastrada (o que tornaria o próprio serviço de flags um vetor de flood/DoS contra terceiros, caso alguém cadastre a URL de uma vítima).

Estratégia adotada: dentro de uma janela de tempo (ex: 1 evento a cada N segundos por webhook), múltiplas mudanças na mesma flag são colapsadas em um único disparo — já que o payload não informa o valor, não há perda de informação relevante em notificar uma vez só "essa flag mudou" em vez de uma vez por edição.

Validação de URL no cadastro (anti-SSRF)

Ao registrar um webhook, validar que a url não aponta para endereços internos (localhost, ranges de IP privado, endereço de metadata de nuvem como 169.254.169.254) — evita que o cadastro de um webhook seja usado para fazer o próprio serviço de flags acessar rede interna que não deveria alcançar.

Entrega e retry — WebhookDelivery

Chamada HTTP para endpoint de terceiro pode falhar (timeout, 5xx, DNS fora do ar), então cada tentativa de entrega é registrada separadamente da configuração do webhook:

WebhookDelivery {
  id
  webhookId
  payload          // snapshot do corpo enviado
  attempt          // número da tentativa (1, 2, 3...)
  httpStatus
  success
  deliveredAt
}
Enter fullscreen mode Exit fullscreen mode

Composição com Webhook (a entrega nasce vinculada ao webhook que a gerou), mas como registro histórico segue o mesmo critério do ExposureEvent/ConnectionSession: pode ser retido para fins de auditoria mesmo que o webhook seja depois editado ou desativado.

Política de retry (fase inicial): backoff exponencial com limite de tentativas (ex: 3 tentativas — 1min, 5min, 30min), depois marca como failed definitivamente e loga para investigação manual. Sem fila de retry infinita.


Fluxo completo de evaluate()

Pipeline de resolução de token → busca da flag → segmentação → rollout → resultado.

Visão geral do pipeline

Request → Auth (SDK key) → Resolve escopo (org/app/env)
        → Buscar definição da flag (cache local ou remoto)
        → Flag existe e está ativa?
        → Avaliar regras (ordem de precedência)
        → Bucketing (rollout % / variante A-B)
        → Montar resultado + reason
        → Emitir evento de exposição (async)
        → Retornar resposta
Enter fullscreen mode Exit fullscreen mode

1. Autenticação e resolução de escopo

  • Validar o SDK key (bearer token) contra o serviço de auth/cache de tokens
  • Token inválido/revogado → 401, encerra o pipeline aqui (sem chegar a tocar em dados de flag)
  • Token válido → resolve { organizationId, applicationId, environment } vinculados àquele token
  • Esse escopo é o que delimita quais flags podem ser buscadas no próximo passo — o context da request nunca escolhe o ambiente, só o token

2. Busca da definição da flag

  • Chave de busca: (organizationId, applicationId, environment, flagKey)
  • Fonte: cache local em memória (thick client) → se miss, busca remota → se a flag não existir, cai no fallback (ver seção de Fallback / default values)
  • Flag encontrada, mas kill switch ligado → resultado imediato = valor "off" configurado, reason: "OFF". Nenhuma regra é avaliada depois disso — ver Decisões de arquitetura sobre precedência do kill switch

3. Ordem de precedência das regras

Ordem Regra Comportamento
1 Kill switch / flag off Curto-circuito — retorna valor off, ignora tudo abaixo
2 Targeting individual Se context.key (ou email) está na lista de override individual → retorna o valor daquele override
3 Segments Se o usuário pertence a um segment com regra própria → aplica o valor definido para aquele segment
4 Regras de segmentação Avaliadas em ordem definida na flag (primeira regra que casar com os atributos do context vence)
5 Rollout percentual (fallthrough) Se nenhuma regra acima casou, aplica o bucketing por hash consistente
6 Default da flag Se nem o rollout resolver, usa o default definido na própria flag — não confundir com o default passado pelo SDK na chamada, que só entra em jogo se a flag inteira não for alcançável

Cada resultado carrega um campo reason (ex: TARGET_MATCH, RULE_MATCH, FALLTHROUGH, OFF, DEFAULT) — essencial para debugar "por que esse usuário caiu nessa variante".

4. Bucketing (rollout % e variantes A/B)

  • Hash consistente: hash(userId + flagKey) % 100 → determina o bucket 0-99
  • Rollout percentual: compara o bucket contra o threshold configurado (ex: 30% → buckets 0-29 entram)
  • A/B com múltiplas variantes: o range 0-99 é dividido proporcionalmente aos pesos (A 33% = buckets 0-32, B 33% = 33-65, C 34% = 66-99)
  • Usar sempre o mesmo algoritmo de hash em todos os SDKs (client e server) — se um SDK usa hash diferente do outro, o mesmo usuário pode cair em variantes diferentes dependendo de onde a avaliação rodou, quebrando a sticky bucketing

5. Fallback (quando a busca da flag falha)

  1. Cache local (último snapshot conhecido) — se a flag existe no cache mas o serviço remoto está fora, usa o cache
  2. Se nunca conectou (sem cache algum) → default fornecido no código de chamada, reason: "ERROR" ou "CLIENT_NOT_READY"
  3. Logar/emitir métrica sempre que cair aqui, para não mascarar degradação do serviço

6. Montagem do resultado

{
  "value": true,
  "variant": "control",
  "reason": "RULE_MATCH",
  "ruleId": "rule_beta_countries_br"
}
Enter fullscreen mode Exit fullscreen mode
  • value: o valor resolvido (bool, string, número ou JSON)
  • variant: nome da variante (relevante em multivariável/A-B)
  • reason: motivo da decisão (debugging)
  • ruleId: qual regra específica decidiu (opcional, mas ajuda muito em suporte/debug)

Exposição desse payload completo é restrita — ver Decisões de arquitetura.

7. Evento de exposição

  • Disparado de forma assíncrona (não pode bloquear a resposta do evaluate())
  • Contém: flagKey, variant, context.key, timestamp, reason
  • Vai para o pipeline de analytics — é o dado bruto que alimenta significância estatística de A/B test (calculada fora do sistema de flags)

8. Diferença entre evaluate() single e /sdk/eval bulk

  • evaluate() single: avalia uma flag específica para um context — típico de uso server-side, ou de debug/teste manual
  • /sdk/eval?context=: avalia todas as flags do ambiente para aquele context de uma vez, retornando o payload minimalista já resolvido — é o que o client-side SDK chama no boot e a cada refresh, evitando N chamadas (uma por flag)
  • Internamente, /sdk/eval roda o mesmo pipeline acima em loop para cada flag do ambiente, mas com uma otimização: busca todas as definições de flag de uma vez (evita N round-trips ao cache/DB)

Decisões de arquitetura

1. Exposição de reason / ruleId no payload

Decisão: o payload completo (reason, ruleId) só é exposto em evaluate() server-side / telas de debug autenticadas. O endpoint client-side (/sdk/eval?context=) devolve só value + variant.

Motivo: expor ruleId/reason no client permite que qualquer um inspecionando o response reconstrua a lógica de segmentação interna (ex: descobrir que existe uma regra específica por país) — o mesmo tipo de vazamento de lógica de negócio que já motivou a decisão de não expor project/env como parâmetros públicos.

2. Kill switch — precedência absoluta

Decisão: o kill switch não é uma regra na lista de precedência de targeting/segmentação/rollout. É um curto-circuito que roda antes de qualquer avaliação de regra, e vence sempre, incondicionalmente — inclusive sobre targeting individual (ex: um QA com override pessoal não continua vendo a flag "on" se o kill switch foi acionado).

Motivo: kill switch é a válvula de emergência do sistema. Se pudesse ter exceções, deixaria de cumprir sua função de parar tudo, sem exceção, sem deploy.

Mecânica:

  • No painel de controle, apertar o botão é uma escrita simples no mesmo lugar de outras configs da flag (status: "killed")
  • A diferença está na prioridade de leitura: evaluate() checa isso primeiro; se killed, nem entra no loop de regras
  • Propagação depende do canal de sync: SDKs com streaming (SSE) ativo recebem o update quase instantaneamente; SDKs em modo polling ou desconectados (fallback em cache local) só verão o kill switch na próxima reconexão/poll — limitação conhecida, não um bug

3. Versionamento de algoritmo de hash — não implementar agora

Decisão: não implementar um registry de múltiplas versões de hash (hashVersion por flag) neste momento. Se o algoritmo de hash for trocado no futuro, os buckets de usuários existentes vão simplesmente mudar — novas decisões serão produzidas a partir da nova conta, sem tentar preservar as antigas.

Motivo: o sistema ainda está em fase de construção, sem testes A/B ativos que dependam de continuidade estatística entre trocas de algoritmo. Implementar registry versionado agora (com todo SDK de toda linguagem precisando reimplementar todas as versões de hash) é custo de engenharia prematuro para um problema que ainda não existe. Trocar hash é evento raro e deliberado — deve ser comunicado como breaking change, não silenciosamente absorvido.

Nota de escopo: essa decisão vale para o estágio atual do projeto. Se no futuro o sistema amadurecer com testes A/B rodando continuamente, essa decisão deve ser revisitada.

4. Verificação client-side nunca é autoritativa

Cenário motivador: uma interface desktop exibe um botão condicionado a uma flag. Se a conexão de streaming cair e o client não for notificado de um kill switch, o botão continua visível com a última informação em cache — até o momento em que o usuário efetivamente submete a ação.

Decisão: toda flag que guarda uma ação com efeito real no backend deve ser re-verificada no servidor no momento da ação, independente do que o client mostrou. A avaliação client-side (cache local) serve só para UX responsiva (mostrar/esconder elementos); nunca é a fonte de verdade.

Fluxo:

Client (cache desatualizado) → mostra botão → usuário clica
        ↓
API recebe a submissão → roda evaluate() fresco, contra o estado atual
        ↓
Kill switch ativo agora → API rejeita/ignora a operação
        ↓
API responde ao client (ex: 403 / "feature indisponível")
        ↓
Client pode usar essa resposta como sinal indireto para invalidar seu cache local,
sem esperar a próxima reconexão de streaming
Enter fullscreen mode Exit fullscreen mode

Motivo: mesmo princípio de nunca confiar em validação só no front-end — o client pode estar desatualizado, ou ser manipulado. A flag no client é sugestão de experiência; a flag no server é o portão real.

Formato da resposta de rejeição

Código HTTP 403 Forbidden — a requisição está bem formada e autenticada, só não é permitida pelo estado atual da flag (não é 400, não é erro de input; não é 404, o recurso existe).

{
  "error": "FEATURE_UNAVAILABLE",
  "reason": "RULE_MATCH",
  "flagKey": "cancelar-nota-fiscal"
}
Enter fullscreen mode Exit fullscreen mode
  • error: código fixo e genérico (FEATURE_UNAVAILABLE) — permite o client reconhecer programaticamente que a rejeição foi por causa de uma flag, diferenciando de outros motivos de 403 que a API possa ter (ex: permissão de usuário)
  • reason: reaproveita o mesmo enum já usado no resultado de evaluate() (OFF, TARGET_MATCH, RULE_MATCH, FALLTHROUGH) — sem vocabulário novo só pra esse caso. Expor a categoria da decisão é seguro e útil: OFF sugere que é sistêmico/temporário (kill switch geral), enquanto RULE_MATCH/FALLTHROUGH sugere que o usuário não é elegível agora — o client pode adaptar a UX de acordo (tentar de novo mais tarde vs não insistir)
  • flagKey: qual flag causou a rejeição — permite o client invalidar exatamente essa flag no cache local, sem esperar a próxima reconexão de streaming

O que fica de fora, deliberadamente: ruleId, attribute, operator, value — o client sabe que categoria de decisão aconteceu (ex: foi uma regra de segmentação), mas não sabe qual atributo ou valor causou isso (ex: não sabe que foi porque country != BR). Isso preserva a distinção entre categoria da decisão (seguro expor) e conteúdo da regra de negócio (não deve vazar), mantendo a mesma postura da Decisão 1.

5. ruleId sem versionamento

Decisão: ruleId não é versionado. É uma referência estável à regra, cujo conteúdo pode ser editado ao longo do tempo sem gerar um novo identificador. ExposureEvent e o resultado de evaluate() guardam apenas o ruleId, não um snapshot da condição avaliada naquele momento.

Limitação conhecida e aceita: se uma regra for editada depois de gerar exposições, o histórico de auditoria não reflete com precisão o que a regra dizia no momento da avaliação — o ruleId de um evento antigo passa a apontar para o conteúdo atual da regra, não para o que ela era quando o evento aconteceu.

Motivo: mesma lógica já aplicada à decisão de não versionar o algoritmo de hash — construir uma trilha de auditoria histórica precisa (versionamento de regra estilo git, ou snapshot embutido no evento) é engenharia adicional para um problema que, no estágio atual do projeto, ainda não se manifestou como necessidade real. Aceitar a limitação agora é mais barato do que resolver um requisito que ainda não apareceu.

Resumo das decisões

Item Decisão
reason/ruleId Só em evaluate() server-side/debug; nunca em /sdk/eval client
Kill switch Curto-circuito com precedência absoluta, fora da lista de regras
Hash versionado Não implementar agora; troca de hash = breaking change documentado
Verificação client vs server Toda ação com efeito real precisa de evaluate() fresco no servidor no momento da ação
Versionamento de ruleId Não implementado; histórico de auditoria pode ficar impreciso após edição de regras — limitação aceita

Modelo de dados

Entidades do domínio e suas relações, em terminologia UML (associação, composição, classe de associação).

Organization

Tenant raiz — representa o cliente/empresa que usa a plataforma.

  • Composição com Application (1 organização possui N aplicações; aplicação não existe fora de uma organização)

Application

Um produto/app específico do cliente (ex: "app mobile", "backend web").

  • Composição com Environment (ambiente não existe fora de uma aplicação)
  • Composição com Segment (segments são definidos no escopo da aplicação)
  • Composição com FlagDefinition (a flag, como conceito, pertence à aplicação)

Environment

Instância de ambiente (dev/staging/produção) dentro de uma aplicação.

  • Composição com SdkKey (chave é emitida para um ambiente específico e não existe fora dele)
  • Participa como um dos lados da classe de associação FlagEnvironmentConfig

SdkKey

Token de autenticação (bearer) que resolve o escopo org/app/ambiente.

  • Composição com Webhook (webhook é registrado atrelado a uma chave; não existe solto)
  • Associação com ConnectionSession (a sessão referencia a chave, mas é um registro de log — pode ser retida mesmo após a chave ser revogada)

Webhook

URL de integração externa registrada no momento da autenticação.

  • Existe apenas como parte de uma SdkKey (composição, conforme acima)

FlagDefinition

A flag em si: chave, tipo (boolean/multivariável), descrição. Conceito estável entre ambientes.

  • Composição com Application
  • Participa como um dos lados da classe de associação FlagEnvironmentConfig

FlagEnvironmentConfig

Classe de associação entre FlagDefinition e Environment. Representa "esta flag, neste ambiente", carregando os atributos que variam por ambiente (status/kill switch, defaultValue, hashVersion). É o motivo de uma flag poder estar "on" em staging e "off" em produção sem duplicar a definição da flag.

  • Composição com Variant, TargetingRule, IndividualOverride (só existem no contexto de uma config específica)
  • Associação com SegmentTargetRule e ExposureEvent

Variant

Uma variante de valor/peso dentro de uma config de flag (ex: "A", 33%).

  • Composição com FlagEnvironmentConfig

TargetingRule

Regra de segmentação ordenada, avaliada dentro de uma config.

  • Composição com FlagEnvironmentConfig
  • Composição com RuleCondition (uma regra é feita de condições; sem elas não tem sentido)

RuleCondition

Condição atômica de uma regra (atributo, operador, valor — ex: country equals "BR").

  • Composição com TargetingRule

Operadores suportados (fase inicial — conjunto fechado, sem comparações como maior/menor):

Operator Formato esperado (máscara) Exemplo de value
equals escalar único (string, número ou bool) "BR"
in array de escalares do mesmo tipo ["BR", "PT", "AO"]
operatorSchema = {
  equals: { shape: "scalar" },
  in:     { shape: "array" }
}
Enter fullscreen mode Exit fullscreen mode
  • O operator funciona como chave que resolve a máscara de validação (shape) esperada para value — o motor de avaliação consulta essa máscara antes de parsear a condição, em vez de um if/else fixo por operador
  • Validação na criação da regra: o backend rejeita uma condição malformada (ex: equals com um array) já no momento do cadastro, sem depender do motor de avaliação pra detectar o erro depois
  • Extensível sem quebra: operadores futuros (greater_than, contains, etc.) só precisam declarar sua própria entrada em operatorSchema — a estrutura de validação não muda, só cresce o dicionário
RuleCondition {
  id
  attribute       // ex: "country", "plan"
  operator        // "equals" | "in" — chave que resolve a máscara de shape
  value           // shape validado contra operatorSchema[operator].shape
}
Enter fullscreen mode Exit fullscreen mode

IndividualOverride

Override de valor para um usuário específico (por ID/email).

  • Composição com FlagEnvironmentConfig

Segment

Lista/grupo nomeado de usuários (ex: beta testers, funcionários internos). Terminologia alinhada ao padrão de mercado (LaunchDarkly).

  • Composição com Application
  • Composição com SegmentMember (a lista de membros só existe dentro do segment)
  • Participa como um dos lados da classe de associação SegmentTargetRule

SegmentMember

Um usuário (userKey) pertencente a um segment.

  • Composição com Segment

SegmentTargetRule

Classe de associação entre Segment e FlagEnvironmentConfig: representa "este segment direciona para este valor, nesta flag/ambiente". Necessário porque o mesmo segment pode ser usado em várias flags, e uma flag pode usar vários segments — é N:N com atributo próprio (resultVariantId).

ExposureEvent

Evento de telemetria: qual usuário viu qual variante, quando, por qual motivo.

  • Associação (não composição) com FlagEnvironmentConfig — registro de log que referencia a config no momento da avaliação, mas com retenção/ciclo de vida independente (a exposição histórica pode ser mantida mesmo que a flag seja depois removida)

ConnectionSession

Registro de uma sessão de streaming (SdkConnectedSdkDisconnected), com connectionId, timestamps e motivo de desconexão.

  • Associação com SdkKey (mesmo raciocínio do ExposureEvent: é log, não parte estrutural)

Critério geral de composição vs associação

  • Composição: quando o filho não tem razão de existir sem o pai e seu ciclo de vida está atrelado (regra sem flag, variante sem config, membro sem segment)
  • Associação: para registros de telemetria/log (ExposureEvent, ConnectionSession) — devem sobreviver independentemente do objeto que os originou, pois são dado histórico, não estrutura viva
  • Classe de associação: quando a relação em si carrega atributos próprios e é N:N entre duas entidades que existem de forma independente (FlagEnvironmentConfig, SegmentTargetRule)

Diagrama de classes


Em aberto

Sem itens pendentes no momento.

Top comments (0)

The discussion has been locked. New comments can't be added.