Feature Toggle | Controle Sua Aplicação em Produção

Tenha total controle de forma rápida e remota sobre um código que já se encontra em produção com essa abordagem simples e super poderosa!

Feature Toggle | Controle Sua Aplicação em Produção
Photo by Steve Johnson / Unsplash

Um feature toggle(também é conhecido como feature flag, feature switch, feature gate ou feature flipper) é uma técnica poderosa que permite às equipes modificarem o comportamento do sistema sem alterar o código.
Uma alternativa para manter múltiplas branches de recursos no código-fonte.
Em ambientes ágeis, o toggle é usado em produção para ativar o recurso sob demanda, para alguns ou todos os usuários, funcionando como rollback para novas features (caso você utilize dessa forma), testes A/B, notificação de manutenção (por favor não use) e muito mais.

Na pratica, uma Feature Toggle é uma variável que podemos ligar e desligar, pensando assim, podemos simplificar com um exemplo de código:

import CartProvider from "~/Providers/Cart"

function getUserCart(){ 
    return CartProvider.getItems()
}
Buscando produtos do carrinho de um usuário X.

A função acima busca os produtos do carrinho do usuário ativo, toda essa regra está abstraída no CartProvider, imaginando que tenhamos outro provider para executar essa tarefa (esse outro provider pode bater em outro endpoint, tem outra implementação, etc), mas que é uma modificação que tem um impacto significativo na aplicação em produção, precisamos de todo cuidado pois essa é uma parte super critica para o funil de vendas.
Tendo em visto esse risco, além da esteira de testes que foi executada em cima da nova implementação, também podemos pensar em um novo fluxo para subir novas features em produção, pensando na implementação do nosso exemplo acima, teríamos o seguinte:

Diagrama demonstrando o fluxo de implementação para utilizar ou não o novo provider no carrinho.

Com o nosso diagrama montado, podemos perceber que antes de buscar os itens do carrinho, validamos se a Flag/Feature está habilitada para usar o novo provider ou não, e só depois disso vamos partir para executar a implementação correspondente.
Vamos ver isso no código, refatorando o primeiro exemplo:

import CartProvider from "~/Providers/Cart"
import CartProviderV2 from "~/Providers/CartV2"

function getUserCart(){
    const useNewProvider = true
    
    if(useNewProvider){
        return CartProviderV2.getItems()
    }
    
    return CartProvider.getItems()
}

A nossa implementação agora verifica se devemos usar o novo provider antes de chamar a função getItens, agora temos um versionamento direto no nosso código, onde podemos mudar rapidamente para versões diferentes da feature.
Porém, essa variável hardcoded ainda não é de grande ajuda, ela ainda seria util em tempo de desenvolvimento, alguém pode ir lá e mudar para false e seguir com seus testes/desenvolvimento, porém, podemos deixar isso muito mais ágil buscando esse valor de forma mais externa ao código, podemos fazer isso de diversas maneiras, mas vou exemplificar com duas; Variáveis de Ambiente e Busca remota.

Variável de ambiente

Podemos busca o valor em tempo de build/run dessa aplicação lendo os valores das variáveis de ambiente que podemos fixar no Sistema Operacional ou informar ela antes da execução da aplicação. Essa abordagem é muito util quando se tem um programa que temos features especificas para cada cliente ou mesmo temos um pacote de features que são oferecidas como um plano, onde cada opção contempla X funcionalidades, no Podcast Fronteiras da Engenharia de Software (que eu sou fã), tem um episódio falando sobre "Sistemas altamente configuráveis" e o assunto abordado casa perfeitamente com essa postagem.
A nossa implementação fica da seguinte forma:

import CartProvider from "~/Providers/Cart"
import CartProviderV2 from "~/Providers/CartV2"

function getUserCart(){
    const useNewProvider = process.env.MY_APP_CART_PROVIDER_V2 || false
    
    if(useNewProvider){
        return CartProviderV2.getItems()
    }
    
    return CartProvider.getItems()
}

Por meio da variável de ambiente MY_APP_CART_PROVIDER_V2 eu vou recuperar o valor para poder utilizar ou não o novo provider, fornecendo também um false utilizando o || para caso não encontre o valor (aqui é muito importante a padronização do nome, quando estamos trabalhando com esse tipo de variável, estamos junto a um mar de outras, tanto do proprio OS quanto de outras aplicações).
Essa é uma abordagem muito utilizada também para versionamento de versões, um exemplo é a Canary release, onde podemos utilizar a base mais estável e atualizada do programa e trabalhar em novas features adicionando sempre os nossos toggles, para que na hora do Build possamos distinguir qual será utilizada.
Rodando a aplicação com o novo provider ativo:

