Noções Básicas de Observabilidade
10 exemplos para você começar com observabilidade móvel - 7 básicos e 3 intermediários. Abrange travamentos nativos , ANRs , erros de JavaScript e como eles se encaixam na pilha de sinais que toda equipe Expo envia antes do lançamento na loja.
Crie um aplicativo TypeScript em branco. Os exemplos usam APIs e padrões integrados que funcionam em builds de lançamento do Expo SDK 57 - nenhum SDK de fornecedor é necessário até que você configure o Sentry em uma página posterior.
npx create-expo-app@latest MyObservabilityApp --template blank-typescript
cd MyObservabilityApp
npx expo start
Ferramentas: Estes exemplos visam Expo SDK 57 (expo ~57.0.4), React Native 0.86.0 e React 19.2.3 .
A pilha de sinais móvel tem quatro camadas que as equipes instrumentam em ordem:
Camada O que captura Ferramenta típica Crash / fatal Nativo SIGSEGV, JS fatal, morte do processo Sentry, Firebase Crashlytics Erro / tratado Falhas de API capturadas, limites de fronteira, JS global Sentry + seu logger Desempenho Início frio, TT de navegação, latência de API Transações Sentry, OTel Análise de produto Funis, retenção, uso de recursos Amplitude, Segment
Use todos os quatro. Um painel sem travamentos com zero orçamentos de desempenho ainda envia lançamentos lentos.
As plataformas móveis classificam as falhas de forma diferente - seus painéis também devem fazer isso.
// src/observability/taxonomy.ts
export type MobileFailureKind =
| "native_crash" // processo terminado - nativo iOS/Android
| "js_fatal" // ErrorUtils isFatal - o bundle pode estar instável
| "js_non_fatal" // capturado, relatado, o aplicativo continua
| "anr" // Android: thread principal bloqueada ~5s+ (Application Not Responding)
| "handled_error" ; // API 5xx exibida na UI - ainda vale a pena registrar
export type FailureReport = {
kind : MobileFailureKind ;
message : string ;
screen ?: string ;
release : string ; // build nativo: 1.4.2 (42)
otaUpdateId ?: string ; // ID do manifesto do expo-updates
sessionId : string ;
};
Travamento nativo encerra a sessão - o usuário deve reiniciar
Fatal de JS pode ou não matar o processo dependendo do motor e do local do erro
ANR é específico do Android - os encerramentos do watchdog do iOS são mais próximos da taxonomia de travamentos nativos
Erros tratados pertencem a logs e rastreadores de problemas não fatais - não ao numerador da taxa de sessões sem travamentos
Relacionado: Manipuladores Globais de Erro - ErrorUtils.setGlobalHandler para falhas de JS
Sem identidade de build, você não pode vincular um pico a uma atualização OTA ou a um build da loja.
// src/observability/releaseContext.ts
import * as Application from "expo-application" ;
import * as Updates from "expo-updates" ;
export async function getReleaseContext () {
const nativeVersion = Application.nativeApplicationVersion ?? "unknown" ;
const buildNumber = Application.nativeBuildVersion ?? "unknown" ;
const ota = Updates.updateId ?? "embedded" ;
const channel = Updates.channel ?? "none" ;
return {
release: `${ nativeVersion }+${ buildNumber }` ,
otaUpdateId: ota,
channel,
runtimeVersion: Updates.runtimeVersion ?? "unknown" ,
};
}
npx expo install expo-application expo-updates
release deve corresponder ao que Sentry e seu backend esperam - version+build é um formato comum
ID da atualização OTA distingue um bundle JavaScript ruim de um binário nativo ruim - veja Noções Básicas de Atualizações OTA
Versão do runtime controla quais OTAs se aplicam - incompatibilidades parecem relatórios misteriosos de "bugs antigos"
Registre o contexto uma vez no início da sessão e anexe a cada erro, lote de logs e trace
Vincule travamentos, logs e traces de API a uma jornada do usuário.
// src/observability/session.ts
import * as Crypto from "expo-crypto" ;
let sessionId : string | null = null ;
export async function getSessionId () {
if ( ! sessionId) {
const bytes = await Crypto. getRandomBytesAsync ( 16 );
sessionId = Array. from (bytes)
. map (( b ) => b. toString ( 16 ). padStart ( 2 , "0" ))
. join ( "" );
}
return sessionId;
}
Gere um novo ID de sessão na inicialização fria - não a cada foreground
Rotacione no logout quando os limites de PII mudarem - combine com Noções Básicas de Autenticação Móvel
Envie sessionId como um cabeçalho (X-Session-Id) em chamadas de API para junção no lado do servidor
Nunca use o ID de sessão como ID de usuário - sessões são anônimas até a autenticação
Agrupar travamentos cientes da rota reduz drasticamente o tempo de triagem.
// src/observability/useActiveRoute.ts
import { usePathname, useSegments } from "expo-router" ;
import { useEffect } from "react" ;
type RouteReporter = ( route : string ) => void ;
export function useActiveRoute ( report : RouteReporter ) {
const pathname = usePathname ();
const segments = useSegments ();
useEffect (() => {
const route = pathname || segments. join ( "/" ) || "unknown" ;
report (route);
}, [pathname, segments, report]);
}
// app/_layout.tsx - conecte uma vez na raiz
import { useCallback, useRef } from "react" ;
import { useActiveRoute } from "@/observability/useActiveRoute" ;
const currentRoute = { value: "boot" };
export default function RootLayout () {
const report = useCallback (( route : string ) => {
currentRoute.value = route;
// crashSdk.setTag("route", route);
}, []);
useActiveRoute (report);
return null ; // Stack no app real
}
Prefira pathname em vez de nomes de exibição de componentes - refatorações renomeiam componentes, rotas permanecem estáveis
Atualize a tag de rota em cada navegação - tags de rota desatualizadas atribuem incorretamente travamentos
Para modais e stacks aninhados, inclua segmentos: checkout/payment
Transações de desempenho devem usar a mesma chave de rota - veja Monitoramento de Desempenho
Breadcrumbs são uma trilha ordenada por tempo - mantenha-os pequenos e estruturados.
// src/observability/breadcrumbs.ts
export type Breadcrumb = {
ts : number ;
category : "navigation" | "network" | "auth" | "user" | "console" ;
message : string ;
data ?: Record < string , string | number | boolean >;
};
const MAX_BREADCRUMBS = 50 ;
const ring : Breadcrumb [] = [];
export function addBreadcrumb ( crumb : Omit < Breadcrumb , "ts" >) {
ring. push ({ ... crumb, ts: Date. now () });
if (ring. length > MAX_BREADCRUMBS ) ring. shift ();
}
export function getBreadcrumbs () {
return [ ... ring];
}
addBreadcrumb ({
category: "network" ,
message: "GET /v1/feed falhou" ,
data: { status: 503 , durationMs: 4200 },
});
Limite o tamanho do anel - arrays ilimitados causam OOM em telas falantes
Nunca coloque tokens, e-mails ou números de cartão em data - apenas hashes ou booleanos
Categorias mapeiam claramente para Sentry e índices de logs estruturados
Breadcrumbs de autenticação (login_success, logout) ajudam a reproduzir bugs de sessão sem armazenar credenciais
Seu painel precisa de dois caminhos de ingestão.
// src/observability/report.ts
import type { FailureReport } from "./taxonomy" ;
type Reporter = ( report : FailureReport ) => void ;
export function reportJsError (
report : Reporter ,
error : Error ,
meta : { isFatal : boolean ; screen : string ; release : string }
) {
report ({
kind: meta.isFatal ? "js_fatal" : "js_non_fatal" ,
message: error.message,
screen: meta.screen,
release: meta.release,
sessionId: "…" , // de getSessionId()
});
}
// Travamentos nativos são capturados pelos SDKs nativos @sentry/react-native / Crashlytics.
// A camada JS só os vê como "sessão anterior terminou inesperadamente" no próximo lançamento.
Instale o SDK do React Native com módulos nativos habilitados - wrappers apenas em JS perdem SIGABRT
Simbolize ambos os mapas de bytecode Hermes e dSYMs nativos / mapeamentos ProGuard
Após um travamento nativo, os logs no dispositivo se foram , a menos que você os tenha enviado no foreground anterior - veja Logs Estruturados no Dispositivo
Falhas de JS ainda podem permitir componentDidCatch em árvores irmãs - travamentos nativos não
Produto e engenharia devem concordar em uma métrica principal de confiabilidade.
// src/observability/metrics.ts
export type SessionOutcome = "clean" | "crashed" | "unknown" ;
export function crashFreeRate ( sessions : SessionOutcome []) {
const known = sessions. filter (( s ) => s !== "unknown" );
const clean = known. filter (( s ) => s === "clean" ). length ;
return known. length === 0 ? 1 : clean / known. length ;
}
// Exemplo de SLO: 99,5% de sessões sem travamentos em 28 dias (ver slos-for-mobile-apps)
Uma sessão termina em travamento, encerramento forçado ou tempo limite em segundo plano - defina a regra explicitamente
Usuários sem travamentos é uma métrica mais rigorosa - um dispositivo ruim não deve dominar
Exclua builds de desenvolvimento e internos dos numeradores de SLO de produção
Combine com SLOs de início frio e sucesso de OTA - SLOs para Aplicativos Móveis
Encadeie o manipulador anterior e anexe o contexto de lançamento antes de encaminhar para seu relator.
// src/bootstrap/installObservability.ts
import { getReleaseContext } from "@/observability/releaseContext" ;
import { getBreadcrumbs } from "@/observability/breadcrumbs" ;
type GlobalHandler = ( error : Error , isFatal ?: boolean ) => void ;
declare const ErrorUtils : {
getGlobalHandler () : GlobalHandler ;
setGlobalHandler ( handler : GlobalHandler ) : void ;
};
export function installGlobalObservability (
send : ( payload : Record < string , unknown >) => void
) {
const previous = ErrorUtils. getGlobalHandler ();
ErrorUtils. setGlobalHandler ( async ( error , isFatal = false ) => {
const release = await getReleaseContext ();
send ({
kind: isFatal ? "js_fatal" : "js_non_fatal" ,
message: error.message,
stack: error.stack,
isFatal,
breadcrumbs: getBreadcrumbs (),
... release,
});
previous (error, isFatal);
});
}
Chame uma vez no bootstrap - antes de provedores e navegação
Sempre encadeie previous - o redbox do desenvolvedor e os SDKs de fornecedores dependem dele
O trabalho assíncrono no manipulador deve ser fire-and-forget - não bloqueie o runtime
Espelhe a mesma forma de carga útil em beforeSend do Sentry para painéis consistentes
Relacionado: Noções Básicas de Tratamento de Erros - modelo de erro de três camadas
ANRs vêm de trabalho síncrono na thread da UI - muitas vezes análise de JSON, inundações de logs ou useMemo pesado em arrays grandes.
import { useEffect, useRef } from "react" ;
import { InteractionManager } from "react-native" ;
export function useMainThreadWatchdog ( thresholdMs = 2000 ) {
const lastTick = useRef (Date. now ());
useEffect (() => {
const id = setInterval (() => {
const now = Date. now ();
const gap = now - lastTick.current;
lastTick.current = now;
if (gap > thresholdMs) {
console. warn ( "[perf] event loop gap ms" , gap);
// report non-fatal: possível ANR / thread JS travada
}
}, 500 );
return () => clearInterval (id);
}, [thresholdMs]);
}
// Adie trabalho pesado fora do caminho crítico
export function deferAfterInteractions ( task : () => void ) {
InteractionManager. runAfterInteractions (task);
}
Lacunas de 2000ms em um timer de 500ms sugerem bloqueio da thread JS - investigue antes que os usuários vejam diálogos de ANR
Grandes conjuntos de dados FlatList sem virtualização são uma fonte comum de ANR - Noções Básicas de Desempenho
A decodificação de imagem na thread JS durante a rolagem causa travamentos - use ativos de tamanho apropriado
Android Vitals relata ANRs no Play Console - correlacione com suas tags de lançamento e OTA
Conecte o relatório de travamentos, logs e análise por trás de uma única fachada para que as telas permaneçam limpas.
// src/observability/index.ts
import { getReleaseContext } from "./releaseContext" ;
import { getSessionId } from "./session" ;
import { addBreadcrumb, getBreadcrumbs } from "./breadcrumbs" ;
export const observability = {
async baseContext () {
const [ release , sessionId ] = await Promise . all ([
getReleaseContext (),
getSessionId (),
]);
return { ... release, sessionId };
},
breadcrumb: addBreadcrumb,
getBreadcrumbs,
captureException ( error : Error , extra ?: Record < string , unknown >) {
// encaminhar para Sentry + buffer de log
console. error ( "[capture]" , error.message, extra);
},
track ( event : string , props ?: Record < string , string | number | boolean >) {
// encaminhar para análise com portão de consentimento - ver analytics-and-privacy
},
};
// app/_layout.tsx
import { useEffect } from "react" ;
import { installGlobalObservability } from "@/bootstrap/installObservability" ;
import { observability } from "@/observability" ;
installGlobalObservability (( payload ) => {
observability. captureException ( new Error (payload.message as string ), payload);
});
export default function RootLayout () {
useEffect (() => {
observability. baseContext (). then (( ctx ) => {
observability. track ( "app_open" , { channel: ctx.channel });
});
}, []);
return null ;
}
Uma fachada impede que cada tela importe três SDKs
baseContext() é executado uma vez por sessão - armazene em cache internamente
Chamadas de análise devem respeitar o consentimento - substitua track até que os portões sejam resolvidos
Expanda esta fachada em Sentry para React Native e Logs Estruturados no Dispositivo
Qual é a diferença entre observabilidade e análise?
Observabilidade responde "o que quebrou e por quê" - travamentos, erros, latência, traces.
Análise responde "o que os usuários fizeram" - funis, retenção, experimentos.
Compartilhe contexto de lançamento e IDs de sessão; não duplique fornecedores sem motivo.
Preciso de ferramentas separadas para travamentos de iOS e Android?
SDKs modernos de RN (Sentry, Crashlytics) enviam um pacote com módulos nativos para ambas as plataformas.
Você ainda precisa de arquivos de símbolo específicos da plataforma - dSYM (iOS) e mapeamento ProGuard/R8 (Android).
O Expo Go pode relatar travamentos semelhantes aos de produção?
Não - o Expo Go usa um shell nativo e um ID de bundle diferentes. Valide em builds de desenvolvimento e binários de pré-visualização EAS .
Como as atualizações OTA afetam a atribuição de travamentos?
Marque relatórios com updateId e runtimeVersion do expo-updates.
Um pico após a publicação de OTA sem um lançamento na loja quase sempre aponta para JavaScript - reverta o canal de atualização primeiro.
Versões da pilha: Esta página foi escrita para React 19.2.3 , React Native 0.86.0 e Expo SDK 57 (expo ~57.0.4).