Module 12: Production and Deployment
Crash Reporting and Analytics
Monitor your app's health and understand user behavior
🎯 Learning Objectives
- Set up crash reporting with Sentry
- Configure error boundaries for graceful error handling
- Implement analytics tracking with Expo and third-party services
- Monitor app performance metrics
- Create custom events and user properties
- Use dashboards to make data-driven decisions
Why Monitoring Matters
Once your app is in production, you need visibility into how it's performing and what issues users are experiencing.
The Monitoring Stack
flowchart TB
subgraph App["Your App"]
A1[User Actions]
A2[Errors/Crashes]
A3[Performance Data]
end
subgraph Services["Monitoring Services"]
S1[Crash Reporting
Sentry, Bugsnag]
S2[Analytics
Amplitude, Mixpanel]
S3[Performance
Sentry, Firebase]
end
subgraph Insights["Insights"]
I1[Fix bugs faster]
I2[Understand users]
I3[Improve performance]
end
A1 --> S2
A2 --> S1
A3 --> S3
S1 --> I1
S2 --> I2
S3 --> I3
style App fill:#e3f2fd
style Services fill:#fff3e0
style Insights fill:#e8f5e9
Key Metrics to Track
| Category | Metrics | Why It Matters |
|---|---|---|
| Stability | Crash-free rate, error rate | User experience quality |
| Engagement | DAU, MAU, session length | Product-market fit |
| Retention | D1, D7, D30 retention | Long-term value |
| Performance | App start time, API latency | User satisfaction |
| Conversion | Signup rate, purchase rate | Business success |
Sentry Crash Reporting
Sentry is the most popular crash reporting solution for React Native, with excellent Expo support.
Installation
# Install Sentry for Expo
npx expo install @sentry/react-native
# This installs the Sentry SDK configured for Expo
Create Sentry Project
// 1. Sign up at sentry.io
// 2. Create new project
// 3. Select "React Native"
// 4. Copy your DSN
// DSN looks like:
// https://xxxxx@yyy.ingest.sentry.io/zzzzz
Configure Sentry
// app.json - Add Sentry plugin
{
"expo": {
"plugins": [
[
"@sentry/react-native/expo",
{
"organization": "your-org",
"project": "your-project"
}
]
]
}
}
// App.tsx - Initialize Sentry
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: 'https://xxxxx@yyy.ingest.sentry.io/zzzzz',
// Set environment
environment: __DEV__ ? 'development' : 'production',
// Enable in production only
enabled: !__DEV__,
// Sample rate (1.0 = 100% of errors)
sampleRate: 1.0,
// Trace sample rate for performance
tracesSampleRate: 0.2,
// Enable native crash reporting
enableNative: true,
// Attach screenshots on crash
attachScreenshot: true,
// Debug mode (disable in production)
debug: __DEV__,
});
export default function App() {
return (
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
);
}
Capturing Errors
import * as Sentry from '@sentry/react-native';
// Automatic error capture (unhandled errors)
// Sentry automatically captures:
// - Unhandled JavaScript errors
// - Native crashes
// - Promise rejections
// Manual error capture
try {
await riskyOperation();
} catch (error) {
Sentry.captureException(error);
}
// Capture with extra context
Sentry.captureException(error, {
tags: {
feature: 'checkout',
paymentMethod: 'credit_card',
},
extra: {
orderId: '12345',
amount: 99.99,
},
});
// Capture message (non-error)
Sentry.captureMessage('User completed onboarding');
// Set user context
Sentry.setUser({
id: user.id,
email: user.email,
username: user.name,
});
// Clear user on logout
Sentry.setUser(null);
// Add breadcrumbs (trail of events)
Sentry.addBreadcrumb({
category: 'navigation',
message: 'Navigated to checkout',
level: 'info',
});
Source Maps for Readable Stack Traces
# EAS Build automatically uploads source maps
# Just ensure Sentry plugin is configured
# For manual builds, upload source maps:
npx sentry-cli sourcemaps upload \
--org your-org \
--project your-project \
./dist
# Verify source maps are working:
# 1. Trigger a test error
# 2. Check Sentry dashboard
# 3. Stack trace should show original source code
Release Tracking
// app.config.js - Add release info
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: '...',
// Track releases
release: `${Constants.expoConfig?.version}+${
Constants.expoConfig?.ios?.buildNumber ||
Constants.expoConfig?.android?.versionCode
}`,
// Distribution (for source map matching)
dist: Constants.expoConfig?.ios?.buildNumber?.toString() ||
Constants.expoConfig?.android?.versionCode?.toString(),
});
Error Boundaries
Error boundaries catch errors in the React component tree and display a fallback UI instead of crashing.
Basic Error Boundary
// components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import * as Sentry from '@sentry/react-native';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Report to Sentry
Sentry.captureException(error, {
extra: {
componentStack: errorInfo.componentStack,
},
});
}
handleReset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<View style={styles.container}>
<Text style={styles.title}>Something went wrong</Text>
<Text style={styles.message}>
We've been notified and are working on a fix.
</Text>
<Button title="Try Again" onPress={this.handleReset} />
</View>
);
}
return this.props.children;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 10,
},
message: {
fontSize: 16,
color: '#666',
textAlign: 'center',
marginBottom: 20,
},
});
export default ErrorBoundary;
Using Sentry's Error Boundary
// Sentry provides its own error boundary
import * as Sentry from '@sentry/react-native';
function App() {
return (
<Sentry.ErrorBoundary
fallback={({ error, resetError }) => (
<View style={styles.errorContainer}>
<Text>An error occurred</Text>
<Text>{error.message}</Text>
<Button title="Reset" onPress={resetError} />
</View>
)}
>
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
</Sentry.ErrorBoundary>
);
}
Screen-Level Error Boundaries
// Wrap individual screens for isolated error handling
function ProfileScreen() {
return (
<ErrorBoundary fallback={<ProfileErrorFallback />}>
<ProfileContent />
</ErrorBoundary>
);
}
// HOC for easier use
function withErrorBoundary<P extends object>(
Component: React.ComponentType<P>,
FallbackComponent?: React.ComponentType
) {
return function WrappedComponent(props: P) {
return (
<ErrorBoundary fallback={FallbackComponent ? <FallbackComponent /> : undefined}>
<Component {...props} />
</ErrorBoundary>
);
};
}
// Usage
export default withErrorBoundary(ProfileScreen, ProfileErrorFallback);
Analytics Setup
Analytics help you understand user behavior and make data-driven decisions.
Popular Analytics Options
| Service | Best For | Pricing |
|---|---|---|
| Amplitude | Product analytics, funnels | Free tier, paid plans |
| Mixpanel | Event tracking, cohorts | Free tier, paid plans |
| PostHog | Open source, self-host option | Free tier, paid plans |
| Firebase Analytics | Google ecosystem | Free |
Setting Up Amplitude
# Install Amplitude
npm install @amplitude/analytics-react-native
# Or with Expo
npx expo install @amplitude/analytics-react-native
// services/analytics.ts
import * as Amplitude from '@amplitude/analytics-react-native';
const AMPLITUDE_API_KEY = process.env.EXPO_PUBLIC_AMPLITUDE_KEY || '';
// Initialize Amplitude
export async function initAnalytics() {
if (__DEV__) {
console.log('Analytics disabled in development');
return;
}
await Amplitude.init(AMPLITUDE_API_KEY, undefined, {
trackingOptions: {
ipAddress: false, // Privacy
},
});
}
// Identify user
export function identifyUser(userId: string, properties?: Record<string, any>) {
Amplitude.setUserId(userId);
if (properties) {
const identify = new Amplitude.Identify();
Object.entries(properties).forEach(([key, value]) => {
identify.set(key, value);
});
Amplitude.identify(identify);
}
}
// Track event
export function trackEvent(
eventName: string,
properties?: Record<string, any>
) {
Amplitude.track(eventName, properties);
}
// Track screen view
export function trackScreen(screenName: string) {
Amplitude.track('Screen Viewed', { screen_name: screenName });
}
// Reset on logout
export function resetAnalytics() {
Amplitude.reset();
}
Initialize in App
// App.tsx
import { useEffect } from 'react';
import { initAnalytics } from './services/analytics';
export default function App() {
useEffect(() => {
initAnalytics();
}, []);
return (
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
);
}
Track Navigation
// Automatic screen tracking with React Navigation
import { NavigationContainer } from '@react-navigation/native';
import { useRef } from 'react';
import { trackScreen } from './services/analytics';
function App() {
const navigationRef = useRef(null);
const routeNameRef = useRef<string>();
return (
<NavigationContainer
ref={navigationRef}
onReady={() => {
routeNameRef.current = navigationRef.current?.getCurrentRoute()?.name;
}}
onStateChange={() => {
const previousRouteName = routeNameRef.current;
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
if (previousRouteName !== currentRouteName && currentRouteName) {
trackScreen(currentRouteName);
}
routeNameRef.current = currentRouteName;
}}
>
<RootNavigator />
</NavigationContainer>
);
}
Custom Events and Tracking
Define meaningful events that track user behavior and business metrics.
Event Naming Conventions
// Use consistent naming patterns
// Format: Object_Action or action_object
// Examples:
// - button_clicked
// - screen_viewed
// - purchase_completed
// - signup_started
// Good event names:
trackEvent('signup_completed', { method: 'email' });
trackEvent('item_added_to_cart', { product_id: '123', price: 29.99 });
trackEvent('checkout_started', { item_count: 3, total: 89.97 });
trackEvent('purchase_completed', { order_id: 'ORD123', revenue: 89.97 });
// Bad event names:
trackEvent('click'); // Too vague
trackEvent('User did thing'); // Not standardized
trackEvent('btnSignup'); // Inconsistent format
Common Events to Track
// Authentication
trackEvent('signup_started');
trackEvent('signup_completed', { method: 'google' });
trackEvent('login_completed', { method: 'email' });
trackEvent('logout');
trackEvent('password_reset_requested');
// Onboarding
trackEvent('onboarding_started');
trackEvent('onboarding_step_completed', { step: 1, step_name: 'profile' });
trackEvent('onboarding_completed');
trackEvent('onboarding_skipped', { step: 2 });
// Core Features
trackEvent('task_created', { has_due_date: true, has_reminder: true });
trackEvent('task_completed', { time_to_complete_hours: 24 });
trackEvent('project_created', { template_used: true });
trackEvent('search_performed', { query_length: 15, results_count: 8 });
// Engagement
trackEvent('share_initiated', { content_type: 'task', method: 'native' });
trackEvent('notification_received', { type: 'reminder' });
trackEvent('notification_opened', { type: 'reminder' });
// Monetization
trackEvent('paywall_viewed', { source: 'feature_limit' });
trackEvent('subscription_started', { plan: 'pro', billing: 'monthly' });
trackEvent('trial_started', { plan: 'pro' });
trackEvent('purchase_completed', { revenue: 9.99, currency: 'USD' });
User Properties
// Set user properties for segmentation
import * as Amplitude from '@amplitude/analytics-react-native';
function setUserProperties(user: User) {
const identify = new Amplitude.Identify();
// Demographics
identify.set('account_type', user.accountType); // 'free', 'pro', 'enterprise'
identify.set('signup_date', user.createdAt);
identify.set('signup_source', user.source);
// Usage stats
identify.set('total_tasks_created', user.taskCount);
identify.set('projects_count', user.projectCount);
identify.set('team_size', user.teamSize);
// Preferences
identify.set('dark_mode_enabled', user.preferences.darkMode);
identify.set('notifications_enabled', user.preferences.notifications);
identify.set('language', user.preferences.language);
Amplitude.identify(identify);
}
// Increment counters
function incrementTaskCount() {
const identify = new Amplitude.Identify();
identify.add('total_tasks_created', 1);
Amplitude.identify(identify);
}
Tracking Hook
// hooks/useAnalytics.ts
import { useCallback } from 'react';
import { trackEvent, trackScreen } from '../services/analytics';
export function useAnalytics() {
const track = useCallback((event: string, properties?: Record<string, any>) => {
trackEvent(event, properties);
}, []);
const screen = useCallback((screenName: string) => {
trackScreen(screenName);
}, []);
return { track, screen };
}
// Usage in component
function CheckoutScreen() {
const { track } = useAnalytics();
const handlePurchase = async () => {
track('checkout_started', { item_count: cart.length });
try {
const result = await processPayment();
track('purchase_completed', {
order_id: result.orderId,
revenue: result.total,
});
} catch (error) {
track('purchase_failed', { error: error.message });
}
};
return (
<Button title="Complete Purchase" onPress={handlePurchase} />
);
}
Performance Monitoring
Track app performance to identify slowdowns and optimize user experience.
Sentry Performance
// Enable performance monitoring in Sentry init
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: '...',
// Enable performance monitoring
tracesSampleRate: 0.2, // 20% of transactions
// Or use sampling function for control
tracesSampler: (samplingContext) => {
// Sample all errors
if (samplingContext.transactionContext.name.includes('error')) {
return 1.0;
}
// Sample 10% of navigation
if (samplingContext.transactionContext.op === 'navigation') {
return 0.1;
}
// Default
return 0.05;
},
// Enable profiling (beta)
profilesSampleRate: 0.1,
});
Wrap Navigation for Tracing
// Automatic navigation performance tracking
import * as Sentry from '@sentry/react-native';
import { NavigationContainer } from '@react-navigation/native';
const routingInstrumentation = new Sentry.ReactNavigationInstrumentation();
Sentry.init({
dsn: '...',
integrations: [
new Sentry.ReactNativeTracing({
routingInstrumentation,
tracingOrigins: ['localhost', 'api.yourapp.com'],
}),
],
});
function App() {
return (
<NavigationContainer
onReady={() => {
routingInstrumentation.registerNavigationContainer(navigationRef);
}}
>
<RootNavigator />
</NavigationContainer>
);
}
Custom Performance Spans
import * as Sentry from '@sentry/react-native';
// Measure API calls
async function fetchUserData(userId: string) {
const transaction = Sentry.startTransaction({
name: 'fetch-user-data',
op: 'http.client',
});
const span = transaction.startChild({
op: 'http.request',
description: `GET /users/${userId}`,
});
try {
const response = await fetch(`${API_URL}/users/${userId}`);
const data = await response.json();
span.setStatus('ok');
return data;
} catch (error) {
span.setStatus('internal_error');
throw error;
} finally {
span.finish();
transaction.finish();
}
}
// Measure component rendering
function ExpensiveComponent() {
useEffect(() => {
const span = Sentry.startSpan({
name: 'ExpensiveComponent render',
op: 'ui.render',
});
// Component mounted
return () => {
span?.finish();
};
}, []);
return <View>{/* ... */}</View>;
}
App Start Performance
// Track app startup time
import * as Sentry from '@sentry/react-native';
// In your App component
function App() {
const [appIsReady, setAppIsReady] = useState(false);
const startTime = useRef(Date.now());
useEffect(() => {
async function prepare() {
try {
// Load resources
await loadFonts();
await initializeServices();
// Track startup time
const startupTime = Date.now() - startTime.current;
Sentry.setMeasurement('app_start_time', startupTime, 'millisecond');
} finally {
setAppIsReady(true);
}
}
prepare();
}, []);
// ...
}
Memory and CPU Monitoring
// Use react-native-performance for detailed metrics
import { PerformanceObserver, performance } from 'react-native-performance';
// Observe performance entries
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(`${entry.name}: ${entry.duration}ms`);
// Send to analytics
trackEvent('performance_metric', {
name: entry.name,
duration: entry.duration,
entryType: entry.entryType,
});
});
});
observer.observe({ entryTypes: ['measure', 'mark'] });
// Create marks and measures
performance.mark('fetchStart');
await fetchData();
performance.mark('fetchEnd');
performance.measure('API Fetch', 'fetchStart', 'fetchEnd');
Dashboards and Alerts
Set up dashboards to monitor app health and alerts for critical issues.
Sentry Dashboard Setup
// Key Sentry features to configure:
// 1. Issue Alerts
// - Alert when error count exceeds threshold
// - Alert on new error types
// - Alert on regression (resolved issue reappears)
// 2. Performance Alerts
// - Alert when p95 latency exceeds threshold
// - Alert when throughput drops
// - Alert when error rate increases
// 3. Custom Dashboards
// - Crash-free sessions over time
// - Error count by release
// - Performance metrics by screen
// - User impact analysis
// 4. Integrations
// - Slack notifications
// - Email alerts
// - PagerDuty for critical issues
// - Jira for issue tracking
Create Alerts in Sentry
// Example alert configurations:
// Alert 1: High Error Rate
// Condition: Error count > 100 in 1 hour
// Action: Send Slack notification
// Filter: Environment = production
// Alert 2: New Issue Type
// Condition: First seen issue
// Action: Email team
// Filter: Level = error or fatal
// Alert 3: Performance Degradation
// Condition: p95(transaction.duration) > 3s
// Action: Slack + PagerDuty
// Filter: Transaction = checkout
// Alert 4: Crash Spike
// Condition: Session crash rate > 1%
// Action: Immediate notification
// Filter: Release = latest
Amplitude Dashboards
// Key dashboards to create:
// 1. User Engagement Dashboard
// - Daily/Weekly/Monthly Active Users
// - Session frequency
// - Session duration
// - Feature adoption rates
// 2. Conversion Funnel
// - Signup funnel (view → start → complete)
// - Purchase funnel (browse → cart → checkout → complete)
// - Onboarding completion rate
// 3. Retention Dashboard
// - Day 1, 7, 30 retention curves
// - Cohort analysis
// - Churn indicators
// 4. Feature Usage
// - Top features by usage
// - Feature adoption over time
// - Power user identification
Monitoring Checklist
✅ Daily Monitoring
- Check crash-free rate (target: >99%)
- Review new errors in Sentry
- Check DAU trends
- Monitor key conversion funnels
📊 Weekly Review
- Analyze retention cohorts
- Review performance trends
- Check feature adoption
- Plan bug fixes based on impact
📈 Monthly Analysis
- User growth trends
- Revenue metrics
- Feature usage patterns
- Performance optimization opportunities
Hands-On Exercises
Exercise 1: Set Up Complete Monitoring
Configure Sentry with proper error handling and release tracking.
Show Solution
// services/monitoring.ts
import * as Sentry from '@sentry/react-native';
import Constants from 'expo-constants';
const SENTRY_DSN = process.env.EXPO_PUBLIC_SENTRY_DSN || '';
export function initMonitoring() {
if (__DEV__ || !SENTRY_DSN) {
console.log('Sentry disabled in development');
return;
}
const release = `${Constants.expoConfig?.slug}@${Constants.expoConfig?.version}`;
const dist = Constants.expoConfig?.ios?.buildNumber ||
Constants.expoConfig?.android?.versionCode?.toString() ||
'1';
Sentry.init({
dsn: SENTRY_DSN,
release,
dist,
environment: 'production',
// Error sampling
sampleRate: 1.0,
// Performance sampling
tracesSampleRate: 0.2,
// Enable native crash reporting
enableNative: true,
// Attach screenshots
attachScreenshot: true,
// Before send hook
beforeSend(event, hint) {
// Filter out non-critical errors
if (event.level === 'info') {
return null;
}
return event;
},
});
}
export function setMonitoringUser(user: { id: string; email?: string }) {
Sentry.setUser({
id: user.id,
email: user.email,
});
}
export function clearMonitoringUser() {
Sentry.setUser(null);
}
export function captureError(error: Error, context?: Record<string, any>) {
Sentry.captureException(error, {
extra: context,
});
}
export function addBreadcrumb(
category: string,
message: string,
data?: Record<string, any>
) {
Sentry.addBreadcrumb({
category,
message,
data,
level: 'info',
});
}
// App.tsx
import { initMonitoring } from './services/monitoring';
export default function App() {
useEffect(() => {
initMonitoring();
}, []);
return (
<Sentry.ErrorBoundary fallback={<ErrorFallback />}>
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
</Sentry.ErrorBoundary>
);
}
Exercise 2: Implement Analytics Service
Create a reusable analytics service with proper event tracking.
Show Solution
// services/analytics.ts
import * as Amplitude from '@amplitude/analytics-react-native';
const AMPLITUDE_KEY = process.env.EXPO_PUBLIC_AMPLITUDE_KEY || '';
class AnalyticsService {
private initialized = false;
async init() {
if (__DEV__ || !AMPLITUDE_KEY || this.initialized) {
return;
}
await Amplitude.init(AMPLITUDE_KEY);
this.initialized = true;
}
identify(userId: string, properties?: Record<string, any>) {
if (!this.initialized) return;
Amplitude.setUserId(userId);
if (properties) {
const identify = new Amplitude.Identify();
Object.entries(properties).forEach(([key, value]) => {
identify.set(key, value);
});
Amplitude.identify(identify);
}
}
reset() {
if (!this.initialized) return;
Amplitude.reset();
}
track(event: string, properties?: Record<string, any>) {
if (!this.initialized) {
if (__DEV__) {
console.log('[Analytics]', event, properties);
}
return;
}
Amplitude.track(event, properties);
}
trackScreen(screenName: string) {
this.track('screen_viewed', { screen_name: screenName });
}
// Common events
trackSignup(method: string) {
this.track('signup_completed', { method });
}
trackLogin(method: string) {
this.track('login_completed', { method });
}
trackPurchase(orderId: string, revenue: number, currency = 'USD') {
this.track('purchase_completed', {
order_id: orderId,
revenue,
currency,
});
}
trackFeatureUsed(feature: string, metadata?: Record<string, any>) {
this.track('feature_used', {
feature_name: feature,
...metadata,
});
}
}
export const analytics = new AnalyticsService();
// hooks/useAnalytics.ts
import { useCallback } from 'react';
import { analytics } from '../services/analytics';
export function useAnalytics() {
const track = useCallback(
(event: string, properties?: Record<string, any>) => {
analytics.track(event, properties);
},
[]
);
const trackScreen = useCallback((name: string) => {
analytics.trackScreen(name);
}, []);
return { track, trackScreen, analytics };
}
Exercise 3: Create Error Boundary with Reporting
Build an error boundary that reports to Sentry and allows recovery.
Show Solution
// components/AppErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Image } from 'react-native';
import * as Sentry from '@sentry/react-native';
import * as Updates from 'expo-updates';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
eventId: string | null;
}
class AppErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
eventId: null,
};
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
const eventId = Sentry.captureException(error, {
extra: {
componentStack: errorInfo.componentStack,
},
});
this.setState({ eventId });
}
handleRestart = async () => {
try {
await Updates.reloadAsync();
} catch {
// Fallback for development
this.setState({ hasError: false, error: null, eventId: null });
}
};
handleReport = () => {
if (this.state.eventId) {
Sentry.showReportDialog({ eventId: this.state.eventId });
}
};
render() {
if (this.state.hasError) {
return (
<View style={styles.container}>
<Image
source={require('../assets/error-illustration.png')}
style={styles.image}
/>
<Text style={styles.title}>Oops! Something went wrong</Text>
<Text style={styles.message}>
We've been notified and are working to fix this issue.
</Text>
<TouchableOpacity style={styles.button} onPress={this.handleRestart}>
<Text style={styles.buttonText}>Restart App</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.linkButton}
onPress={this.handleReport}
>
<Text style={styles.linkText}>Send Feedback</Text>
</TouchableOpacity>
{__DEV__ && (
<Text style={styles.errorDetail}>
{this.state.error?.message}
</Text>
)}
</View>
);
}
return this.props.children;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 24,
backgroundColor: '#fff',
},
image: {
width: 200,
height: 200,
marginBottom: 24,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 12,
textAlign: 'center',
},
message: {
fontSize: 16,
color: '#666',
textAlign: 'center',
marginBottom: 32,
paddingHorizontal: 24,
},
button: {
backgroundColor: '#007AFF',
paddingHorizontal: 32,
paddingVertical: 16,
borderRadius: 8,
marginBottom: 16,
},
buttonText: {
color: '#fff',
fontSize: 18,
fontWeight: '600',
},
linkButton: {
padding: 12,
},
linkText: {
color: '#007AFF',
fontSize: 16,
},
errorDetail: {
marginTop: 24,
padding: 12,
backgroundColor: '#f5f5f5',
borderRadius: 8,
fontFamily: 'monospace',
fontSize: 12,
},
});
export default AppErrorBoundary;
Summary
🎯 Key Takeaways
- Sentry: Best-in-class crash reporting with excellent Expo support
- Error boundaries: Catch errors gracefully and prevent full app crashes
- Analytics: Track user behavior with consistent event naming
- Performance: Monitor app start, API calls, and navigation timing
- User context: Attach user info to errors for better debugging
- Alerts: Set up notifications for critical issues
- Dashboards: Monitor crash-free rate, retention, and key metrics
Monitoring Quick Reference
// Sentry
import * as Sentry from '@sentry/react-native';
Sentry.init({ dsn: '...', tracesSampleRate: 0.2 });
Sentry.captureException(error);
Sentry.captureMessage('Info message');
Sentry.setUser({ id, email });
Sentry.addBreadcrumb({ category, message });
// Analytics (Amplitude)
import * as Amplitude from '@amplitude/analytics-react-native';
Amplitude.init(API_KEY);
Amplitude.track('event_name', { property: 'value' });
Amplitude.setUserId(userId);
Amplitude.identify(new Amplitude.Identify().set('prop', 'value'));
// Error Boundary
<Sentry.ErrorBoundary fallback={ErrorComponent}>
<App />
</Sentry.ErrorBoundary>
In the next lesson, we'll set up CI/CD pipelines to automate building, testing, and deploying your app.