Sunstone Apps
← Voltar para o blog

Debug de release React Native

Quando o versionCode muda, mas o JavaScript não: uma armadilha em builds React Native

Nem todo bug de release crasha o app. Às vezes a Google Play instala a versão nova enquanto o React Native continua rodando metadados JavaScript antigos.

Publicado: 6 min de leitura
Imagem de prévia OpenGraph deste artigo. Quando o versionCode muda, mas o JavaScript não: uma armadilha em builds React Native

O bug mais perigoso de release não é necessariamente um crash. É o bug que parece corrigido no Play Console, mas continua errado dentro do app instalado.

Foi isso que aconteceu em um envio interno do Daily Sudoku: Offline Puzzle.

O Play Console aceitava o AAB. O Android instalava o app com versionCode=3. O erro anterior do RevenueCat não aparecia mais. Parecia concluído.

Mas o log de inicialização do Sentry dizia outra coisa:

release=com.sunstoneapps.dailysudoku@0.1.0+2
dist=2

O app instalado era versionCode=3, mas o bundle JavaScript ainda anunciava +2.

O sintoma

Depois do update pela Play Store, o adb confirmava:

versionCode=3
versionName=0.1.0
installerPackageName=com.android.vending

Até aí tudo parecia certo. O app vinha da Play, não de sideload.

Mas o runtime mostrava:

[crash-reporting] initialize {
  "release": "com.sunstoneapps.dailysudoku@0.1.0+2",
  "dist": "2"
}

Esse tipo de divergência é fácil de ignorar porque o usuário não vê. Mas para operação de release, ele é grave: qualquer erro capturado no Sentry seria associado à build errada.

Por que isso importa

No React Native, o app Android não é só manifest, recursos e código nativo. Existe também o bundle JavaScript.

Se o manifest muda, mas o bundle JS não é refeito, você pode acabar com uma combinação inválida:

Android package: versionCode=3
JavaScript runtime: dist=2
Sentry source maps esperados: dist=3

Nesse cenário, os relatórios de erro ficam ambíguos. Você não sabe se está olhando para a build nova, para a anterior ou para um híbrido acidental.

A hipótese errada

A primeira hipótese natural era que o Play Store ainda não tinha propagado a versão correta. Isso acontece bastante em faixa interna: o Console mostra a release publicada, mas o device ainda recebe a anterior por alguns minutos.

Só que o adb eliminou essa hipótese:

versionCode=3
installerPackageName=com.android.vending

O pacote novo estava instalado.

A hipótese certa

O problema estava no artefato de build. O AAB tinha manifest novo, mas o bundle JS usado pelo Gradle ainda carregava metadados gerados anteriormente.

O código fonte estava correto:

android.versionCode = 3

O Sentry source map também tinha sido enviado para +3.

Mas o runtime não concordava. Isso apontava para cache no caminho de build.

A correção

A solução foi gerar um novo AAB com versionCode=4, forçando as tasks do Gradle:

pnpm sudoku:aab:clean

O script passou a rodar:

./gradlew bundleRelease --rerun-tasks

E o build confirmou a correção ainda antes do upload:

createBundleReleaseJsAndAssets_SentryUpload_com.sunstoneapps.dailysudoku@0.1.0+4_4
Uploading sourcemaps for release com.sunstoneapps.dailysudoku@0.1.0+4 distribution 4

Depois de publicar e instalar pela Play Store, o runtime finalmente bateu:

versionCode=4
installerPackageName=com.android.vending
release=com.sunstoneapps.dailysudoku@0.1.0+4
dist=4

O que mudou no processo

A correção técnica foi pequena. A mudança importante foi no processo.

Antes:

pnpm sudoku:aab

Depois, para AAB que será enviado à Play Store após troca de versionCode:

pnpm sudoku:aab:clean

O comando incremental ainda serve para iteração local. O comando limpo virou o caminho de release.

Como detectar esse problema cedo

Não confie apenas no Play Console e no versionCode.

Valide três camadas:

  1. Android package:
adb shell dumpsys package com.sunstoneapps.dailysudoku | rg 'versionCode|versionName|installerPackageName'
  1. Sentry runtime:
[crash-reporting] initialize
  1. Source maps enviados:
