react-native-sop-docs/11-post-release-maintenance/crash-monitoring.md

14 KiB

Crash Monitoring

Bugsnag Setup

Installation

yarn add @bugsnag/react-native
yarn add @bugsnag/react-native-cli

Configuration

bugsnag.config.js:

import Bugsnag from '@bugsnag/react-native';
import Config from 'react-native-config';

Bugsnag.start({
  apiKey: Config.BUGSNAG_API_KEY,
  appVersion: Config.APP_VERSION,
  releaseStage: Config.APP_ENV,
  enabledReleaseStages: ['production', 'staging'],
  autoDetectErrors: true,
  autoCaptureSessions: true,
  enabledErrorTypes: {
    unhandledExceptions: true,
    unhandledRejections: true,
    nativeCrashes: true,
  },
  onError: (event) => {
    // Add custom metadata
    event.addMetadata('app', {
      buildNumber: Config.BUILD_NUMBER,
      environment: Config.APP_ENV,
    });
    
    // Filter sensitive data
    event.request.url = event.request.url?.replace(/token=[^&]+/, 'token=***');
    
    return true;
  },
  onSession: (session) => {
    // Add user context
    session.user = {
      id: 'user-id',
      name: 'User Name',
      email: 'user@example.com',
    };
    
    return true;
  },
});

export default Bugsnag;

Integration

App.tsx:

import React from 'react';
import Bugsnag from './src/config/bugsnag.config';
import {AppNavigator} from './src/navigation/AppNavigator';

const App: React.FC = () => {
  React.useEffect(() => {
    // Set user context when available
    Bugsnag.setUser('user-123', 'john@example.com', 'John Doe');
    
    // Leave breadcrumb for app start
    Bugsnag.leaveBreadcrumb('App started');
  }, []);

  return <AppNavigator />;
};

export default App;

Firebase Crashlytics

Installation

yarn add @react-native-firebase/app
yarn add @react-native-firebase/crashlytics

Configuration

firebase.config.js:

import crashlytics from '@react-native-firebase/crashlytics';
import Config from 'react-native-config';

class CrashlyticsService {
  static initialize() {
    if (__DEV__) {
      // Disable crashlytics in development
      crashlytics().setCrashlyticsCollectionEnabled(false);
    } else {
      crashlytics().setCrashlyticsCollectionEnabled(true);
    }
  }

  static setUserId(userId: string) {
    crashlytics().setUserId(userId);
  }

  static setUserAttributes(attributes: Record<string, string>) {
    Object.entries(attributes).forEach(([key, value]) => {
      crashlytics().setAttribute(key, value);
    });
  }

  static logError(error: Error, context?: Record<string, any>) {
    if (context) {
      Object.entries(context).forEach(([key, value]) => {
        crashlytics().setAttribute(key, String(value));
      });
    }
    
    crashlytics().recordError(error);
  }

  static log(message: string) {
    crashlytics().log(message);
  }

  static crash() {
    crashlytics().crash();
  }
}

export default CrashlyticsService;

Error Boundary

React Error Boundary

// src/components/ErrorBoundary/ErrorBoundary.tsx
import React, {Component, ErrorInfo, ReactNode} from 'react';
import {View, Text, TouchableOpacity, StyleSheet} from 'react-native';
import Bugsnag from '@config/bugsnag.config';
import CrashlyticsService from '@config/firebase.config';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {hasError: false};
  }

  static getDerivedStateFromError(error: Error): State {
    return {hasError: true, error};
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('ErrorBoundary caught an error:', error, errorInfo);
    
    // Log to crash reporting services
    Bugsnag.notify(error, (event) => {
      event.addMetadata('errorBoundary', {
        componentStack: errorInfo.componentStack,
        errorBoundary: true,
      });
    });
    
    CrashlyticsService.logError(error, {
      componentStack: errorInfo.componentStack,
      errorBoundary: 'true',
    });
  }

  handleRetry = () => {
    this.setState({hasError: false, error: undefined});
  };

  render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback;
      }

      return (
        <View style={styles.container}>
          <Text style={styles.title}>Oops! Something went wrong</Text>
          <Text style={styles.message}>
            We're sorry for the inconvenience. The error has been reported and we're working on a fix.
          </Text>
          <TouchableOpacity style={styles.button} onPress={this.handleRetry}>
            <Text style={styles.buttonText}>Try Again</Text>
          </TouchableOpacity>
        </View>
      );
    }

    return this.props.children;
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 16,
    textAlign: 'center',
    color: '#333',
  },
  message: {
    fontSize: 16,
    textAlign: 'center',
    marginBottom: 24,
    color: '#666',
    lineHeight: 24,
  },
  button: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 24,
    paddingVertical: 12,
    borderRadius: 8,
  },
  buttonText: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: '600',
  },
});

