feat: Implement comprehensive error handling and production deployment pipeline
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
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
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
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
## Error Handling System - Add centralized error handling utilities (errorHandler.ts) - Create reusable error components (ErrorMessage, ErrorToast) - Implement multilingual error support (preserves backend error messages in 5 languages) - Update 15+ forms and components with consistent error handling - Auth forms: login, register, forgot-password - Family management: family page, join family dialog - Child management: child dialog - All tracking forms: feeding, sleep, diaper, medicine, growth, activity ## Production Build Fixes - Fix backend TypeScript errors: InviteCode.uses → InviteCode.useCount (5 instances) - Remove non-existent savedFamily variable from registration response - Fix admin panel TypeScript errors: SimpleMDE toolbar type, PieChart label type ## User Experience Improvements - Auto-uppercase invite code and share code inputs - Visual feedback for case conversion with helper text - Improved form validation with error codes ## CI/CD Pipeline - Create comprehensive production deployment checklist (PRODUCTION_DEPLOYMENT_CHECKLIST.md) - Add automated pre-deployment check script (pre-deploy-check.sh) - Validates frontend, backend, and admin panel builds - Checks git status, branch, and sync state - Verifies environment files and migrations - Add quick start deployment guide (DEPLOYMENT_QUICK_START.md) - Add production deployment automation template (deploy-production.sh) ## Cleanup - Remove outdated push notifications documentation files - Remove outdated PWA implementation plan 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { tokenStorage } from '@/lib/utils/tokenStorage';
|
||||
import { logError } from '@/lib/utils/errorHandler';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020';
|
||||
|
||||
@@ -25,12 +26,15 @@ apiClient.interceptors.request.use(
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor to handle token refresh
|
||||
// Response interceptor to handle token refresh and error logging
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Log all API errors for debugging and error tracking
|
||||
logError(error, `API ${originalRequest?.method?.toUpperCase()} ${originalRequest?.url}`);
|
||||
|
||||
// Only handle token refresh on client side
|
||||
if (typeof window === 'undefined') {
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createContext, useContext, useEffect, useState, ReactNode } from 'react
|
||||
import { useRouter } from 'next/navigation';
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { tokenStorage } from '@/lib/utils/tokenStorage';
|
||||
import { handleError, formatErrorMessage } from '@/lib/utils/errorHandler';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
@@ -214,8 +215,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
router.push('/');
|
||||
} catch (error: any) {
|
||||
console.error('Login failed:', error);
|
||||
throw new Error(error.response?.data?.message || 'Login failed');
|
||||
const errorMessage = handleError(error, 'AuthContext.login');
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -285,8 +286,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
// Redirect to onboarding
|
||||
router.push('/onboarding');
|
||||
} catch (error: any) {
|
||||
console.error('Registration failed:', error);
|
||||
throw new Error(error.response?.data?.message || error.message || 'Registration failed');
|
||||
const errorMessage = handleError(error, 'AuthContext.register');
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
267
maternal-web/lib/utils/errorHandler.ts
Normal file
267
maternal-web/lib/utils/errorHandler.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* Error Handler Utility
|
||||
* Provides centralized error handling with multilingual support
|
||||
* Preserves backend error messages which are already localized in 5 languages:
|
||||
* - English (en)
|
||||
* - Spanish (es)
|
||||
* - French (fr)
|
||||
* - Portuguese (pt)
|
||||
* - Chinese (zh)
|
||||
*/
|
||||
|
||||
export interface ErrorResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
field?: string;
|
||||
details?: any;
|
||||
}
|
||||
|
||||
export interface ExtractedError {
|
||||
code: string;
|
||||
message: string;
|
||||
field?: string;
|
||||
details?: any;
|
||||
isBackendError: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error information from various error types
|
||||
* Prioritizes backend error messages to preserve multilingual support
|
||||
*/
|
||||
export function extractError(error: any): ExtractedError {
|
||||
// Default error
|
||||
const defaultError: ExtractedError = {
|
||||
code: 'UNKNOWN_ERROR',
|
||||
message: 'An unexpected error occurred. Please try again.',
|
||||
isBackendError: false,
|
||||
};
|
||||
|
||||
// If no error, return default
|
||||
if (!error) {
|
||||
return defaultError;
|
||||
}
|
||||
|
||||
// Axios error response with backend error
|
||||
if (error.response?.data?.error) {
|
||||
const backendError = error.response.data.error;
|
||||
return {
|
||||
code: backendError.code || 'BACKEND_ERROR',
|
||||
message: backendError.message || defaultError.message,
|
||||
field: backendError.field,
|
||||
details: backendError.details,
|
||||
isBackendError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Axios error response with message
|
||||
if (error.response?.data?.message) {
|
||||
return {
|
||||
code: error.response.data.code || 'BACKEND_ERROR',
|
||||
message: error.response.data.message,
|
||||
field: error.response.data.field,
|
||||
details: error.response.data.details,
|
||||
isBackendError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Network errors
|
||||
if (error.code === 'ERR_NETWORK' || error.message === 'Network Error') {
|
||||
return {
|
||||
code: 'NETWORK_ERROR',
|
||||
message: 'Unable to connect to the server. Please check your internet connection.',
|
||||
isBackendError: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Timeout errors
|
||||
if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
|
||||
return {
|
||||
code: 'TIMEOUT_ERROR',
|
||||
message: 'Request timed out. Please try again.',
|
||||
isBackendError: false,
|
||||
};
|
||||
}
|
||||
|
||||
// HTTP status code errors
|
||||
if (error.response?.status) {
|
||||
const status = error.response.status;
|
||||
|
||||
if (status === 401) {
|
||||
return {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Your session has expired. Please log in again.',
|
||||
isBackendError: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 403) {
|
||||
return {
|
||||
code: 'FORBIDDEN',
|
||||
message: 'You do not have permission to perform this action.',
|
||||
isBackendError: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 404) {
|
||||
return {
|
||||
code: 'NOT_FOUND',
|
||||
message: 'The requested resource was not found.',
|
||||
isBackendError: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 429) {
|
||||
return {
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
message: 'Too many requests. Please try again later.',
|
||||
isBackendError: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (status >= 500) {
|
||||
return {
|
||||
code: 'SERVER_ERROR',
|
||||
message: 'A server error occurred. Please try again later.',
|
||||
isBackendError: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Generic error with message
|
||||
if (error.message) {
|
||||
return {
|
||||
code: error.code || 'ERROR',
|
||||
message: error.message,
|
||||
isBackendError: false,
|
||||
};
|
||||
}
|
||||
|
||||
// String error
|
||||
if (typeof error === 'string') {
|
||||
return {
|
||||
code: 'ERROR',
|
||||
message: error,
|
||||
isBackendError: false,
|
||||
};
|
||||
}
|
||||
|
||||
return defaultError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format error message for display
|
||||
* Preserves backend messages (which are already localized)
|
||||
*/
|
||||
export function formatErrorMessage(error: any): string {
|
||||
const extracted = extractError(error);
|
||||
return extracted.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly error message based on error code
|
||||
* Only used for client-side errors; backend errors use their own messages
|
||||
*/
|
||||
export function getUserFriendlyMessage(errorCode: string): string {
|
||||
const messages: Record<string, string> = {
|
||||
// Network errors
|
||||
NETWORK_ERROR: 'Unable to connect to the server. Please check your internet connection.',
|
||||
TIMEOUT_ERROR: 'Request timed out. Please try again.',
|
||||
|
||||
// Authentication errors
|
||||
UNAUTHORIZED: 'Your session has expired. Please log in again.',
|
||||
FORBIDDEN: 'You do not have permission to perform this action.',
|
||||
|
||||
// HTTP errors
|
||||
NOT_FOUND: 'The requested resource was not found.',
|
||||
RATE_LIMIT_EXCEEDED: 'Too many requests. Please try again later.',
|
||||
SERVER_ERROR: 'A server error occurred. Please try again later.',
|
||||
|
||||
// Generic errors
|
||||
UNKNOWN_ERROR: 'An unexpected error occurred. Please try again.',
|
||||
ERROR: 'An error occurred. Please try again.',
|
||||
};
|
||||
|
||||
return messages[errorCode] || messages.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a specific type
|
||||
*/
|
||||
export function isErrorType(error: any, errorCode: string): boolean {
|
||||
const extracted = extractError(error);
|
||||
return extracted.code === errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a network error
|
||||
*/
|
||||
export function isNetworkError(error: any): boolean {
|
||||
return isErrorType(error, 'NETWORK_ERROR');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is an authentication error
|
||||
*/
|
||||
export function isAuthError(error: any): boolean {
|
||||
return isErrorType(error, 'UNAUTHORIZED') || isErrorType(error, 'AUTH_TOKEN_EXPIRED');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a validation error
|
||||
*/
|
||||
export function isValidationError(error: any): boolean {
|
||||
const extracted = extractError(error);
|
||||
return extracted.code.startsWith('VALIDATION_') || extracted.field !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field-specific error message
|
||||
*/
|
||||
export function getFieldError(error: any, fieldName: string): string | null {
|
||||
const extracted = extractError(error);
|
||||
|
||||
if (extracted.field === fieldName) {
|
||||
return extracted.message;
|
||||
}
|
||||
|
||||
if (extracted.details && typeof extracted.details === 'object') {
|
||||
return extracted.details[fieldName] || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error for debugging (can be extended with error tracking service)
|
||||
*/
|
||||
export function logError(error: any, context?: string): void {
|
||||
const extracted = extractError(error);
|
||||
|
||||
console.error('[Error Handler]', {
|
||||
context,
|
||||
code: extracted.code,
|
||||
message: extracted.message,
|
||||
field: extracted.field,
|
||||
details: extracted.details,
|
||||
isBackendError: extracted.isBackendError,
|
||||
originalError: error,
|
||||
});
|
||||
|
||||
// TODO: Send to error tracking service (Sentry, LogRocket, etc.)
|
||||
// Example:
|
||||
// if (window.Sentry) {
|
||||
// window.Sentry.captureException(error, {
|
||||
// tags: { code: extracted.code },
|
||||
// contexts: { errorHandler: { context } },
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle error and return user-friendly message
|
||||
* Main function to use in catch blocks
|
||||
*/
|
||||
export function handleError(error: any, context?: string): string {
|
||||
logError(error, context);
|
||||
return formatErrorMessage(error);
|
||||
}
|
||||
Reference in New Issue
Block a user