MY_APP_CART_PROVIDER_V2=true node app.js
Comando para executar no terminal.

Nesse exemplo não utilizei nenhuma biblioteca para gerenciar minhas variáveis de ambiente, porém você pode utilizar algo como o dotenv se julgar necessário.

Busca Remota

Uma solução que podemos usar junto à o nosso Feature Toggle é a busca dos valores de forma remota, utilizando uma requisição HTTP ou via socket por exemplo. Fazendo assim nossa solução ficar bem mais interessante e controlável principalmente em produção.

Imaginando que no nosso exemplo, vamos querer fazer um teste A/B para coletar métricas e validar se o novo provider de carrinho é melhor que o primeiro, então vamos fazer algo super simples, vamos criar um endpoint HTTP que retorna o valor para utilizarmos, algo como GET /features/. Atualizando nosso exemplo:

import CartProvider from "~/Providers/Cart"
import CartProviderV2 from "~/Providers/CartV2"

async function getUserCart(){
    const featuresResponse = await fetch("/features/")
    const features = await featuresResponse.json()
        
    const useNewProvider = features.cartProviderV2 || false
    
    if(useNewProvider){
        return CartProviderV2.getItems()
    }
    
    return CartProvider.getItems()
}
Para que o exemplo não fique grande, não tratei exceções nem status code, mas você deve fazer isso.

Agora temos toda vez que a função getUserCart for chamada, uma requisição é disparada para o endpoint /features/ para recuperar o valor das nossas variáveis e nós dizer qual ação tomar a partir do código. Isso é surpreendente, agora podemos ligar ou desligar uma feature sem precisar de nenhum deploy e tudo em "tempo real", seja em aplicativos moveis que por vezes precisam de longos tempo de espera de aprovação por parte das lojas ou em Websites e Backends que tem deploys mais rápidos, mas por vezes um rollback ou mesmo hotfix pode ser custoso.

Caso implemente um endpoint para buscar essas features, você precisa pensar em estratégias de cache.

Com a nossa implementação a nível técnico completa, nos resta agora partir para nossa estratégia de entrega para validarmos nossa nova implementação/feature do carrinho.
Agora temos total liberdade de dizer quando ou não usar, então podemos pensar de uma forma simples que para obter nossas métricas, podemos criar um calendário de quando ligar e desligar o novo provider e coletar insumos durante os dois períodos, coisas como feedback do usuário, tempo de resposta das APIs, tempo da jornada de sucesso, taxas de conversão e tudo mais.

Frontend

No frontend, seja mobile ou web, essa abordagem fica incrível também, temos o poder de exibir ou ocultar componentes, mudar comportamento, cores, textos e muito mais.

Feature flag para exibir ou não a nova feature de "agendar corrida".

Soluções prontas

Claro que existem muito soluções prontinhas para você só plugar no seu código e sair por aí ligando e desligando coisas.
O mais famoso talvez seja o Firebase Remote Config, a solução do google para configurações remotas funciona não só para valores booleanos (como os que testamos até aqui), mas também aceita string, números e até JSON.
Particularmente não acho que seja a melhor opção, o Remote config costuma "engasgar" com uma quantidade muito alta de buscas simultâneas e retornar alguns falsos positivos, isso tem diminuído bastante com o passar do tempo. Outro ponto é o cache configurado do lado do cliente que tem alguns comportamentos inesperados e a interface que não é das melhores.
Uma segunda opção que eu conheci recentemente foi o ConfigCat (obrigado Andre), que tem um painel de configuração muito intuitivo além de ter muita personalização para as features, suporte a ambientes, ouvir modificações em tempo real e muito mais.

Conclusão

Essa abordagem é fantastica e pode ajudar muito o ciclo de desenvolvimento do seu projeto, só não se esqueça de usar com bastante cuidado e sempre validando se essa flag vai ficar para sempre no seu código ou chegará um momento em que ela irá sumir. É interessante sempre documentar os fluxos que utilizam as flags também, pois elas podem dificultar em alguns momentos de debugging.

Querem que eu traga mais conteúdo sobre o Firebase Remote Config (ou outros produtos do Firebase) ou do ConfigCat ?
Comenta ai!