612 lines
14 KiB
Markdown
612 lines
14 KiB
Markdown
# Crash Monitoring
|
|
|
|
## Bugsnag Setup
|
|
|
|
### Installation
|
|
|
|
```bash
|
|
yarn add @bugsnag/react-native
|
|
yarn add @bugsnag/react-native-cli
|
|
```
|
|
|
|
### Configuration
|
|
|
|
**bugsnag.config.js:**
|
|
```javascript
|
|
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:**
|
|
```typescript
|
|
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
|
|
|
|
```bash
|
|
yarn add @react-native-firebase/app
|
|
yarn add @react-native-firebase/crashlytics
|
|
```
|
|
|
|
### Configuration
|
|
|
|
**firebase.config.js:**
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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: {},
|
|
};
|
|
}
|
|
}
|
|
``` |