Global Error Handler

Unhandled Promise Rejections

// src/utils/errorHandler.ts
import Bugsnag from '@config/bugsnag.config';
import CrashlyticsService from '@config/firebase.config';

export class GlobalErrorHandler {
  static initialize() {
    // Handle unhandled promise rejections
    const originalHandler = global.Promise.prototype.catch;
    global.Promise.prototype.catch = function(onRejected) {
      return originalHandler.call(this, (error) => {
        GlobalErrorHandler.handleError(error, 'unhandledRejection');
        if (onRejected) {
          return onRejected(error);
        }
        throw error;
      });
    };

    // Handle uncaught exceptions
    if (global.ErrorUtils) {
      const originalGlobalHandler = global.ErrorUtils.getGlobalHandler();
      global.ErrorUtils.setGlobalHandler((error, isFatal) => {
        GlobalErrorHandler.handleError(error, isFatal ? 'fatal' : 'nonfatal');
        originalGlobalHandler(error, isFatal);
      });
    }
  }

  static handleError(error: Error, type: string) {
    console.error(`${type} error:`, error);

    // Log to crash reporting services
    Bugsnag.notify(error, (event) => {
      event.addMetadata('error', {
        type,
        timestamp: new Date().toISOString(),
      });
    });

    CrashlyticsService.logError(error, {
      errorType: type,
      timestamp: new Date().toISOString(),
    });
  }

  static logError(error: Error, context?: Record<string, any>) {
    this.handleError(error, 'manual');
    
    if (context) {
      Bugsnag.addMetadata('context', context);
      CrashlyticsService.setUserAttributes(
        Object.fromEntries(
          Object.entries(context).map(([k, v]) => [k, String(v)])
        )
      );
    }
  }
}

Network Error Monitoring

API Error Tracking

// src/services/errorTrackingAPI.ts
import {AxiosError, AxiosResponse} from 'axios';
import Bugsnag from '@config/bugsnag.config';
import CrashlyticsService from '@config/firebase.config';

export class APIErrorTracker {
  static trackRequest(config: any) {
    Bugsnag.leaveBreadcrumb(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
    CrashlyticsService.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
  }

  static trackResponse(response: AxiosResponse) {
    const {status, config} = response;
    Bugsnag.leaveBreadcrumb(`API Response: ${status} ${config.url}`);
    CrashlyticsService.log(`API Response: ${status} ${config.url}`);
  }

  static trackError(error: AxiosError) {
    const {response, config, message} = error;
    
    const errorData = {
      url: config?.url,
      method: config?.method,
      status: response?.status,
      statusText: response?.statusText,
      message,
      responseData: response?.data,
    };

    // Log to crash reporting
    Bugsnag.notify(error, (event) => {
      event.addMetadata('apiError', errorData);
      event.severity = response?.status && response.status >= 500 ? 'error' : 'warning';
    });

    CrashlyticsService.logError(error, {
      apiUrl: config?.url || 'unknown',
      apiMethod: config?.method || 'unknown',
      apiStatus: String(response?.status || 0),
    });

    console.error('API Error:', errorData);
  }
}

Performance Monitoring

Performance Metrics

// src/utils/performanceMonitor.ts
import Bugsnag from '@config/bugsnag.config';
import CrashlyticsService from '@config/firebase.config';

export class PerformanceMonitor {
  private static metrics: Map<string, number> = new Map();

  static startTiming(label: string) {
    this.metrics.set(label, Date.now());
    CrashlyticsService.log(`Performance: Started ${label}`);
  }

  static endTiming(label: string): number {
    const startTime = this.metrics.get(label);
    if (!startTime) {
      console.warn(`No start time found for ${label}`);
      return 0;
    }

    const duration = Date.now() - startTime;
    this.metrics.delete(label);

    // Log performance metrics
    Bugsnag.leaveBreadcrumb(`Performance: ${label} took ${duration}ms`);
    CrashlyticsService.log(`Performance: ${label} took ${duration}ms`);

    // Alert on slow operations
    if (duration > 5000) {
      Bugsnag.notify(new Error(`Slow operation: ${label}`), (event) => {
        event.severity = 'warning';
        event.addMetadata('performance', {
          operation: label,
          duration,
          threshold: 5000,
        });
      });
    }

    return duration;
  }

