Tagueamento de Apps React Native e Flutter: Desafios e Boas Práticas
SDK nativo não vê tela híbrida. Como ligar React Navigation e Navigator, Consent Mode no app, Privacy Manifests e um Analytics Service sem acoplar o widget ao vendor.
Contexto
Firebase “está no app”. O GA4 mostra uma MainActivity ou um FlutterView. O funil in-app está vazio. O MMP conta install. Produto não vê onboarding.
Não é SDK quebrado. É ponte JS/Dart ≠ view nativa. O auto screen reporting do Firebase lê o container nativo e ignora a rota que o usuário realmente viu. Até você desligar o automático e ligar o router, o relatório mente.
Este guia é o contrato de integração em código compartilhado: tela, consentimento, manifesto e um serviço que não deixa cada widget falar com o vendor. O job do Firebase (produto) versus MMP (UA) está em Firebase vs AppsFlyer — aqui o foco é como o evento nasce no React Native e no Flutter.
Por que a arquitetura muda o tagueamento
React Native (Nova Arquitetura, ~0.76+). A Bridge antiga serializava JSON de forma assíncrona. Telemetria de alta frequência (gesto, scroll, vários destinos ao mesmo tempo) congestionava a ponte e drobrava frame. JSI + TurboModules passam a chamar nativo com referência em memória: menos latência e lazy load — o SDK de analytics não precisa subir no cold start. O Fabric ainda mapeia para UIView / android.view.View. Session replay “clássico” continua enxergando a árvore.
Flutter (Impeller). Dart AOT desenha no próprio Canvas. Para o inspetor nativo o app é um quadro opaco. Screen view só existe se o Dart avisar (NavigatorObserver). Replay precisa fotografar buffer (CPU, bateria, máscara manual) ou interceptar draw instructions (mais leve, frágil quando o engine muda).
Não escolha framework neste post. Escolha como o SDK entra: no router, atrás de um serviço, depois do consentimento.
Telas: o contrato que falta
Desligue o automático. Instrumente a navegação.
React Native. No firebase.json:
google_analytics_automatic_screen_reporting_enabled: false
No NavigationContainer, compare rota anterior e atual em onReady / onStateChange e chame logScreenView. Com navigators aninhados (tab dentro de stack), desça o estado até a rota folha — senão você loga o pai e o GA4 nunca vê Checkout. Se o evento só faz sentido com a tela visível, use navigation.addListener('focus', ...).
import { useRef } from 'react';
import analytics from '@react-native-firebase/analytics';
import { NavigationContainer } from '@react-navigation/native';
// navigationRef: createNavigationContainerRef() no mesmo módulo
function getActiveRouteName(state) {
const route = state.routes[state.index];
if (route.state) {
return getActiveRouteName(route.state);
}
return route.name;
}
export function AppNav() {
const routeNameRef = useRef();
return (
<NavigationContainer
onReady={() => {
routeNameRef.current = getActiveRouteName(navigationRef.getRootState());
}}
onStateChange={async () => {
const previous = routeNameRef.current;
const current = getActiveRouteName(navigationRef.getRootState());
if (previous !== current) {
await analytics().logScreenView({
screen_name: current,
screen_class: current,
});
}
routeNameRef.current = current;
}}
/>
);
}
Flutter. FirebaseAnalyticsObserver na lista navigatorObservers do MaterialApp (ou no GoRouter). O nome vem de RouteSettings. ID de pedido na path vira centenas de telas iguais no relatório — use um nome estável (OrderDetail) e o ID como parâmetro, não como name.
final observer = FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance);
MaterialApp(
navigatorObservers: [observer],
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: RouteSettings(name: settings.name?.split('/').first),
builder: (_) => const SizedBox(),
);
},
);
Com go_router, registre o mesmo observer nas configs do roteador. Sem nome na rota, o observer não tem o que mandar.
Consent e Privacy Manifests
Consent Mode v2 no app não é o mesmo fluxo do GTM web (Consent Mode v2 cobre o container). Os quatro sinais (analytics_storage, ad_storage, ad_user_data, ad_personalization) começam denied no nativo antes de qualquer SDK: meta-data no AndroidManifest.xml e equivalentes no Info.plist. Depois do CMP, atualize em runtime: analytics().setConsent(...) no React Native Firebase; FirebaseAnalytics.instance.setConsent(...) no Flutter.
Híbrido não isenta iOS. Cada SDK (Firebase, MMP, replay) precisa de PrivacyInfo.xcprivacy no XCFramework. Manifesto faltando ou Required Reason API sem código de razão → rejeição no Connect. ATT, conversion value e S2S: tracking iOS pós-ATT. Gate de telemetria no cold start (também em JS/Dart, não só Application.onCreate()): LGPD no app.
Session replay: critério, não ranking
Três regras, sem “melhor ferramenta 2026”:
- RN + Fabric: replay que lê a árvore nativa costuma aplicar máscara de PII no componente (campo de senha, documento). Confira se o SDK declara suporte à Nova Arquitetura.
- Flutter: o nativo não vê widget. Frame-buffer é fiel e caro; draw instructions é mais leve e quebra com update do Impeller. Teste CPU e bateria no device, não no simulador.
- Consentimento: não inicialize replay (nem ads ID) no mesmo init que crash log. Replay é telemetria comportamental — opt-in quando houver PII na tela.
UXCam, PostHog, FullStory e similares diferem no mecanismo (view nativa vs canvas). Escolha pelo mecanismo e pelo custo de runtime, não pelo slide de feature.
Analytics Service Pattern
Widget, hook e BLoC não importam @react-native-firebase/analytics nem o SDK do AppsFlyer. Um serviço interno tipado (TypeScript ou Dart) expõe screen, track(event, params) e setUser. Por baixo: multiplex para Firebase, MMP, produto; enriquece OS, versão do app, ambiente; recusa nome de evento que não está no enum.
Batching e fila offline: deixe o SDK fazer o default. Persistência em secure store só se você for além do vendor e tiver requisito de não perder timestamp offline.
QA: DebugView no device com o mesmo build de staging. Se o evento não aparece lá, não discute dashboard de mídia. Eventos de conversão que alimentam AEO/VO nas campanhas: eventos in-app para install.
Checklist
- Auto screen reporting off (
firebase.json/ equivalente). - Router dispara
logScreenView/ observer em toda troca de rota real. - Nested navigator: nome da folha, não do container.
- Consent Mode: default denied no Manifest/plist;
setConsentdepois do CMP. - Audit
PrivacyInfo.xcprivacyde app e dependências. - UI não chama vendor: Analytics Service tipado.
- Replay só depois de teste de bateria/CPU e regra de máscara.
- DebugView no device de QA antes do release.
Próximo passo
Pacote instalado sem router e sem gate de consentimento é o motivo pelo qual o GA4 mostra uma activity e o produto acha que “analytics não funciona”.
Se o app já é RN ou Flutter com Firebase (e às vezes MMP) no index.js / main.dart, o trabalho é desligar o automático, fechar o contrato de nome de tela e isolar o vendor — não adicionar o sétimo SDK.
Se quiser, partimos do seu navigator (React Navigation vs go_router) e dos destinos atuais e fechamos: mapa de telas, ordem do setConsent e o que o serviço multiplexa.