Ferramentas React Native
Por que __DEV__ é false em build de release, e como passamos a gatear as dev tools por config
Tínhamos atalhos de desenvolvimento dentro do jogo escondidos atrás de __DEV__. Aí precisamos de um deles num build de release para validar uma tela, e ele não estava lá. __DEV__ era false.
O gate que protege um recurso durante o desenvolvimento pode ser o mesmo gate que esconde esse recurso quando você finalmente precisa dele.
Foi o que aconteceu no Daily Sudoku: Offline Puzzle. O jogo tem um conjunto pequeno de atalhos de desenvolvimento: resolver o tabuleiro, avançar o cronômetro até a meta de tempo da dificuldade, adicionar pontos, adicionar um crédito. Eles existem para chegar em estados que são lentos ou chatos de alcançar na mão, principalmente a tela de vitória, que normalmente exige terminar um puzzle de verdade.
Esses atalhos estavam protegidos por __DEV__. Todo handler começava com if (!__DEV__) return;, e o botão que os abre estava embrulhado em {__DEV__ ? ... : null}. Parecia certo. Ferramentas só de desenvolvimento não deveriam ir para produção. Mas era o gate errado, e o descompasso só apareceu quando tentamos validar a tela de vitória num build que se comporta como o app real.
O momento em que isso doeu
Tínhamos acabado de finalizar um redesign visual e queríamos confirmar que a tela de vitória estava correta no aparelho, não só na revisão de código. O plano era banal: gerar um APK no formato de release, instalar num Redmi Note 7 e num emulador, jogar até ganhar e conferir a tela de resultado.
O problema é que ganhar um puzzle de verdade só para inspecionar uma tela é lento, e tínhamos um atalho exatamente para isso: “resolver tabuleiro”. Só que o atalho não estava ali. O APK de release tinha __DEV__ como false, então o botão de desenvolvimento nunca renderizava. A ferramenta que construímos para chegar na tela de vitória ficou invisível no único build em que queríamos olhar para ela.
O reflexo é apelar para um build de debug, onde __DEV__ é true. Não fizemos isso de propósito. Nossa própria regra de repositório diz que validação parecida com release não deve usar APK de debug, porque uma instalação de debug pode carregar assinatura e runtime diferentes e invalidar em silêncio aquilo que você acha que está testando. Ainda por cima, nosso servidor de debug tinha um problema de bundling não relacionado com uma dependência do Skia, então o caminho de debug estava errado no princípio e quebrado na prática naquele dia.
Ou seja, a pergunta honesta não era “como faço um build de debug rodar”. Era “por que uma ferramenta de teste está amarrada a uma flag que builds de teste não têm”.
O que __DEV__ realmente é
__DEV__ é uma global que os bundlers do React Native e do Expo substituem em tempo de build. Num bundle de desenvolvimento servido pelo Metro, ela é true. Num bundle compilado para release, o minificador troca __DEV__ por false e depois remove os ramos mortos que ficaram atrás dela.
Isso é ótimo para a função de verdade dela: tirar do código enviado o custo que só serve para desenvolvimento. Warnings, invariantes e checagens caras somem do bundle de produção porque o compilador consegue provar que o ramo está morto.
Mas isso significa que __DEV__ responde uma pergunta específica: “esse JavaScript foi empacotado em modo de desenvolvimento?”. Ela não responde a pergunta que a gente de fato queria: “esse build deveria expor ferramentas de desenvolvimento?”. As duas perguntas coincidem enquanto você trabalha no notebook com o Metro rodando. Elas param de coincidir no instante em que você compila um bundle de release para o QA, que é justamente o objetivo de um build de teste no formato de release.
Um build assinado como release feito para teste continua sendo um bundle de release. __DEV__ é false nele. O ambiente pode ser test ou local, o app pode estar cheio de recursos de teste, e __DEV__ ainda vai dizer “não”.
Movendo o gate para a config de runtime
A correção foi parar de perguntar ao bundler e começar a perguntar à própria configuração do app.
O projeto já gera uma config tipada por ambiente a partir de arquivos YAML: config.production.yml, config.test.yml, config.local.yml. Cada arquivo é autossuficiente e carrega uma flag devModeEnabled. Produção deixa em false. Test e local deixam em true. Um passo de build transforma o YAML selecionado num módulo tipado que o app importa, então a flag é um valor normal e estaticamente tipado em runtime, não uma global mágica.
A tela de configurações já gateava a própria entrada de desenvolvimento em config.devModeEnabled. Os atalhos dentro do jogo eram o ponto fora da curva, ainda usando __DEV__. Então a mudança foi pequena e mecânica: todo if (!__DEV__) return; virou if (!config.devModeEnabled) return;, e o render do botão saiu de {__DEV__ ? ... } para {config.devModeEnabled ? ... }.
// Antes: amarrado a como o bundle foi construído
if (!__DEV__) return;
// Depois: amarrado ao que este ambiente declara
if (!config.devModeEnabled) return;
Nada dos atalhos em si mudou. O que mudou foi o significado do gate. Agora a ferramenta aparece quando o ambiente diz que o modo de desenvolvimento está ligado, sem depender de o JavaScript ter sido empacotado para desenvolvimento ou release.
O que isso de fato entrega
O ganho é que um build assinado como release, compilado com o ambiente test, agora expõe as ferramentas de desenvolvimento num runtime de release real. Produção, construído com devModeEnabled: false, nunca as mostra. Mesmo código, dois resultados honestos guiados por config em vez de por um acidente de como o bundle foi compilado.
Isso destravou direto a tarefa original. Geramos um APK de release com o ambiente de teste, instalamos no emulador, abrimos o jogo, e o botão de desenvolvimento finalmente estava lá. Um toque em “resolver tabuleiro”, o puzzle se preencheu, e a tela de vitória apareceu exatamente como projetada. Pudemos inspecionar a tela real no runtime real, e depois reconstruir o APK de produção para deixar os dois aparelhos no estado de envio.
A conversa de depuração mudou de “a ferramenta some neste build” para “este build declara modo de desenvolvimento, então a ferramenta está presente”. Isso é uma propriedade sobre a qual você consegue raciocinar, não um efeito colateral que você precisa lembrar.
O guardrail que mantém isso seguro
Mover um gate de uma flag de compilação para uma flag de config é poderoso, e poder aqui também é risco. Uma flag de config pode ser ligada em mais lugares do que __DEV__ jamais poderia, então a fronteira precisa ser explícita.
Nossa regra é estreita de propósito. devModeEnabled só pode gatear atalhos de gameplay e recursos de teste que existem apenas para desenvolvimento: avançar tempo, conceder pontos ou moedas, forçar vitória ou derrota, abrir um painel de debug. Ela nunca pode gatear nada comercial ou crítico em runtime. Anúncios, analytics, consentimento, crash reporting, fluxos de compra, configuração de produção e comportamento voltado à loja estão fora. Esses sistemas decidem o próprio comportamento a partir das próprias entradas, nunca de uma chave de desenvolvedor.
O motivo é simples. Se devModeEnabled um dia influenciar monetização ou consentimento, então um build de teste deixa de testar o produto que os usuários recebem. A flag passaria a esconder exatamente o comportamento que ela deveria ajudar a verificar. Mantê-la restrita a atalhos descartáveis é o que torna seguro deixar esses atalhos rodarem num runtime de release.
O único lugar onde __DEV__ precisa ficar
Não apagamos __DEV__ da base de código, e isso foi deliberado.
O resolvedor de ambiente ainda usa a flag. Quando o app sobe e nenhum ambiente explícito está definido, ele cai para local quando __DEV__ é true e production caso contrário. Esse gate não pode migrar para config, porque é o código que decide qual config carregar. Perguntar config.devModeEnabled ali seria circular: a config ainda não existe no momento em que essa função roda. Este é o lar correto do __DEV__, porque a pergunta de verdade é “isso é um bundle de desenvolvimento”, que é exatamente o que a flag sabe.
O setup de teste também mantém __DEV__, num caso em que um teste stuba a global para exercitar essa lógica do resolvedor. Isso não é comportamento de produto; é um teste unitário afirmando como o bootstrap escolhe um ambiente.
A distinção é a lição inteira. Use __DEV__ para perguntas sobre o bundle. Use config para perguntas sobre o produto. Escolher o ambiente no bootstrap é uma pergunta de bundle. Mostrar ou não ferramentas de desenvolvimento é uma pergunta de produto.
Como aplicar isso no seu app
Se você tem ferramentas escondidas atrás de __DEV__ que o QA, ou você, algum dia vai precisar num build compilado, a migração é curta:
- Adicione um booleano à sua config por ambiente, desligado em produção e ligado nos builds de test e local.
- Troque os gates de
__DEV__em recursos de desenvolvimento ou teste por essa flag de config. - Mantenha
__DEV__para o bootstrap de ambiente e para remover custo que é genuinamente só de desenvolvimento. - Escreva, em uma frase, o que a flag tem permissão de gatear, e trate monetização, consentimento e analytics como permanentemente fora dessa lista.
- Prove construindo um build de teste assinado como release e confirmando que a ferramenta aparece ali, enquanto um build de produção a esconde.
Nada disso é exótico. É mais perceber que “empacotado em modo de desenvolvimento” e “com permissão para mostrar ferramentas de desenvolvimento” são perguntas diferentes que por acaso concordam no seu notebook e discordam em todo lugar.
O que vigiaríamos a seguir
O risco que sobra é disciplina, não código. Uma flag de config é fácil de alcançar, então a próxima pessoa pode gatear algo que não deveria. A mitigação é a regra escrita e a revisão de código, não um mecanismo mais esperto.
Também ficaríamos de olho na higiene de build. Como a ferramenta agora depende de qual ambiente compilou o bundle, importa que os scripts de release forcem production para artefatos de envio e só usem test ou local para validação interna. Um APK de ambiente de teste é ótimo para segurar num aparelho de QA por uma tarde; não é o que você sobe para a loja. Nomear e roteirizar essa diferença com clareza é o que impede um recurso de teste útil de virar um erro de envio.
O gate agora é honesto sobre o que protege. Essa é a parte que vale a pena guardar.