  static measureAsync<T>(label: string, fn: () => Promise<T>): Promise<T> {
    this.startTiming(label);
    return fn().finally(() => {
      this.endTiming(label);
    });
  }
}

Custom Crash Reports

User Feedback Integration

// src/components/FeedbackModal/FeedbackModal.tsx
import React, {useState} from 'react';
import {Modal, View, Text, TextInput, TouchableOpacity, StyleSheet} from 'react-native';
import Bugsnag from '@config/bugsnag.config';

interface FeedbackModalProps {
  visible: boolean;
  onClose: () => void;
  error?: Error;
}

export const FeedbackModal: React.FC<FeedbackModalProps> = ({
  visible,
  onClose,
  error,
}) => {
  const [feedback, setFeedback] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = () => {
    if (error) {
      Bugsnag.notify(error, (event) => {
        event.addMetadata('userFeedback', {
          feedback,
          email,
          timestamp: new Date().toISOString(),
        });
      });
    } else {
      Bugsnag.leaveBreadcrumb('User feedback submitted', {
        feedback,
        email,
      });
    }

    setFeedback('');
    setEmail('');
    onClose();
  };

  return (
    <Modal visible={visible} transparent animationType="slide">
      <View style={styles.overlay}>
        <View style={styles.container}>
          <Text style={styles.title}>Help us improve</Text>
          <Text style={styles.subtitle}>
            Tell us what happened so we can fix the issue
          </Text>
          
          <TextInput
            style={styles.input}
            placeholder="Your email (optional)"
            value={email}
            onChangeText={setEmail}
            keyboardType="email-address"
          />
          
          <TextInput
            style={[styles.input, styles.textArea]}
            placeholder="Describe what you were doing when the error occurred"
            value={feedback}
            onChangeText={setFeedback}
            multiline
            numberOfLines={4}
          />
          
          <View style={styles.buttonContainer}>
            <TouchableOpacity style={styles.cancelButton} onPress={onClose}>
              <Text style={styles.cancelText}>Cancel</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.submitButton} onPress={handleSubmit}>
              <Text style={styles.submitText}>Send Feedback</Text>
            </TouchableOpacity>
          </View>
        </View>
      </View>
    </Modal>
  );
};

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  container: {
    backgroundColor: '#FFFFFF',
    borderRadius: 12,
    padding: 20,
    width: '90%',
    maxWidth: 400,
  },
  title: {
    fontSize: 18,
    fontWeight: '600',
    marginBottom: 8,
    textAlign: 'center',
  },
  subtitle: {
    fontSize: 14,
    color: '#666',
    textAlign: 'center',
    marginBottom: 20,
  },
  input: {
    borderWidth: 1,
    borderColor: '#E5E5E7',
    borderRadius: 8,
    paddingHorizontal: 12,
    paddingVertical: 10,
    marginBottom: 12,
    fontSize: 16,
  },
  textArea: {
    height: 80,
    textAlignVertical: 'top',
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginTop: 20,
  },
  cancelButton: {
    flex: 1,
    paddingVertical: 12,
    marginRight: 8,
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#E5E5E7',
  },
  submitButton: {
    flex: 1,
    paddingVertical: 12,
    marginLeft: 8,
    borderRadius: 8,
    backgroundColor: '#007AFF',
  },
  cancelText: {
    textAlign: 'center',
    fontSize: 16,
    color: '#666',
  },
  submitText: {
    textAlign: 'center',
    fontSize: 16,
    color: '#FFFFFF',
    fontWeight: '600',
  },
});

Crash Report Analysis

Automated Alerts

// src/utils/crashAlerts.ts
export class CrashAlerts {
  static setupAlerts() {
    // Configure Bugsnag alerts
    // This would typically be done in the Bugsnag dashboard
    
    // High-priority alerts:
    // - App crashes affecting > 1% of users
    // - New error types
    // - Errors in critical user flows (login, payment)
    
    // Medium-priority alerts:
    // - Performance degradation
    // - API errors > 5% error rate
    // - Memory warnings
  }

  static generateCrashReport(timeframe: string = '24h') {
    // This would integrate with Bugsnag API to generate reports
    return {
      totalCrashes: 0,
      uniqueErrors: 0,
      affectedUsers: 0,
      topErrors: [],
      performanceMetrics: {},
    };
  }
}