588 lines
14 KiB
Markdown
588 lines
14 KiB
Markdown
# Error Handling & Logging Standards - Maternal Organization App
|
|
|
|
## Error Philosophy
|
|
|
|
### Core Principles
|
|
- **Parent-Friendly Messages**: Never show technical jargon to users
|
|
- **Graceful Degradation**: App remains usable even with errors
|
|
- **Recovery Guidance**: Always suggest next steps
|
|
- **Preserve User Work**: Never lose unsaved data due to errors
|
|
- **Privacy First**: Never log sensitive data (PII, health info)
|
|
|
|
---
|
|
|
|
## Error Code Hierarchy
|
|
|
|
### Error Code Structure
|
|
Format: `[CATEGORY]_[SPECIFIC_ERROR]`
|
|
|
|
### Categories
|
|
```typescript
|
|
enum ErrorCategory {
|
|
AUTH = 'AUTH', // Authentication/Authorization
|
|
VALIDATION = 'VAL', // Input validation
|
|
SYNC = 'SYNC', // Synchronization issues
|
|
NETWORK = 'NET', // Network/connectivity
|
|
DATA = 'DATA', // Database/storage
|
|
AI = 'AI', // AI service errors
|
|
LIMIT = 'LIMIT', // Rate limiting/quotas
|
|
PAYMENT = 'PAY', // Subscription/payment
|
|
SYSTEM = 'SYS', // System/internal errors
|
|
COMPLIANCE = 'COMP' // COPPA/GDPR compliance
|
|
}
|
|
```
|
|
|
|
### Complete Error Code Registry
|
|
```typescript
|
|
export const ErrorCodes = {
|
|
// Authentication
|
|
AUTH_INVALID_CREDENTIALS: 'AUTH_001',
|
|
AUTH_TOKEN_EXPIRED: 'AUTH_002',
|
|
AUTH_TOKEN_INVALID: 'AUTH_003',
|
|
AUTH_DEVICE_NOT_TRUSTED: 'AUTH_004',
|
|
AUTH_MFA_REQUIRED: 'AUTH_005',
|
|
AUTH_ACCOUNT_LOCKED: 'AUTH_006',
|
|
|
|
// Validation
|
|
VAL_REQUIRED_FIELD: 'VAL_001',
|
|
VAL_INVALID_EMAIL: 'VAL_002',
|
|
VAL_WEAK_PASSWORD: 'VAL_003',
|
|
VAL_INVALID_DATE: 'VAL_004',
|
|
VAL_FUTURE_BIRTHDATE: 'VAL_005',
|
|
VAL_INVALID_AMOUNT: 'VAL_006',
|
|
|
|
// Sync
|
|
SYNC_CONFLICT: 'SYNC_001',
|
|
SYNC_OFFLINE_QUEUE_FULL: 'SYNC_002',
|
|
SYNC_VERSION_MISMATCH: 'SYNC_003',
|
|
SYNC_FAMILY_UPDATE_FAILED: 'SYNC_004',
|
|
|
|
// Network
|
|
NET_OFFLINE: 'NET_001',
|
|
NET_TIMEOUT: 'NET_002',
|
|
NET_SERVER_ERROR: 'NET_003',
|
|
NET_SLOW_CONNECTION: 'NET_004',
|
|
|
|
// AI
|
|
AI_SERVICE_UNAVAILABLE: 'AI_001',
|
|
AI_QUOTA_EXCEEDED: 'AI_002',
|
|
AI_INAPPROPRIATE_REQUEST: 'AI_003',
|
|
AI_CONTEXT_TOO_LARGE: 'AI_004',
|
|
|
|
// Limits
|
|
LIMIT_RATE_EXCEEDED: 'LIMIT_001',
|
|
LIMIT_CHILDREN_EXCEEDED: 'LIMIT_002',
|
|
LIMIT_FAMILY_SIZE_EXCEEDED: 'LIMIT_003',
|
|
LIMIT_STORAGE_EXCEEDED: 'LIMIT_004',
|
|
|
|
// Compliance
|
|
COMP_PARENTAL_CONSENT_REQUIRED: 'COMP_001',
|
|
COMP_AGE_VERIFICATION_FAILED: 'COMP_002',
|
|
COMP_DATA_RETENTION_EXPIRED: 'COMP_003'
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## User-Facing Error Messages
|
|
|
|
### Message Structure
|
|
```typescript
|
|
interface UserErrorMessage {
|
|
title: string; // Brief, clear title
|
|
message: string; // Detailed explanation
|
|
action?: string; // What user should do
|
|
retryable: boolean; // Can user retry?
|
|
severity: 'info' | 'warning' | 'error';
|
|
}
|
|
```
|
|
|
|
### Localized Error Messages
|
|
```typescript
|
|
// errors/locales/en-US.json
|
|
{
|
|
"AUTH_001": {
|
|
"title": "Sign in failed",
|
|
"message": "The email or password you entered doesn't match our records.",
|
|
"action": "Please check your credentials and try again.",
|
|
"retryable": true
|
|
},
|
|
"SYNC_001": {
|
|
"title": "Update conflict",
|
|
"message": "This activity was updated by another family member.",
|
|
"action": "We've merged the changes. Please review.",
|
|
"retryable": false
|
|
},
|
|
"AI_002": {
|
|
"title": "AI assistant limit reached",
|
|
"message": "You've used all 10 free AI questions today.",
|
|
"action": "Upgrade to Premium for unlimited questions.",
|
|
"retryable": false
|
|
},
|
|
"NET_001": {
|
|
"title": "You're offline",
|
|
"message": "Don't worry! Your activities are saved locally.",
|
|
"action": "They'll sync when you're back online.",
|
|
"retryable": true
|
|
}
|
|
}
|
|
```
|
|
|
|
### Localization for Other Languages
|
|
```typescript
|
|
// errors/locales/es-ES.json
|
|
{
|
|
"AUTH_001": {
|
|
"title": "Error al iniciar sesión",
|
|
"message": "El correo o contraseña no coinciden con nuestros registros.",
|
|
"action": "Por favor verifica tus credenciales e intenta nuevamente.",
|
|
"retryable": true
|
|
}
|
|
}
|
|
|
|
// errors/locales/fr-FR.json
|
|
{
|
|
"AUTH_001": {
|
|
"title": "Échec de connexion",
|
|
"message": "L'email ou le mot de passe ne correspond pas.",
|
|
"action": "Veuillez vérifier vos identifiants et réessayer.",
|
|
"retryable": true
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Logging Strategy
|
|
|
|
### Log Levels
|
|
```typescript
|
|
enum LogLevel {
|
|
DEBUG = 0, // Development only
|
|
INFO = 1, // General information
|
|
WARN = 2, // Warning conditions
|
|
ERROR = 3, // Error conditions
|
|
FATAL = 4 // System is unusable
|
|
}
|
|
|
|
// Environment-based levels
|
|
const LOG_LEVELS = {
|
|
development: LogLevel.DEBUG,
|
|
staging: LogLevel.INFO,
|
|
production: LogLevel.WARN
|
|
};
|
|
```
|
|
|
|
### Structured Logging Format
|
|
```typescript
|
|
interface LogEntry {
|
|
timestamp: string;
|
|
level: LogLevel;
|
|
service: string;
|
|
userId?: string; // Hashed for privacy
|
|
familyId?: string; // For family-related issues
|
|
deviceId?: string; // Device fingerprint
|
|
errorCode?: string;
|
|
message: string;
|
|
context?: Record<string, any>;
|
|
stack?: string;
|
|
duration?: number; // For performance logs
|
|
correlationId: string; // Trace requests
|
|
}
|
|
|
|
// Example log entry
|
|
{
|
|
"timestamp": "2024-01-10T14:30:00.123Z",
|
|
"level": "ERROR",
|
|
"service": "ActivityService",
|
|
"userId": "hash_2n4k8m9p",
|
|
"errorCode": "SYNC_001",
|
|
"message": "Sync conflict detected",
|
|
"context": {
|
|
"activityId": "act_123",
|
|
"conflictType": "simultaneous_edit"
|
|
},
|
|
"correlationId": "req_8k3m9n2p"
|
|
}
|
|
```
|
|
|
|
### Logger Implementation
|
|
```typescript
|
|
// logger/index.ts
|
|
import winston from 'winston';
|
|
|
|
const logger = winston.createLogger({
|
|
level: process.env.LOG_LEVEL || 'info',
|
|
format: winston.format.combine(
|
|
winston.format.timestamp(),
|
|
winston.format.errors({ stack: true }),
|
|
winston.format.json()
|
|
),
|
|
defaultMeta: {
|
|
service: process.env.SERVICE_NAME,
|
|
version: process.env.APP_VERSION
|
|
},
|
|
transports: [
|
|
new winston.transports.Console({
|
|
format: winston.format.simple(),
|
|
silent: process.env.NODE_ENV === 'test'
|
|
}),
|
|
new winston.transports.File({
|
|
filename: 'error.log',
|
|
level: 'error'
|
|
})
|
|
]
|
|
});
|
|
|
|
// Privacy wrapper
|
|
export const log = {
|
|
info: (message: string, meta?: any) => {
|
|
logger.info(message, sanitizePII(meta));
|
|
},
|
|
error: (message: string, error: Error, meta?: any) => {
|
|
logger.error(message, {
|
|
...sanitizePII(meta),
|
|
errorMessage: error.message,
|
|
stack: error.stack
|
|
});
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Sentry Configuration
|
|
|
|
### Sentry Setup
|
|
```typescript
|
|
// sentry.config.ts
|
|
import * as Sentry from '@sentry/node';
|
|
|
|
Sentry.init({
|
|
dsn: process.env.SENTRY_DSN,
|
|
environment: process.env.NODE_ENV,
|
|
integrations: [
|
|
new Sentry.Integrations.Http({ tracing: true }),
|
|
new Sentry.Integrations.Express({ app }),
|
|
],
|
|
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
|
|
beforeSend(event, hint) {
|
|
// Remove sensitive data
|
|
if (event.request) {
|
|
delete event.request.cookies;
|
|
delete event.request.headers?.authorization;
|
|
}
|
|
|
|
// Filter out user-caused errors
|
|
if (event.exception?.values?.[0]?.type === 'ValidationError') {
|
|
return null; // Don't send to Sentry
|
|
}
|
|
|
|
return sanitizeEvent(event);
|
|
},
|
|
ignoreErrors: [
|
|
'NetworkError',
|
|
'Request aborted',
|
|
'Non-Error promise rejection'
|
|
]
|
|
});
|
|
```
|
|
|
|
### React Native Sentry
|
|
```typescript
|
|
// Mobile sentry config
|
|
import * as Sentry from '@sentry/react-native';
|
|
|
|
Sentry.init({
|
|
dsn: process.env.SENTRY_DSN,
|
|
debug: __DEV__,
|
|
environment: __DEV__ ? 'development' : 'production',
|
|
attachScreenshot: true,
|
|
attachViewHierarchy: true,
|
|
beforeSend: (event) => {
|
|
// Don't send events in dev
|
|
if (__DEV__) return null;
|
|
|
|
// Remove sensitive context
|
|
delete event.user?.email;
|
|
delete event.contexts?.app?.device_name;
|
|
|
|
return event;
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Error Recovery Procedures
|
|
|
|
### Automatic Recovery
|
|
```typescript
|
|
// services/errorRecovery.ts
|
|
class ErrorRecoveryService {
|
|
async handleError(error: AppError): Promise<RecoveryAction> {
|
|
switch (error.code) {
|
|
case 'NET_OFFLINE':
|
|
return this.queueForOfflineSync(error.context);
|
|
|
|
case 'AUTH_TOKEN_EXPIRED':
|
|
return this.refreshToken();
|
|
|
|
case 'SYNC_CONFLICT':
|
|
return this.resolveConflict(error.context);
|
|
|
|
case 'AI_SERVICE_UNAVAILABLE':
|
|
return this.fallbackToOfflineAI();
|
|
|
|
default:
|
|
return this.defaultRecovery(error);
|
|
}
|
|
}
|
|
|
|
private async queueForOfflineSync(context: any) {
|
|
await offlineQueue.add(context);
|
|
return {
|
|
recovered: true,
|
|
message: 'Saved locally, will sync when online'
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### User-Guided Recovery
|
|
```typescript
|
|
// components/ErrorBoundary.tsx
|
|
class ErrorBoundary extends React.Component {
|
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
// Log to Sentry
|
|
Sentry.captureException(error, { contexts: { react: errorInfo } });
|
|
|
|
// Show recovery UI
|
|
this.setState({
|
|
hasError: true,
|
|
error,
|
|
recovery: this.getRecoveryOptions(error)
|
|
});
|
|
}
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
return (
|
|
<ErrorRecoveryScreen
|
|
title={i18n.t('error.something_went_wrong')}
|
|
message={i18n.t('error.we_are_sorry')}
|
|
actions={[
|
|
{ label: 'Try Again', onPress: this.retry },
|
|
{ label: 'Go to Dashboard', onPress: this.reset },
|
|
{ label: 'Contact Support', onPress: this.support }
|
|
]}
|
|
/>
|
|
);
|
|
}
|
|
return this.props.children;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Audit Logging
|
|
|
|
### COPPA/GDPR Compliance Logging
|
|
```typescript
|
|
interface AuditLog {
|
|
timestamp: string;
|
|
userId: string;
|
|
action: AuditAction;
|
|
entityType: string;
|
|
entityId: string;
|
|
changes?: Record<string, any>;
|
|
ipAddress: string;
|
|
userAgent: string;
|
|
result: 'success' | 'failure';
|
|
reason?: string;
|
|
}
|
|
|
|
enum AuditAction {
|
|
// Data access
|
|
VIEW_CHILD_DATA = 'VIEW_CHILD_DATA',
|
|
EXPORT_DATA = 'EXPORT_DATA',
|
|
|
|
// Data modification
|
|
CREATE_CHILD_PROFILE = 'CREATE_CHILD_PROFILE',
|
|
UPDATE_CHILD_DATA = 'UPDATE_CHILD_DATA',
|
|
DELETE_CHILD_DATA = 'DELETE_CHILD_DATA',
|
|
|
|
// Consent
|
|
GRANT_CONSENT = 'GRANT_CONSENT',
|
|
REVOKE_CONSENT = 'REVOKE_CONSENT',
|
|
|
|
// Account
|
|
DELETE_ACCOUNT = 'DELETE_ACCOUNT',
|
|
CHANGE_PASSWORD = 'CHANGE_PASSWORD'
|
|
}
|
|
```
|
|
|
|
### Audit Log Implementation
|
|
```sql
|
|
-- Audit log table with partitioning
|
|
CREATE TABLE audit_logs (
|
|
id BIGSERIAL,
|
|
timestamp TIMESTAMP NOT NULL,
|
|
user_id VARCHAR(20),
|
|
action VARCHAR(50) NOT NULL,
|
|
entity_type VARCHAR(50),
|
|
entity_id VARCHAR(20),
|
|
changes JSONB,
|
|
ip_address INET,
|
|
user_agent TEXT,
|
|
result VARCHAR(20),
|
|
PRIMARY KEY (id, timestamp)
|
|
) PARTITION BY RANGE (timestamp);
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Monitoring
|
|
|
|
### Response Time Logging
|
|
```typescript
|
|
// middleware/performanceLogger.ts
|
|
export const performanceLogger = (req: Request, res: Response, next: Next) => {
|
|
const start = Date.now();
|
|
|
|
res.on('finish', () => {
|
|
const duration = Date.now() - start;
|
|
|
|
if (duration > 1000) { // Log slow requests
|
|
logger.warn('Slow request detected', {
|
|
method: req.method,
|
|
path: req.path,
|
|
duration,
|
|
statusCode: res.statusCode
|
|
});
|
|
|
|
// Send to monitoring
|
|
metrics.histogram('request.duration', duration, {
|
|
path: req.path,
|
|
method: req.method
|
|
});
|
|
}
|
|
});
|
|
|
|
next();
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Alert Configuration
|
|
|
|
### Critical Alerts
|
|
```yaml
|
|
# alerts/critical.yml
|
|
alerts:
|
|
- name: high_error_rate
|
|
condition: error_rate > 5%
|
|
duration: 5m
|
|
action: page_on_call
|
|
|
|
- name: auth_failures_spike
|
|
condition: auth_failures > 100
|
|
duration: 1m
|
|
action: security_team_alert
|
|
|
|
- name: ai_service_down
|
|
condition: ai_availability < 99%
|
|
duration: 2m
|
|
action: notify_team
|
|
|
|
- name: database_connection_pool_exhausted
|
|
condition: available_connections < 5
|
|
action: scale_database
|
|
```
|
|
|
|
---
|
|
|
|
## Client-Side Error Tracking
|
|
|
|
### React Native Global Handler
|
|
```typescript
|
|
// errorHandler.ts
|
|
import { setJSExceptionHandler } from 'react-native-exception-handler';
|
|
|
|
setJSExceptionHandler((error, isFatal) => {
|
|
if (isFatal) {
|
|
logger.fatal('Fatal JS error', { error });
|
|
Alert.alert(
|
|
'Unexpected error occurred',
|
|
'The app needs to restart. Your data has been saved.',
|
|
[{ text: 'Restart', onPress: () => RNRestart.Restart() }]
|
|
);
|
|
} else {
|
|
logger.error('Non-fatal JS error', { error });
|
|
// Show toast notification
|
|
Toast.show({
|
|
type: 'error',
|
|
text1: 'Something went wrong',
|
|
text2: 'Please try again'
|
|
});
|
|
}
|
|
}, true); // Allow in production
|
|
```
|
|
|
|
---
|
|
|
|
## Error Analytics Dashboard
|
|
|
|
### Key Metrics
|
|
```typescript
|
|
interface ErrorMetrics {
|
|
errorRate: number; // Errors per 1000 requests
|
|
errorTypes: Record<string, number>; // Count by error code
|
|
affectedUsers: number; // Unique users with errors
|
|
recoveryRate: number; // % of errors recovered
|
|
meanTimeToRecovery: number; // Seconds
|
|
criticalErrors: ErrorEvent[]; // P0 errors
|
|
}
|
|
|
|
// Monitoring queries
|
|
const getErrorMetrics = async (timeRange: TimeRange): Promise<ErrorMetrics> => {
|
|
const errors = await db.query(`
|
|
SELECT
|
|
COUNT(*) as total_errors,
|
|
COUNT(DISTINCT user_id) as affected_users,
|
|
AVG(recovery_time) as mttr,
|
|
error_code,
|
|
COUNT(*) as count
|
|
FROM error_logs
|
|
WHERE timestamp > $1
|
|
GROUP BY error_code
|
|
`, [timeRange.start]);
|
|
|
|
return processMetrics(errors);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Development Error Tools
|
|
|
|
### Debug Mode Enhancements
|
|
```typescript
|
|
// Development only error overlay
|
|
if (__DEV__) {
|
|
// Show detailed error information
|
|
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
console.group('🔴 Error Details');
|
|
console.error('Error:', error.message);
|
|
console.error('Stack:', error.stack);
|
|
console.error('Component Stack:', error.componentStack);
|
|
console.error('Fatal:', isFatal);
|
|
console.groupEnd();
|
|
});
|
|
|
|
// Network request inspector
|
|
global.XMLHttpRequest = decorateXHR(global.XMLHttpRequest);
|
|
}
|
|
``` |