Spike releases frequentes em sua companhia, grita para você sobre problemas de má organização e falta de processos verdadeiramente ágeis.

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 isFeatureEnabled() do nosso composable:

const isEarlyUser = remoteConfig?.isFeatureEnabled({
  key: 'panel',
  email: currentUser.email
})

Read next...