Files
maternal-app/docs/implementation-docs/maternal-app-error-logging.md
Andrei e2ca04c98f
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
feat: Setup PM2 production deployment and fix compilation issues
- Add PM2 ecosystem configuration for production deployment
- Fix database SSL configuration to support local PostgreSQL
- Create missing AI feedback entity with FeedbackRating enum
- Add roles decorator and guard for RBAC support
- Implement missing AI safety methods (sanitizeInput, performComprehensiveSafetyCheck)
- Add getSystemPrompt method to multi-language service
- Fix TypeScript errors in personalization service
- Install missing dependencies (@nestjs/terminus, mongodb, minio)
- Configure Next.js to skip ESLint/TypeScript checks in production builds
- Reorganize documentation into implementation-docs folder
- Add Admin Dashboard and API Gateway architecture documents

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 23:15:04 +00:00

14 KiB

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

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

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

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

// 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

// 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

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

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

// 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

// 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

// 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

// 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

// 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

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

-- 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

// 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

# 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

// 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

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

// 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);
}