release=com.sunstoneapps.dailysudoku@0.1.0+4
dist=4

Se as três camadas não batem, o release ainda não está confiável.

O aprendizado

versionCode prova que o Android recebeu um pacote novo. Não prova que o bundle JavaScript foi regenerado do jeito que você esperava.

Em apps React Native, release confiável precisa validar manifest, JS runtime e observabilidade juntos.

Esse é o tipo de bug que não aparece em screenshot, não quebra o fluxo principal e não necessariamente gera crash. Mesmo assim, ele compromete a capacidade de entender qualquer problema que venha depois.

Torne o build JavaScript observável

Um bundle JavaScript cacheado é perigoso porque a casca nativa pode parecer correta. O version code muda, o APK instala, o ícone abre e, ainda assim, o runtime se comporta como a build de ontem. Isso cria falsa confiança de release.

A correção não é apenas limpar cache. Limpar cache é ação de recuperação. Prevenção é tornar o build JavaScript observável dentro do app. Uma tela de release, log de startup ou payload diagnóstico deve expor versão do app, build nativo, marcador do bundle JavaScript, ambiente e origem da configuração.

Quando esses valores ficam visíveis, a pergunta muda de “o Gradle rodou?” para “qual runtime este device está executando de verdade?” Essa é a pergunta que importa na validação de release.

Checks de release que pegam isso cedo

Um check prático deveria incluir:

  • desinstalar o pacote anterior quando o teste exigir estado limpo;
  • instalar o mesmo artefato que será enviado ou distribuído;
  • abrir uma tela que mostre build e ambiente;
  • verificar um comportamento que só existe no novo bundle JavaScript;
  • confirmar release/dist do crash reporting quando Sentry ou outro reporter estiver ativo.

Isso importa mais em apps Expo e React Native porque build nativa e bundle JavaScript andam juntos, mas não são a mesma coisa. Versionamento nativo sozinho não prova que a lógica em runtime mudou.

Trate bundle velho como bug de processo

Se o JavaScript errado foi enviado uma vez, assuma que o processo permitiu. Adicione um marcador visível, um passo de validação de release e comandos de build determinísticos no lugar de sequências manuais. O objetivo não é decorar a invocação certa; é fazer o artefato errado ficar óbvio antes de testers ou usuários encontrarem.

Essa disciplina pequena ajuda muito em correções urgentes. Quando uma build de hotfix é instalada em um aparelho, o time deveria saber em segundos se ela está rodando o código novo.

Separe identidade de build da identidade do app

Um hábito útil é manter uma identidade de build que não seja apenas a versão pública do app. A versão pública é para usuários e lojas. A identidade de build é para o time. Ela pode incluir timestamp, hash de commit, ambiente de config ou marcador do bundle gerado.

Essa informação não precisa ficar exposta para usuários para sempre, mas deve ser acessível durante validação. Um toque escondido de diagnóstico, uma linha interna nas configurações ou um log de startup pode bastar. O importante é que um tester consiga provar qual código JavaScript está rodando sem deduzir pelo comportamento.

Se o app tem crash reporting, envie a mesma identidade para lá. Um crash vindo de bundle velho deve ficar claramente ligado aos metadados do release velho.

Bugs de cache são mais fáceis de prevenir do que explicar

Quando testers perdem confiança em uma build, todo relato posterior fica mais difícil de interpretar. O bug ainda existe ou o aparelho está rodando JavaScript velho? A correção quebrou ou o artefato errado foi instalado? Essas perguntas atrasam mais que o problema original de cache.

Por isso identidade de build deve fazer parte da UX de release para o time. Ela pode ficar escondida de usuários comuns, mas precisa ser rápida para um tester encontrar. Uma linha simples de diagnóstico pode economizar horas durante um release candidate.

A regra é chata: toda instalação usada para validar release deve provar a identidade nativa e a identidade JavaScript. Se não consegue, o processo está pedindo que as pessoas confiem em uma caixa-preta.

Conclusão

O melhor momento para descobrir que seu bundle JS está cacheado é antes de ampliar a faixa de teste.

O segundo melhor momento é quando você ainda consegue gerar um novo versionCode, publicar na faixa interna e validar de novo com calma.

O pior momento é depois de usuários reais começarem a reportar erros que você não consegue mapear com segurança.