Por que um app Expo precisa de uma camada de analytics antes de adicionar Amplitude
18 min de leituraEscrito por: Jonathan Reis em
Adicionar Amplitude a um app Expo não deveria espalhar um segundo SDK pelo código de produto. Uma camada de analytics orientada ao runtime preserva Firebase e GA4 e abre espaço para rollout independente, falha isolada e análise de produto sem herdar as fronteiras de cada fornecedor.

Um app Expo compila para Android, iOS e navegador, mas os SDKs de analytics não viram multiplataforma automaticamente. Em um app de produção, o Firebase Analytics já cuidava dos eventos nativos. Ao abrir o mesmo app no navegador, era necessário outro transporte: o Google Analytics 4 por meio do gtag.js. Depois surgiu a necessidade de adicionar a Amplitude sem substituir nenhum dos dois destinos.
De início, isso parece uma integração de provider. Na prática, é uma decisão de arquitetura sobre onde fatos de produto terminam e fornecedores de analytics começam. Sem essa fronteira, toda pergunta nova sobre retenção, um dashboard novo, uma revisão de privacidade ou uma indisponibilidade de provider vaza para telas, lógica de jogo e navegação. O atalho de chamar cada SDK diretamente só é barato até o segundo provider chegar.
A intenção não era substituir o Firebase. Era manter um contrato de eventos de produto e enviá-lo a todos os providers habilitados em cada runtime. Android e iOS continuam no Firebase, com Amplitude disponível como segundo destino nativo. O Expo Web usa um stream Web do GA4 dentro da mesma propriedade do app, com Amplitude disponível ao lado. Isso mantém a análise de gameplay consolidada e ainda deixa clara a plataforma de origem.
O atalho errado é reutilizar o Measurement ID da landing page. Funciona tecnicamente, mas mistura eventos de gameplay com aquisição, campanhas, cliques de CTA e scroll da página. Os dados chegam, só que os relatórios deixam de responder perguntas simples sobre o produto.
A vantagem real é poder mudar analytics sem mudar o produto
Um evento como game_started é um fato de produto. Ele deveria significar a mesma coisa quando segue para Firebase, GA4, Amplitude, um warehouse ou nenhum provider. A tela que inicia a partida não deveria saber qual fornecedor precisa do evento, qual SDK funciona no runtime atual ou o que fazer se a requisição falhar. Ela só sabe que uma partida começou.
Essa distinção fica concreta depois do lançamento. Um time pode querer paths e coortes de retenção em uma segunda ferramenta, um transporte de navegador enquanto o nativo mantém Firebase ou uma forma de desligar um destino depois de uma decisão de privacidade ou cota. Um provider também pode ficar lento ou indisponível durante compra, ads ou início de partida. Uma nova superfície pode precisar de eventos sem aprender três APIs de SDK.
Com chamadas de fornecedores dentro do código de produto, cada pedido vira uma reescrita transversal: nomes de eventos, mapeamento de propriedades, ciclo de inicialização, guardas de plataforma, comportamento de erro e doubles de teste se espalham pelo app. Com uma camada de analytics, essas decisões ficam em um lugar. O código de produto muda uma vez, onde o fato é emitido; a entrega muda de forma independente.
Isso não é um argumento para coletar o mesmo dado em todo lugar por padrão. É um argumento para tornar essa escolha reversível. Um provider pode entrar, pausar ou sair pela configuração enquanto o contrato de produto fica estável. A aplicação é dona da semântica dos eventos; integrações continuam sendo infraestrutura substituível.
O que uma camada multi-provider muda na operação
O resultado visível é fan-out. O resultado operacional é um raio de explosão menor. Quando um provider é adapter atrás de um dispatcher, uma requisição com falha pode ser registrada e isolada. Ela não transforma uma ação de produto em ação com falha. Um jogador ainda precisa iniciar uma partida se um endpoint de analytics estiver indisponível. Um visitante ainda precisa chegar à loja se um CTA da landing não conseguir fazer flush de um evento. Analytics é observacional e não pode virar dependência de gameplay, navegação, compra ou aquisição.
Configuração independente também transforma rollout de deploy de código em decisão operacional controlada. Uma build pode conter o adapter, mas mantê-lo desligado. O time pode validar a configuração de produção gerada, habilitar um destino, observar ingestão e falhas e então decidir se permanece. O fallback não é uma reescrita emergencial removendo chamadas do app. É uma flag do provider que preserva o contrato e deixa os outros destinos ativos.
A mesma fronteira melhora os testes. Testes de produto verificam que game_started foi emitido com as propriedades esperadas sem iniciar SDKs de rede. Testes de adapter verificam que payloads Firebase, GA4 ou Amplitude derivam corretamente desse fato. Smokes de integração validam credenciais e ingestão reais. São perguntas diferentes e não deveriam virar um único teste end-to-end frágil.
Ela também melhora a conversa entre engenharia e produto. Engenharia consegue dizer quais fatos existem e quando disparam. Produto decide quais perguntas importam, qual provider serve melhor a uma análise e quais propriedades são permitidas. Nenhum dos dois lados precisa deduzir comportamento a partir de chamadas de SDK espalhadas pelas telas.
Comece com um único contrato de eventos
O app já enviava eventos por um pacote compartilhado de analytics. Telas, início e fim de partida, configurações e algumas user properties não pessoais usavam os mesmos métodos públicos:
trackEvent(event);
logScreenView(screen);
setUserProperty(property);
Esse contrato não deve fazer o código do jogo perguntar Platform.OS antes de cada chamada. Quem chama descreve um fato de produto, como game_started; o pacote de analytics escolhe o transporte.
Antes da mudança, a configuração tinha um único provider:
analytics:
enabled: true
provider: firebase
Esse formato ficou ambíguo quando Web passou a ser um destino real. firebase descrevia corretamente o nativo, mas o navegador poderia usar a tag JavaScript do GA4 ou o Browser SDK da Amplitude. Acrescentar um campo chamado webMeasurementId também criaria uma dívida: uma chave da Amplitude não é um Measurement ID do GA4.
O formato final separa runtime, provider e chave específica:
analytics:
enabled: true
native:
providers:
- provider: firebase
enabled: true
- provider: amplitude
enabled: false
apiKey: AMPLITUDE_PUBLIC_API_KEY
web:
providers:
- provider: ga4
enabled: true
measurementId: G-XXXXXXXXXX
pageViews: false
- provider: amplitude
enabled: false
apiKey: AMPLITUDE_PUBLIC_API_KEY
Cada provider tem sua própria chave e seu próprio campo enabled. Assim é possível testar ou desligar a Amplitude sem remover Firebase ou GA4:
web:
providers:
- provider: amplitude
enabled: true
apiKey: sua-chave-publica
O mesmo formato permite um segundo destino sem prender o contrato compartilhado ao vocabulário do GA4.
Mantenha cada bundle no seu runtime
O React Native resolve arquivos por plataforma antes do arquivo comum. Usamos isso em vez de importar react-native e espalhar condicionais em cada chamada de evento. O dispatcher envia cada chamada a todos os providers habilitados e isola falhas.
O resolvedor Web lê config.web.providers. O equivalente nativo lê config.native.providers.
// providers.native.ts
const providers: Record<string, AnalyticsProvider> = {
none: noopAnalyticsProvider,
mock: noopAnalyticsProvider,
firebase: createFirebaseProvider(),
amplitude: createAmplitudeNativeProvider(),
};
function trackEvent(config: AnalyticsConfig, event: AnalyticsEvent) {
return dispatchAnalytics(config, config.native.providers, providers, (provider) =>
provider.trackEvent(event, config),
);
}
O arquivo nativo importa firebase.native.ts, onde está @react-native-firebase/analytics. O bundle Web não resolve esse arquivo. O resolvedor comum importa os providers Web:
const providers: Record<string, AnalyticsProvider> = {
none: noopAnalyticsProvider,
mock: noopAnalyticsProvider,
ga4: createGa4Provider(),
amplitude: createAmplitudeBrowserProvider(),
};
function trackEvent(config: AnalyticsConfig, event: AnalyticsEvent) {
return dispatchAnalytics(config, config.web.providers, providers, (provider) =>
provider.trackEvent(event, config),
);
}
Isso não é só organização. Importar um SDK nativo em um módulo Web pode quebrar o bundle, adicionar dependências desnecessárias ou deixar um provider que falha em silêncio. Entradas separadas por plataforma deixam a dependência de cada runtime explícita no sistema de arquivos. O dispatcher também impede que uma indisponibilidade da Amplitude pare Firebase ou GA4.
Adicionamos testes para cada resolvedor, além de fan-out, falha de provider, destinos desligados e o gate de ativação tardia. Assim a separação não fica apenas na tipagem.
Escolha providers para trabalhos diferentes, não como verdades concorrentes
Manter Firebase/GA4 e Amplitude juntos não é uma votação para decidir qual dashboard está “certo”. Eles respondem perguntas que se sobrepõem por modelos de dados, relatórios, definições de sessão, atrasos de processamento e ferramentas de exploração diferentes. Tratar um provider como réplica automática do outro cria um critério falso de release: totais iguais que talvez nunca existam mesmo com as duas integrações saudáveis.
Firebase e GA4 mantêm continuidade para relatórios nativos existentes, contexto de aquisição e ferramentas específicas da plataforma. Um stream Web do GA4 permite que Expo Web entre na propriedade de produto sem fingir que eventos de navegador são eventos nativos do Firebase. A Amplitude fornece outra superfície de análise de produto para paths, coortes, retenção e funis. O resultado útil não são três fontes de verdade. É um contrato de eventos com várias lentes.
Essa distinção muda como o time discute divergências. Uma diferença de contagem pode ser bug real de implementação, mas também pode ser comportamento esperado: um provider pode processar lote depois, aplicar outro limite de sessão, omitir um evento porque consentimento ou configuração difere ou mostrar atraso de processamento. Comece pelo contrato e pela evidência realtime de um fluxo conhecido. Só depois compare agregados, com as definições de cada provider visíveis.
A mesma disciplina mantém a analytics da landing útil. Tráfego de aquisição responde de onde o visitante veio e se usou um CTA de loja. Analytics de produto responde o que aconteceu depois que alguém entrou no app. Juntar essas perguntas exige desenho deliberado de atribuição, não a reutilização de um identificador client-side por conveniência.
Crie um stream Web na propriedade do app
O Firebase já conectava o app nativo a uma propriedade GA4 existente. Uma tag de navegador não consegue enviar usando os identificadores Android ou iOS. Ela precisa de um Measurement ID Web no formato G-....
O arranjo correto é criar um stream Web dentro da propriedade existente do app e configurar a Amplitude como destino de produto separado:
Propriedade GA4: seu app
├── Stream Android -> Firebase SDK
├── Stream iOS -> Firebase SDK
└── Stream Web -> gtag.js do Expo Web
Projeto Amplitude -> SDK Browser do Expo Web e SDK nativo
Isso não cria outra propriedade de produto nem exige refazer relatórios. Os streams identificam a origem; a propriedade continua sendo o lugar que reúne o gameplay nativo e Web.
A landing permanece separada. A propriedade dela mede aquisição: page views, origem de tráfego, campanhas e cliques para a loja. O stream do app mede gameplay. Reutilizar o Measurement ID da landing criaria um conjunto de dados tecnicamente válido, mas ruim de analisar.
A URL pedida ao criar o stream Web é uma informação útil no cadastro, não uma lista de domínios permitidos. Se o app for publicado depois em outro domínio, ele ainda pode enviar usando o mesmo Measurement ID. Mantenha o ID na configuração de produção quando seu repositório trata IDs públicos de client-side como configuração versionada.
Inicialize analytics Web sem bloquear o primeiro paint
Cada adapter primeiro confere se o provider está habilitado e tem uma chave válida. Os SDKs Browser não são inicializados pelo primeiro efeito do React. Eventos, screen views manuais, user properties e chamadas de inicialização entram em um gate compartilhado e aguardam a primeira interação de ponteiro/teclado/scroll, o documento ficar oculto ou um prazo de seis segundos. isConfiguredWebProvider também impede que chaves vazias ou placeholders entrem na configuração renderizada do site.
Depois da ativação, o adapter GA4 cria a fila normal do dataLayer e configura o GA4:
globalThis.gtag("js", new Date());
globalThis.gtag("config", measurementId, { send_page_view: false });
send_page_view: false é deliberado. Expo Web é uma SPA e o app já envia screen_view pelo React Navigation. Deixar o GA4 gerar page view automático enquanto o navegador envia screen view manual cria dois sinais de navegação concorrentes.
O script externo é adiado até um destes momentos:
- a primeira interação de ponteiro, teclado ou scroll;
- seis segundos depois do evento
load; - a página ficar oculta.
O gate compartilhado faz o mesmo para a Amplitude: preserva chamadas iniciais sem inicializar um SDK externo pelo primeiro efeito do React. A landing pode chamar flush durante pagehide; o flush ativa o gate, drena as operações pendentes e pede flush aos providers. O CTA não chama preventDefault nem aguarda analytics antes de navegar.
Não mande telemetria local para a propriedade real
Os três YAMLs têm a mesma estrutura, mas não os mesmos valores:
# config.development.yml e config.test.yml
analytics:
enabled: true
native:
providers:
- provider: mock
enabled: true
web:
providers:
- provider: mock
enabled: true
Isso é mais seguro do que depender de um campo ausente. Qualquer ambiente expõe o contrato inteiro para inspeção. Desenvolvimento e testes continuam chamando a mesma API de analytics, mas o provider mock impede sessões locais, testes de navegador e fixtures de chegarem ao GA4.
Produção mantém cada provider explicitamente habilitado ou desligado. Nesta implementação, a Amplitude só foi ligada depois que a chave pública de ingestão, a divulgação de privacidade, o smoke de coleta real e a revisão de Data Safety da loja estavam prontos. O gerador de configuração tipado confere o array de providers, então um campo antigo como analytics.provider falha no typecheck em vez de mudar o comportamento sem aviso.
Valide o artefato de produção, não apenas o código-fonte
Uma armadilha apareceu na validação: exportar EXPO_PUBLIC_APP_ENV=production para gerar a configuração não aplica esse valor automaticamente ao próximo comando.
Isto está errado se expo export vier depois de &&:
EXPO_PUBLIC_APP_ENV=production pnpm --filter <seu-app> config:generate && \
pnpm --filter <seu-app> exec expo export --platform web
O segundo processo pode executar app.config.js no ambiente development e empacotar o provider mock. Aplique o ambiente nos dois comandos:
EXPO_PUBLIC_APP_ENV=production pnpm --filter <seu-app> config:generate && \
EXPO_PUBLIC_APP_ENV=production pnpm --filter <seu-app> exec expo export --platform web
Depois buscamos o ID do stream Web no JavaScript exportado e confirmamos que o bundle não continha o ID da landing page. Esse check encontrou o erro de ambiente antes de qualquer deploy.
A próxima validação é externa: abra o app Web publicado, navegue para uma tela, inicie um jogo e verifique Realtime ou DebugView na propriedade GA4 do app. O bundle prova que a tag e o ID corretos entraram no artefato; não prova que uma sessão de navegador publicada chegou à ingestão do Google.
Para validar o lado nativo da mesma propriedade, veja o guia de Firebase DebugView em um build release React Native. A Amplitude precisa de seu próprio smoke com credencial real: o mesmo evento deve aparecer nos dois destinos antes de habilitar o provider para usuários de produção.
Ligue o segundo destino somente depois de um smoke real
O caminho no código não é o rollout. Antes de habilitar a Amplitude em produção, validamos o mesmo fluxo de produto em Android, iOS, Expo Web e landing pública. Cada superfície enviou uma sequência pequena e observável: bootstrap, navegação de tela, uma ação de produto e, quando aplicável, atribuição. A ideia era encontrar falhas de fronteira que teste unitário não enxerga: chave pública errada, provider fora da configuração gerada, bundle Web resolvendo a implementação errada ou deploy ainda servindo artefato antigo.
O critério de aceitação foi deliberadamente mais estreito que comparar dashboards. Providers de analytics processam eventos em ritmos diferentes, aplicam regras de sessão diferentes e expõem os dados em relatórios diferentes. Totais iguais não são um gate válido de release. Um smoke útil responde quatro perguntas:
- O app permaneceu utilizável enquanto cada provider inicializava ou falhava?
- O evento esperado e suas propriedades não sensíveis saíram do runtime atual?
- Cada provider aceitou o evento na própria superfície realtime ou resposta de ingestão?
- O time consegue identificar o build e o runtime que produziram a evidência?
Nos caminhos Web e landing, respostas bem-sucedidas de rede mostraram que os SDKs Browser aceitaram o lote; as visões realtime confirmaram a chegada. No nativo, uma build próxima da release é mais útil que uma debug porque exercita configuração gerada, integração dos SDKs nativos e o mesmo caminho de startup entregue ao usuário. Uma instalação recebida pela loja ainda merece um smoke curto: ela elimina a última diferença entre um artefato assinado localmente e o pacote distribuído pela loja.
Não transforme uma checagem de ingestão em conclusão de produto. O tráfego inicial depois do release pode provar que a versão foi distribuída e que os campos de atribuição foram preenchidos. Não sustenta uma decisão sobre retenção, aquisição, anúncios ou compra até existir comportamento suficiente para análise.
Separe privacidade, atribuição e analytics de produto
Adicionar outro destino muda o fluxo de dados, então muda o trabalho do release. A política de privacidade aplicável e a declaração da loja precisam descrever os providers de analytics ativos e as categorias de dados que recebem. Essa revisão faz parte do rollout; não é documento para acertar depois.
Isso não significa que analytics deve reaproveitar o prompt de consentimento de anúncios. Nesta arquitetura, o consentimento de ads continua sob responsabilidade da stack de ads. Analytics de produto não mostra um segundo gate de consentimento apenas porque a Amplitude está ativa. A regra correta depende da base legal e das jurisdições do produto, mas a responsabilidade de runtime deve ficar explícita em vez de tratar todo SDK como intercambiável.
O Install Referrer é outra fronteira que vale manter estreita. Um app Android pode registrar um resultado de atribuição sanitizado, como source, medium, campaign e content atribuídos. Não deve encaminhar a string bruta de referrer como telemetria de produto e isso não substitui um MMP. O evento responde uma pergunta modesta: quais valores de campanha entregues pela loja chegaram a esta instalação? Ele não prova que toda reinstalação é uma aquisição nova nem que um clique na landing causou uma instalação posterior.
A arquitetura torna opções futuras baratas, não gratuitas
Uma camada de analytics não remove o trabalho de definir eventos. Ela torna o trabalho visível. Um nome de evento vago continua vago em todo provider. Uma propriedade que não é segura de coletar continua insegura depois de passar por um adapter. Um funil não fica confiável porque aparece em um dashboard bonito. A camada protege a fronteira; não inventa semântica de produto nem governança.
Ela também não significa que todo provider novo merece acesso à produção. Cada destino adiciona peso de SDK, fluxo de dados, obrigações de privacidade, custo operacional e mais um lugar em que o evento pode ser mal interpretado. O default correto é uma lista pequena de providers, contrato de eventos limitado e uma razão explícita para cada integração.
O que a estrutura compra é opcionalidade dentro dessas restrições. Um export futuro para warehouse pode consumir os mesmos eventos de domínio. Uma nova plataforma pode receber adapter próprio em vez de copiar SDK nativo. Uma migração de provider pode rodar em paralelo tempo suficiente para validar a substituição. Uma indisponibilidade temporária pode reduzir observabilidade sem mudar o que o usuário consegue fazer. Isso não é promessa de analytics grátis. É uma forma de evitar que uma decisão de analytics vire reescrita de produto.
O que o rollout provou e o que não provou
A release publicada mostrou que o produto consegue manter Firebase/GA4 e Amplitude em paralelo sem mudar nomes de eventos de domínio, bloquear gameplay ou misturar tráfego da landing com tráfego de produto. A propagação pública na loja e eventos de primeiro uso confirmaram que usuários receberam a nova versão do app. Os sites públicos também entregaram seus eventos GA4 e Amplitude sem erros de console.
Uma limitação continua importante de declarar. O dashboard agregado de um provider não prova que a mesma sessão instalada pela loja chegou a todos os destinos. Essa última correlação exige um aparelho disponível para um smoke curto da instalação da loja. Mantê-la como follow-up rastreado é mais honesto do que inferi-la de relatórios agregados separados.
Checklist
- Mantenha um contrato tipado de eventos de produto.
- Resolva providers com
.native.tse arquivos Web, não com condicionais espalhadas no app. - Configure cada provider de forma independente para adicionar um segundo destino sem substituir o primeiro.
- Crie um stream Web do GA4 dentro da propriedade do app.
- Não reutilize o Measurement ID da landing para eventos de gameplay.
- Mantenha o Browser SDK atrás de um gate compartilhado; user properties do boot não podem inicializá-lo.
- Use
send_page_view: falsese React Navigation é responsável porscreen_viewmanual. - Mantenha development e test em
mock, com chave Web vazia. - Mantenha testes automatizados em
mock; marque telemetria manual intencional de development como tráfego de QA para ela não parecer uso de produção. - Aplique o ambiente de produção a todos os comandos do build.
- Inspecione o bundle final e depois valide uma sessão real no GA4.
- Valide Amplitude e Firebase/GA4 separadamente em realtime; não exija totais iguais entre providers.
- Revise declarações de privacidade e Data Safety da loja antes de habilitar um novo destino em produção.
Firebase, GA4 e Amplitude não são contratos de evento concorrentes. São transportes específicos de runtime para os mesmos eventos de produto. Deixar a lista de providers, o gate de ativação, a resolução de módulos e a evidência de release explícitos permite adicionar destinos sem alterar Android/iOS nem fazer a primeira renderização Web pagar o custo do SDK. Mais importante: mantém analytics como infraestrutura que pode evoluir enquanto o produto preserva a própria linguagem.
Post relacionadoComo validar eventos no Firebase DebugView em um APK release React Native9 min de leituraEscrito por: Jonathan Reis em