screen_view no GA4 com React Native: por que eventos customizados não bastam
O app já mandava game_started e shop_viewed, mas o GA4 não respondia onde o usuário navegava. A correção não foi mais eventos — foi screen_view ligado ao React Navigation.
Abra Páginas e telas no GA4 de um app React Native e encontre zero visualizações, com quase tudo em (not set). Não é bug do Google — na maioria dos casos o app simplesmente não manda screen_view.
Foi o que vimos no Daily Sudoku: Offline Puzzle depois dos testes internos de julho de 2026. O GA4 já recebia uns quatrocentos eventos em poucos dias. game_started, shop_viewed, home_play_pressed apareciam no topo. O relatório de navegação, não.
Eventos customizados contam o que aconteceu. Não dizem em qual tela o jogador estava quando apertou Play, abriu a Loja ou desistiu no meio da partida.
Na Fase 1 daquele mês estendemos @apps/analytics, ligamos screen_view ao React Navigation, sincronizamos user properties, configuramos o GA4 Admin e validamos num APK de produção. O ponto chato foi o device físico: DebugView vazio, logcat salvando o dia.
O que o GA4 já mostrava
O projeto Firebase sunstoneapps-daily-sudoku alimenta a propriedade GA4 sunstoneapps-daily-sudoku-app. A telemetria de gameplay estava viva:
shop_viewed,app_opened,game_started,home_play_pressed,difficulty_selected,game_finished, entre outros.
Isso serve para funis montados a partir de toques explícitos e hooks de ciclo de vida. Não substitui relatório por tela. O relatório Páginas e telas do GA4 espera screen_view (ou o rastreamento automático de telas que o Firebase pode fazer quando configurado). Sem isso, perguntas como “o usuário abre a Loja antes ou depois da primeira partida?” exigem inferência frágil pela ordem dos eventos.
Tínhamos parâmetros nos eventos (difficultyId, result, source) que só viram dimensões fáceis depois de registrados no GA4 Admin. Nomes de parâmetro no stream bruto não são a mesma coisa que dimensões prontas para relatório.
Objetivos da Fase 1
Escopo da Fase 1: visibilidade, não telemetria nova de gameplay:
screen_viewautomático em cada mudança de rota do React Navigation.setUserPropertypara um conjunto pequeno de campos de segmentação sem PII.- Dimensões customizadas no GA4 para parâmetros de evento e user properties que já enviamos.
- Eventos principais marcados para as métricas que importam ao negócio.
- Validação num APK parecido com produção, não só em providers mock.
Funis de ads, abandono e hábito Daily ficam para depois. A Fase 1 só precisava tornar legível no GA4 o contrato de eventos que o app já mandava. (Configurar mensagens de privacidade do AdMob UMP foi uma preocupação separada que veio depois.)
Estendendo o @apps/analytics
O monorepo já roteava eventos de produto pelo @apps/analytics, com provider Firebase no nativo e no-op na web. Adicionamos dois métodos na superfície pública:
logScreenView({ screenName, screenClass? })setUserProperty({ name, value })
Ambos respeitam o mesmo gate analytics.enabled do trackEvent. No nativo chamam @react-native-firebase/analytics. Na web fazem noop para SSR e builds Expo web não quebrarem.
O runtime Firebase fica em firebaseRuntime.ts para Jest e stubs web continuarem finos. Testes unitários mockam o provider e verificam o dispatch com analytics habilitado.
Regra que mantivemos: não logar PII. User properties são estado de produto (theme_mode, locale, max_mistakes_setting), não e-mail nem identificadores de conta.
Ligando o React Navigation
O Sudoku usa um NavigationContainer em AppNavigator.tsx. O padrão:
- Manter ref do container de navegação.
- Em
onReadyeonStateChange, ler o nome da rota ativa. - Se o nome mudou desde a última emissão, chamar
logScreenView.
O helper getActiveRouteName percorre navegadores aninhados para a rota folha (Game, Shop, WinFlow, etc.). O hook useAnalyticsNavigationTracking deduplica a rota anterior para não spammar analytics em eventos de estado repetidos.
Telas mapeadas no MVP: Home, Profile, Favorites, Shop, Settings, PreGameAd, Game, WinFlow, WinStats. screen_class espelha o nome da rota, alinhado ao builtin firebase_screen_class quando os relatórios hidratarem.
SyncAnalyticsUserProperties é um componente pequeno perto da raiz do app. Observa versão, locale, tema, limite de erros, tutorial concluído e entitlement sem anúncios, chamando setUserProperty quando os valores mudam. Tutorial concluído atualiza ao fim do welcome, não só no cold start.
O que entrou no GA4 Admin
Com o código pronto, configuramos a propriedade (passos manuais no Admin; parte da UI usa overlays e comboboxes difíceis de automatizar):
Dimensões de evento (8): difficultyId, result, source (rótulo “Game start source”), productId, step, stepId, scoreBucket, hasActiveGame.
Dimensões de usuário (6): app_version, locale, theme_mode, max_mistakes_setting, tutorial_completed, has_noads_entitlement.
Eventos principais: game_finished, daily_complete, shop_purchase_completed (além de first_open padrão do Firebase).
Resumo dos relatórios: hub com usuários ativos, contagem de eventos, retenção e breakdown. Páginas e telas só popula depois que devices rodam build com screen_view — espere 24–48 h de atraso após o release.
Pulamos evento derivado game_won no Admin. Filtrar game_finished com result = win basta; a UI “sem código” do GA4 assume acionador por nome de tela e é fácil errar.
Follow-up manual opcional: salvar exploração de funil app_opened → home_play_pressed → game_started → game_finished pelo modelo Exploração de funil na Biblioteca.
Validando num APK de produção
Build debug é péssimo substituto do comportamento de loja. Usamos:
pnpm sudoku:apk:prd
adb shell setprop debug.firebase.analytics.app com.sunstoneapps.dailysudoku
Instalar o APK release, cold start, navegar Home → Loja → Jogo.
Quando o DebugView some — e o logcat salva
Num build Play anterior já tínhamos batido de frente com isso: em Xiaomi/HyperOS o seletor do DebugView fica vazio mesmo com debug.firebase.analytics.app setado, logcat mostrando Faster debug mode event logging enabled e upload 204 para GOOGLE_ANALYTICS. O GA Realtime até mostrava usuários ativos; só a UI de debug que não listava o aparelho.
Na validação da Fase 1, repetimos o ritual no Redmi Note 7 (94fed41):
adb logcat -s FA-SVC | rg "screen_view|Setting user property|app_opened"
No cold start vimos:
Logging event: name=screen_view(_vs)comga_screen(_sn)=Home,ga_screen_class(_sc)=Home,manual_tracking(_mst)=1Setting user property: theme_mode, light(e as outras cinco properties)app_openedcomenvironment=production
Isso basta para shipar o código da Fase 1. A UI do GA4 alcança depois que o volume acumula.
Checklist operacional no repositório
O checklist vivo está em docs/app-sudoku/SUDOKU_ANALYTICS_OBSERVABILIDADE_CHECKLIST.md. Fases:
- Fase 1: visibilidade de jornada (
screen_view, dimensões, eventos principais) — em grande parte concluída. - Fase 2: instrumentação de ads e abandono em
@apps/adse bridges de gameplay. - Fase 3: hábito Daily e eventos de settings.
- Fase 4: export BigQuery quando volume real justificar SQL.
O contrato tipado de eventos fica em apps/sudoku/src/app/analytics/events.ts e na seção Telemetria do SUDOKU_PLANO.md. Ao adicionar eventos, atualize tipos, docs e dimensões GA4 na mesma mudança.
Três erros que quase nos custaram tempo
Registrar dimensão no Admin só quando o relatório já está vazio atrasa uma semana. Parâmetro no stream não vira filtro sozinho — agora criamos dimensão de evento e de usuário no mesmo PR do código.
DebugView não é prova única. No Redmi, adb logcat -s FA-SVC com _dbg=1 e upload 204 provou transporte enquanto o picker do console continuava vazio.
screen_view vale mais que o décimo evento customizado. O GA4 ficou legível antes de qualquer telemetria nova de gameplay que ainda não segmentávamos.
Analytics continua fora do devModeEnabled e fora do caminho crítico do jogo — fire-and-forget, warning em falha, zero modal para o jogador.
Depois disso
Fase 2 cobre ciclo de vida de ads (ad_request, ad_loaded, ad_failed, …) e game_abandoned para quem sai sem game_finished. Monetização e desistência só ficam mensuráveis quando isso existir.
Se Páginas e telas está vazio e você já tem Firebase no React Native, comece por screen_view e umas poucas user properties. O resto espera.