Skip to main content

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.