
🧠 De conceptos a implementación práctica
Si ya leíste sobre las 5 prácticas para apps estables, probablemente te quedaste con ganas de ver más detalles técnicos. Aquí van los 10 checks concretos que todo equipo de React Native debería validar antes de cada release.
Estos no son solo "buenas ideas", son validaciones reales con código que puedes copiar y adaptar a tu proyecto hoy mismo.
📋 Qué esperar de este checklist
Cada check incluye:
- 📖 Por qué importa - El contexto no-técnico para entender el valor
- 🔧 Cómo implementarlo - Código específico que puedes usar hoy
- ✅ Mejores prácticas - Consejos para evitar errores comunes
Este checklist está diseñado para ser tu prerelease gate: una lista de validación que revisas antes de cada producción. No necesitas implementar todos de golpe—empieza con los primeros 3 y ve añadiendo los demás gradualmente.
Cada check reduce el riesgo de un incidente en producción. Piénsalo como una lista de seguridad para tu equipo: no es burocracia, es protección.
1️⃣ Crash rate bajo control
📖 Por qué importa
Un crash es la peor experiencia que puede tener un usuario. Si tu app se cierra inesperadamente, no hay segunda oportunidad. El crash rate es tu indicador de salud más importante: un crash-free rate superior al 99,5% debería ser tu meta en producción.
🔧 Cómo implementarlo
Usa herramientas como Sentry, Firebase Crashlytics o Bugsnag para monitorear cada release.
Configuración básica con Sentry:
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: 'TU_DSN_AQUI',
debug: __DEV__, // Solo en desarrollo
tracesSampleRate: 1.0, // 100% de muestreo para sesiones
});
// Identifica el release y distribución
flatMap(
appInfo => {
Sentry.setRelease(`${appInfo.version}-${appInfo.buildNumber}`);
Sentry.setDist(appInfo.buildNumber);
}
)(Application.nativeApplicationVersion)();
// Captura errores de JavaScript
Sentry.captureException(error, {
tags: { section: 'Checkout' },
extra: { userId: user.id },
});
🚨 Evita esto:
- No captures TODO (filtra errores esperados)
- No guardes datos sensibles del usuario en logs
- No esperes a tener crashes para implementar monitoreo
✅ Mejores prácticas:
- Implementa
setRelease()ysetDist()para identificar qué versión introdujo un error - Agrupa errores por sesión y usuario
- Configura alertas cuando el crash rate supere el 0.5%
2️⃣ Manejo de errores centralizado
📖 Por qué importa
Ya sabes que addBreadcrumb() y captureException() son esenciales en tus funciones críticas (check #2 del artículo anterior). Pero aquí hay algo más: un error handler global que capture excepciones que ni siquiera sabías que existían.
Los errores silenciosos son como termitas: no los ves hasta que rompen toda la estructura. Un manejo de errores centralizado captura TODAS las excepciones—JavaScript, Native, promesas rechazadas y errores de red—para que nada se te escape.
🔧 Cómo implementarlo
Define un errorHandler global que capture todos los tipos de errores.
Instalación de dependencias:
npm install react-native-exception-handler @sentry/react-native
Configuración completa del error handler:
import { setJSExceptionHandler, setNativeExceptionHandler } from 'react-native-exception-handler';
import * as Sentry from '@sentry/react-native';
// Handler para errores de JavaScript
const errorHandler = (e: Error, isFatal: boolean) => {
if (isFatal) {
Sentry.captureException(e, {
tags: { type: 'JSException' },
level: 'fatal',
});
Alert.alert(
'Error fatal',
'Lo sentimos, la app necesita reiniciarse.',
[{ text: 'Cerrar' }]
);
} else {
// Error no fatal, solo loguear
Sentry.captureException(e, {
tags: { type: 'JSException' },
level: 'error',
});
}
};
setJSExceptionHandler(errorHandler, true);
// Handler para errores nativos
setNativeExceptionHandler(exceptionString => {
Sentry.captureMessage(exceptionString, {
tags: { type: 'NativeException' },
level: 'fatal',
});
});
// Captura promesas rechazadas sin catch
Promise.prototype.catch = function(originalCatch) {
return function(error: Error) {
Sentry.captureException(error, {
tags: { type: 'UnhandledPromise' },
});
return originalCatch.call(this, error);
};
}(Promise.prototype.catch);
🧩 Ejemplo práctico con breadcrumbs:
const handleUpdateProfile = async (user: User) => {
// Registra la intención del usuario
Sentry.addBreadcrumb({
category: 'user.action',
message: 'Usuario intenta actualizar perfil',
level: 'info',
data: { userId: user.id },
});
try {
await api.updateProfile(user);
Sentry.addBreadcrumb({
category: 'api.success',
message: 'Perfil actualizado exitosamente',
level: 'info',
});
Alert.alert('✅ Perfil actualizado');
} catch (error) {
Sentry.captureException(error, {
tags: { section: 'ProfileUpdate' },
extra: { userId: user.id, email: user.email },
});
Alert.alert('❌ Error al actualizar tu perfil. Inténtalo de nuevo.');
}
};
✅ Mejores prácticas:
- Diferencia entre errores recuperables (muestra un fallback) y críticos (reporta y detén)
- Añade contexto del usuario y acción previa con breadcrumbs
- Usa tags para agrupar errores por feature o sección
3️⃣ Versionado y seguimiento de builds
📖 Por qué importa
Imagina que un usuario reporta un bug y tú no sabes qué versión de la app está usando. Es como buscar una aguja en un pajar. Cada build debe tener un identificador único visible para que QA, soporte o usuarios puedan reportar problemas con precisión.
🔧 Cómo implementarlo
Cada build debe tener un identificador único visible en pantalla (vista de perfil o "about").
Instalación:
npm install expo-application
# O si no usas Expo:
npm install react-native-device-info
Pantalla de información de la app:
import * as Application from 'expo-application';
import { Text, View, TouchableOpacity } from 'react-native';
const AboutScreen = () => {
const version = Application.nativeApplicationVersion;
const buildNumber = Application.nativeBuildVersion;
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 16, marginBottom: 10 }}>
Versión: {version} ({buildNumber})
</Text>
{/* Permite copiar la info para reportar */}
<TouchableOpacity onPress={() => Clipboard.setString(`${version}-${buildNumber}`)}>
<Text>📋 Copiar información de versión</Text>
</TouchableOpacity>
</View>
);
};
Integra con Sentry para contexto automático:
// En tu archivo de inicialización (index.js o App.tsx)
import * as Application from 'expo-application';
Sentry.setContext('app', {
version: Application.nativeApplicationVersion,
build: Application.nativeBuildVersion,
deviceModel: Device.modelName,
osVersion: Device.osVersion,
});
✅ Mejores prácticas:
- Muestra versión + build number en formato legible (ej: "1.2.3 (456)")
- Permite copiar al portapapeles con un tap
- Incluye fecha del build si es posible
- Asegúrate que esté visible en múltiples pantallas
4️⃣ Feature flags activas y rollback rápido
📖 Por qué importa
¿Te imaginas poder desactivar una feature problemática sin tener que esperar días a que las stores aprueben un nuevo build? Los feature flags te dan ese superpoder. Nunca lances una feature directamente a todos los usuarios: configura flags que te permitan activar o desactivar funciones sin publicar una nueva versión.
🔧 Cómo implementarlo
Configura feature flags con ConfigCat, LaunchDarkly o tu propio backend.
Ejemplo con ConfigCat:
npm install configcat-react-native
import { withConfigCatProvider, useFeatureFlag } from 'configcat-react-native';
const configCatKey = 'TU_CONFIGCAT_KEY';
// Envuelve tu App
export default withConfigCatProvider(App, configCatKey);
// Usa en cualquier componente
const MyComponent = () => {
const { value: newFeatureEnabled, loading } = useFeatureFlag('NEW_FEATURE', false);
if (loading) return <Loading />;
return (
<>
{newFeatureEnabled ? (
<NewFeatureUI />
) : (
<OldFeatureUI />
)}
</>
);
};
Implementación sencilla sin servicios externos:
// services/FeatureFlags.ts
class FeatureFlagService {
private flags: Record<string, boolean> = {};
async fetchFlags() {
try {
const response = await fetch('https://tu-api.com/feature-flags');
const data = await response.json();
this.flags = data;
} catch (error) {
console.error('Error fetching flags:', error);
// Usa valores por defecto en caso de error
}
}
isEnabled(flag: string): boolean {
return this.flags[flag] ?? false;
}
async updateFlag(flag: string, enabled: boolean) {
this.flags[flag] = enabled;
}
}
export const featureFlags = new FeatureFlagService();
// Uso en componentes
const { isEnabled } = featureFlags;
if (isEnabled('SHOW_CHECKOUT_V2')) {
return <CheckoutV2 />;
}
✅ Mejores prácticas:
- Implementa valores por defecto (fallback) si el servicio de flags falla
- Versiona tus flags para mantener compatibilidad
- Monitorea el uso de cada flag para decidir cuándo remover código legacy
- Considera flags por usuario o porcentaje de rollout
5️⃣ Logs con contexto
📖 Por qué importa
Ya implementaste addBreadcrumb() en funciones críticas. Ahora escalémoslo: crea un sistema de logging centralizado que capture contexto en TODA la app, no solo en los puntos donde manualmente lo añades.
"El botón de pago no funciona" no te dice nada. "El usuario tocó el botón de pago después de 3 reintentos, Gmail abierto en background, iOS 15.2" sí.
Los logs sin contexto son como mapas sin puntos de referencia. Te dicen que algo falló, pero no por qué ni cómo llegaste ahí.
🔧 Cómo implementarlo
Captura logs útiles, no ruido.
Estructura un sistema de logging con niveles:
enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARNING = 'warning',
ERROR = 'error',
}
class Logger {
log(level: LogLevel, message: string, context?: Record<string, any>) {
const timestamp = new Date().toISOString();
// En desarrollo: consola
if (__DEV__) {
console.log(`[${level.toUpperCase()}] ${timestamp}:`, message, context);
}
// En producción: Sentry
if (level === LogLevel.ERROR) {
Sentry.captureMessage(message, {
level: level as Sentry.Severity,
extra: context,
});
} else {
Sentry.addBreadcrumb({
category: level,
message,
level: level as Sentry.Severity,
data: context,
});
}
}
debug(message: string, context?: Record<string, any>) {
this.log(LogLevel.DEBUG, message, context);
}
info(message: string, context?: Record<string, any>) {
this.log(LogLevel.INFO, message, context);
}
error(message: string, context?: Record<string, any>) {
this.log(LogLevel.ERROR, message, context);
}
}
export const logger = new Logger();
// Uso en tu app
logger.info('Usuario inició checkout', {
userId: user.id,
cartItems: cart.items.length,
});
logger.debug('Llamada API iniciada', {
endpoint: '/api/checkout',
method: 'POST',
payloadSize: JSON.stringify(payload).length,
});
Captura contexto del usuario:
// Configura contexto del usuario al iniciar sesión
Sentry.setUser({
id: user.id,
email: user.email,
username: user.username,
});
// Añade contexto global de la app
Sentry.setContext('device', {
model: Device.modelName,
osVersion: Device.osVersion,
memory: Device.totalMemory,
networkType: await getNetworkType(),
});
// Breadcrumbs para navegación
const navigationBreadcrumb = (screenName: string) => {
Sentry.addBreadcrumb({
category: 'navigation',
message: `User navigated to ${screenName}`,
level: 'info',
});
};
✅ Mejores prácticas:
- No loguees datos sensibles (contraseñas, tokens, tarjetas)
- Usa niveles apropiados (debug para dev, error para producción crítica)
- Limpia logs antiguos para evitar acumulación de memoria
- Incluye timestamps y trace IDs para debugging distribuido
6️⃣ Performance tracking real
📖 Por qué importa
Una app que funciona pero es lenta es una app rota desde la perspectiva del usuario. Mide lo que importa: tiempo de carga, latencia de red, JS thread y FPS. Si no mides, no puedes mejorar.
Ejemplo: Los usuarios abandonan si una pantalla tarda más de 3 segundos en cargar. Rastrear métricas de performance te permite identificar cuellos de botella antes de que lleguen a producción.
🔧 Cómo implementarlo
Usa Flipper, Sentry Performance o React Native Performance Monitor.
Configuración básica con Sentry Performance:
import * as Sentry from '@sentry/react-native';
// Rastrea transacciones (pantallas, flujos)
const loadProfileTransaction = Sentry.startTransaction({
name: 'LoadProfile',
op: 'navigation',
});
// Rastrea spans (operaciones específicas)
const fetchUserSpan = loadProfileTransaction.startChild({
op: 'http.client',
description: 'GET /api/user',
});
try {
const user = await api.getUser();
fetchUserSpan.setHttpStatus(200);
} catch (error) {
fetchUserSpan.setHttpStatus(error.response?.status || 500);
Sentry.captureException(error);
} finally {
fetchUserSpan.finish();
loadProfileTransaction.finish();
}
Mide tiempo de carga de pantallas:
import { useEffect } from 'react';
import { InteractionManager } from 'react-native';
const useScreenLoadTime = (screenName: string) => {
useEffect(() => {
const startTime = performance.now();
InteractionManager.runAfterInteractions(() => {
const loadTime = performance.now() - startTime;
Sentry.addBreadcrumb({
category: 'performance',
message: `${screenName} loaded in ${loadTime.toFixed(0)}ms`,
data: { loadTime },
});
// Alerta si es demasiado lento
if (loadTime > 3000) {
Sentry.captureMessage(`Slow screen load: ${screenName}`, {
level: 'warning',
extra: { loadTime },
});
}
});
}, []);
};
// Uso en componente
const ProfileScreen = () => {
useScreenLoadTime('ProfileScreen');
// ... resto del componente
};
Mide FPS (Frames Per Second):
// Para monitor de performance en tiempo real
import { useFPSMetrics } from 'react-native-performance-monitor';
const MyComponent = () => {
const fps = useFPSMetrics();
useEffect(() => {
if (fps < 30) {
Sentry.addBreadcrumb({
category: 'performance',
message: 'Low FPS detected',
data: { fps },
});
}
}, [fps]);
return null; // Componente invisible
};
✅ Mejores prácticas:
- Apunta a un TTI (Time To Interactive) menor a 5 segundos en dispositivos medios
- Rastrea métricas en dispositivos reales, no solo simuladores
- Configura alertas para degradación de performance
- Mide antes y después de optimizaciones para validar impacto
7️⃣ Testing automatizado en CI/CD
📖 Por qué importa
En el artículo anterior mencionamos "automatiza lo que te salva" con testing E2E y CI. Pero ¿cómo configurarlo realmente? Aquí está el setup completo.
Un bug detectado en CI cuesta 10 minutos de tu tiempo. El mismo bug en producción cuesta horas de debugging, stress del equipo, y pérdida de confianza de usuarios. Cada build debe pasar pruebas E2E, linting y type checking. Rompe el build si algo falla: es más barato detener un error en CI que en producción.
🔧 Cómo implementarlo
Cada build debe pasar pruebas E2E (con Detox o Maestro), linting y type checking.
Configuración de CI/CD con GitHub Actions:
# .github/workflows/ci.yml
name: CI
on: [pull_request] #[push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Type check
run: npm run type-check
- name: Run unit tests
run: npm run test
Ejemplo de tests E2E con Detox:
// e2e/checkout.e2e.ts
describe('Checkout Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should complete checkout successfully', async () => {
// Login
await element(by.id('email-input')).typeText('test@example.com');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
// Add item to cart
await element(by.id('product-card-0')).tap();
await element(by.id('add-to-cart-button')).tap();
// Go to checkout
await element(by.id('cart-button')).tap();
await element(by.id('checkout-button')).tap();
// Complete checkout
await element(by.id('pay-button')).tap();
await expect(element(by.text('Order confirmed'))).toBeVisible();
});
});
✅ Mejores prácticas:
- Prueba flujos críticos (login, checkout, settings)
- Corre tests en múltiples dispositivos y versiones de OS
- Asegúrate que el CI falle si algún test falla
- Mantén tests rápidos (< 10 minutos para conjunto completo)
8️⃣ Manejo de estados offline y errores de API
📖 Por qué importa
Ya hablamos de "diseñar para fallar gracefully". Ahora, el cómo técnico: implementa offline detection, automatic retry logic, y user-friendly error messages.
"Error 500" no dice nada al usuario; "Parece que tenemos problemas, inténtalo en unos minutos" sí. Los usuarios están en metro, en áreas con señal débil, o con WiFi que se cae constantemente. Una app que maneja bien estos casos es una app que genera confianza.
🔧 Cómo implementarlo
Implementa un estado de red global:
import NetInfo from '@react-native-community/netinfo';
const NetworkStatus = () => {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener(state => {
setIsConnected(state.isConnected);
// Reporta cambios de conectividad
Sentry.addBreadcrumb({
category: 'network',
message: state.isConnected ? 'Network connected' : 'Network disconnected',
data: { connectionType: state.type },
});
});
return () => unsubscribe();
}, []);
return { isConnected };
};
// Componente de UI offline
const OfflineBanner = () => {
const { isConnected } = NetworkStatus();
if (!isConnected) {
return (
<View style={styles.offlineBanner}>
<Text>Sin conexión. Revisando...</Text>
</View>
);
}
return null;
};
Implementa retry con exponential backoff:
const fetchWithRetry = async (
url: string,
options: RequestInit,
maxRetries = 3
) => {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
// Si es error 5xx, reintentar
if (response.status >= 500 && i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
continue;
}
return response;
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
};
// Uso
try {
const response = await fetchWithRetry('/api/checkout', {
method: 'POST',
body: JSON.stringify(cartData),
});
} catch (error) {
Alert.alert(
'Error de conexión',
'Parece que tenemos problemas. Por favor intenta de nuevo en unos momentos.',
[{ text: 'Reintentar', onPress: retryCheckout }]
);
}
✅ Mejores prácticas:
- Revisa patrones de retry y circuit breakers
- Cachea datos críticos localmente para modo offline
- Muestra estados de carga claros ("Guardando...", "Sincronizando...")
- Distingue entre errores del usuario y errores recuperables del servidor
9️⃣ Seguridad y datos sensibles
📖 Por qué importa
Una brecha de seguridad puede destruir tu app en minutos. Asegúrate de no exponer tokens ni credenciales. Valida certificados, usa almacenamiento seguro y configura correctamente HTTPS y App Transport Security.
🔧 Cómo implementarlo
Almacenamiento seguro de tokens:
import * as Keychain from 'react-native-keychain';
import * as SecureStore from 'expo-secure-store';
// Guardar token
await Keychain.setGenericPassword('authToken', userToken, {
service: 'myApp',
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY,
});
// Recuperar token
const credentials = await Keychain.getGenericPassword({ service: 'myApp' });
const token = credentials ? credentials.password : null;
O con Expo:
await SecureStore.setItemAsync('authToken', token, {
requireAuthentication: true,
});
const token = await SecureStore.getItemAsync('authToken');
SSL Pinning para APIs críticas:
import RNFetchBlob from 'rn-fetch-blob';
const response = await RNFetchBlob.config({
trusty: true,
}).fetch('GET', 'https://api.example.com/data');
// En producción, usa certificados específicos
Ocultar datos sensibles de logs:
const sanitizeLogData = (data: any): any => {
if (typeof data !== 'object') return data;
const sensitiveKeys = ['password', 'token', 'ssn', 'creditCard'];
const sanitized = { ...data };
Object.keys(sanitized).forEach(key => {
if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
sanitized[key] = '***REDACTED***';
}
});
return sanitized;
};
logger.info('Login attempt', sanitizeLogData({ email: user.email, password: user.password }));
// Logs: { email: 'user@example.com', password: '***REDACTED***' }
✅ Mejores prácticas:
- Usa
react-native-keychainoexpo-secure-storepara guardar datos sensibles - Nunca hardcodees secrets en el código
- Implementa SSL pinning para APIs críticas
- Revoca tokens cuando detectes actividad sospechosa
- Valida y sanitiza todos los inputs del usuario
🔟 Alertas y visibilidad en tiempo real
📖 Por qué importa
Ya definimos las métricas de estabilidad que debemos trackear (del artículo anterior). Ahora, automatizemos las alertas: tu app no debería depender del usuario para enterarte de un bug.
Configura alertas automáticas (Slack, Discord o PagerDuty) cuando haya picos de errores o baja estabilidad. Un equipo maduro tiene visibilidad antes de que el problema escale. Las alertas proactivas te permiten reaccionar en minutos, no en horas.
🔧 Cómo implementarlo
Configuración de alertas en Sentry:
// En Sentry Dashboard, configura alertas:
// - Crash rate > 0.5%
// - Nuevos errores
// - Degradación de performance
// En tu código, trigger manual de alertas críticas
if (criticalError) {
Sentry.captureException(error, {
tags: { severity: 'critical' },
extra: { requiresImmediateAction: true },
});
}
Webhook a Slack:
const sendSlackAlert = async (message: string) => {
await fetch('https://hooks.slack.com/services/YOUR/WEBHOOK/URL', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: message,
channel: '#alerts',
}),
});
};
// En tu error handler
const errorHandler = (error: Error, isFatal: boolean) => {
if (isFatal) {
sendSlackAlert(`🚨 Crash fatal detectado: ${error.message}`);
}
};
Dashboard de métricas clave:
// Configura en tu servicio de monitoreo un dashboard con:
// 1. Crash rate (últimas 24h)
// 2. Error rate por release
// 3. Performance promedio (TTI, FPS)
// 4. Usuarios activos vs usuarios con errores
✅ Mejores prácticas:
- Configura alertas en Slack, Discord o PagerDuty
- Define umbrales claros (ej: > 1% crash rate = alerta)
- Incluye contexto útil en alertas (versión, dispositivo, stack trace)
- Revisa y ajusta alertas para evitar ruido excesivo
- Ten un runbook para responder a cada tipo de alerta
🚀 Checklist final
| Check | Estado | |-------|--------| | Crash rate < 0.5% | ✅ | | Error handler global | ✅ | | Feature flags implementadas | ✅ | | Logs con contexto | ✅ | | Tests automatizados | ✅ | | Offline y fallback UI | ✅ | | Seguridad básica cubierta | ✅ | | Alertas en tiempo real | ✅ |
🧩 De checklist a proceso rutinario
Los 5 fundamentos del artículo anterior + estos 10 checks de implementación = Tu escudo de resiliencia completo.
No hay app sin bugs, pero sí equipos con visibilidad y procesos sólidos.
Cómo implementar ambos artículos juntos:
Semana 1-2: Fundamentos (del artículo anterior)
- Instala Sentry y configura crash tracking (Check #1 de este artículo)
- Implementa error handlers globales (Check #2)
Semana 3-4: Profundiza
- Configura versionado y build tracking (Check #3)
- Implementa feature flags básicos (Check #4)
Mes 2: Automatización
- Agrega CI/CD con tests E2E (Check #7)
- Configura performance tracking (Check #6)
Mes 3: Madurez
- Implementa manejo offline robusto (Check #8)
- Revisa seguridad y alertas (Checks #9 y #10)
Cada check es una inversión en resiliencia. La estabilidad no es glamorosa, pero es lo que separa una app de un producto que perdura.
💡 Tip: Comparte este checklist con tu equipo antes del próximo release. Hagan una quick review juntos.
¿Te gustó este enfoque?
💬 Cuéntame qué otros checks aplicas en tus proyectos o qué herramientas te han salvado en producción.