Fotografia de capa por Jonathan Borba, tirada por uma Canon, EOS 6D
Você provavelmente já viu essa palestra com o título "Scaling Instagram Infrastructure", a palestra em si fala sobre as mudanças de arquitetura que o Instagram foi passando ao decorrer do tempo por conta das proporções de escala, mas sem dúvida umas das partes mais intrigantes (pelo menos pra mim) foi essa aqui:
Você em seu trabalho que provavelmente usa gitflow e vive perdendo código em merges deve pensar: como raios isso pode ser possível??
E realmente é estranho pensar que todos os devs usem apenas uma branch pra hotfixes e novas funcionalidades, mas na palestra é falado o porquê dessa escolha e os problemas de ter mais de uma branch.
Ter apenas uma branch te incentiva a pensar em produto como pequenas evoluções, pequenos deploys ao invés de um spike ao fim do mês. O que acaba diminuindo as chances de erros em produção e uma percepção de entrega de valor maior ao usuário.
Não é nada fácil conseguir isso, pois o investimento em geração de código e tooling pra dev, além de infra é muito grande, mas podemos tentar diminuir o "gitflow hell" do nosso dia-a-dia de algumas formas, e aí, entra o assunto desse artigo.
Uma das filosofias que temos em produto desde que lançamos a Ext. Contabilidade é ship fast e uma das formas que podemos fazer pra tornar isso verdade é feature flag.
Usamos o Remote Config do Firebase e esboçamos primeiro com tipos o que é e como se comporta, para depois fazermos a implementação nos composables.
entities/RemoteConfig/RemoteConfig.ts
export interface Feature {
enabled: boolean
enabledFor: string[]
}
export interface RemoteConfig {
initialized: boolean
features?: {
[key: string]: Feature
}
}
export interface IsFeatureEnabledOptions {
key: string
email: string
}
export interface IRemoteConfig {
state: RemoteConfig
init: () => Promise<void>
isFeatureEnabled: ({ key, email }: IsFeatureEnabledOptions) => boolean
}
Features ficam em um objeto onde definimos como habilitado e se queremos que fique disponível para determinados usuários, ex:
"panel": {
"enabled": true,
"enabledFor": [
"igor@extcontabilidade.com.br"
]
}
Se quisermos que fique aberto para todos os usuários basta deixar enabledFor
como um array vazio, e se quisermos segmentar (para 20% da base ou para usuários do ceará) usamos o A/B Testing do Firebase para a chave do objeto da feature.
Já para a implementação eu apenas respeito a interface, o que torna essa solução portável para seu projeto React por exemplo, dado que o que é e como se comporta é definido através de uma abstração.
composables/useRemoteConfig/useRemoteConfig.ts
import { fetchAndActivate, getAll } from "firebase/remote-config"
import {
IRemoteConfig,
RemoteConfig,
IsFeatureEnabledOptions
} from '@/entities/RemoteConfig/RemoteConfig'
export function useRemoteConfig(): IRemoteConfig {
const { $remoteConfig } = useNuxtApp()
const state = reactive<RemoteConfig>({
initialized: false,
features: undefined
})
async function init() {
await fetchAndActivate($remoteConfig)
loadAllConfigs()
state.initialized = true
}
function isFeatureEnabled({ key, email }: IsFeatureEnabledOptions) {
if (!state.features) {
return false
}
const feature = state.features[key]
if (!feature?.enabled) {
return false
}
// if has no emails defined, it's enabled for everyone (a/b testing delegation)
if (feature.enabledFor.length === 0) {
return feature.enabled
}
const enabled = feature.enabledFor.includes(email)
return enabled
}
function loadAllConfigs() {
const response = getAll($remoteConfig)
try {
state.features = JSON.parse(response.features.asString()) as RemoteConfig['features']
} catch (e) {
console.log('* remote config error', e)
}
}
return {
state,
init,
isFeatureEnabled
}
}
Agora pra deixar disponivel para todos os componentes usamos uma feature pouco conhecida do Vue: provide/inject
layout/admin.vue
<script setup lang="ts">
// ...
const remoteConfig = useRemoteConfig()
provide('RemoteConfig', readonly(remoteConfig))
onMounted(() => {
remoteConfig.init()
})
</script>
<template>
<VitePwaManifest />
<DefaultAppAdminWithImageLoading
v-if="!remoteConfig.state.initialized" />
<div class="layout-container" v-if="remoteConfig.state.initialized">
<!-- ... -->
</div>
</template>
Isso nos permite testar novas coisas muito rápido, um exemplo é o painel de controle da empresa que estamos testando com alguns usuários e logo deve ser liberado para toda a base.
O panel é visto apenas por usuários que queremos e todo ele tem eventos de analytics para trackear e rodarmos nossos testes antes do rollout final.
Basta usar apenas
do nosso composable:isFeatureEnabled
()
const isEarlyUser = remoteConfig?.isFeatureEnabled({
key: 'panel',
email: currentUser.email
})