Add comprehensive .gitignore
This commit is contained in:
588
docs/maternal-app-error-logging.md
Normal file
588
docs/maternal-app-error-logging.md
Normal file
@@ -0,0 +1,588 @@
|
||||
# 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);
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user