not sure why those errors got removed we'll see

This commit is contained in:
2025-12-24 16:16:42 -08:00
parent 7399a27600
commit de3f21a7ec
20 changed files with 96 additions and 258 deletions

View File

@@ -44,7 +44,7 @@ export const FlyerCorrectionTool: React.FC<FlyerCorrectionToolProps> = ({
})
.catch((err) => {
console.error('[DEBUG] FlyerCorrectionTool: Failed to fetch image.', { err });
logger.error('Failed to fetch image for correction tool', { error: err });
logger.error({ error: err }, 'Failed to fetch image for correction tool');
notifyError('Could not load the image for correction.');
});
}
@@ -164,7 +164,7 @@ export const FlyerCorrectionTool: React.FC<FlyerCorrectionToolProps> = ({
const msg = err instanceof Error ? err.message : 'An unknown error occurred.';
console.error('[DEBUG] handleRescan: Caught an error.', { error: err });
notifyError(msg);
logger.error('Error during rescan:', { error: err });
logger.error({ error: err }, 'Error during rescan:');
} finally {
console.debug('[DEBUG] handleRescan: Finished. Setting isProcessing=false.');
setIsProcessing(false);

View File

@@ -112,7 +112,7 @@ export const FlyerUploader: React.FC<FlyerUploaderProps> = ({ onProcessingComple
break;
}
} catch (error) {
logger.error('Error during polling:', { error });
logger.error({ error }, 'Error during polling:');
setErrorMessage(
error instanceof Error ? error.message : 'An unexpected error occurred during polling.',
);
@@ -157,7 +157,7 @@ export const FlyerUploader: React.FC<FlyerUploaderProps> = ({ onProcessingComple
setProcessingState('polling');
} catch (error: any) {
// Handle the structured error thrown by the API client.
logger.error('An error occurred during file upload:', { error });
logger.error({ error }, 'An error occurred during file upload:');
// Handle 409 Conflict for duplicate flyers
if (error?.status === 409 && error.body?.flyerId) {
setErrorMessage(`This flyer has already been processed. You can view it here:`);

View File

@@ -1,94 +1,53 @@
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import {
DatabaseError,
UniqueConstraintError,
ForeignKeyConstraintError,
NotFoundError,
ValidationError,
ValidationIssue,
} from '../services/db/errors.db';
import crypto from 'crypto';
import { ZodError } from 'zod';
import { NotFoundError, UniqueConstraintError, ValidationError } from '../services/db/errors.db';
import { logger } from '../services/logger.server';
interface HttpError extends Error {
status?: number;
}
export const errorHandler = (err: HttpError, req: Request, res: Response, next: NextFunction) => {
// If the response headers have already been sent, we must delegate to the default Express error handler.
/**
* A centralized error handling middleware for the Express application.
* This middleware should be the LAST `app.use()` call to catch all errors from previous routes and middleware.
*
* It standardizes error responses and ensures consistent logging.
*/
export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
// If headers have already been sent, delegate to the default Express error handler.
if (res.headersSent) {
return next(err);
}
// The pino-http middleware guarantees that `req.log` will be available.
const log = req.log;
// Use the request-scoped logger if available, otherwise fall back to the global logger.
const log = req.log || logger;
// --- 1. Determine Final Status Code and Message ---
let statusCode = err.status ?? 500;
const message = err.message;
let validationIssues: ValidationIssue[] | undefined;
let errorId: string | undefined;
// Refine the status code for known error types. Check for most specific types first.
if (err instanceof UniqueConstraintError) {
statusCode = 409; // Conflict
} else if (err instanceof NotFoundError) {
statusCode = 404;
} else if (err instanceof ForeignKeyConstraintError) {
statusCode = 400;
} else if (err instanceof ValidationError) {
statusCode = 400;
validationIssues = err.validationErrors;
} else if (err instanceof DatabaseError) {
// This is a generic fallback for other database errors that are not the specific subclasses above.
statusCode = err.status;
} else if (err.name === 'UnauthorizedError') {
statusCode = err.status || 401;
// --- Handle Zod Validation Errors ---
if (err instanceof ZodError) {
log.warn({ err: err.flatten() }, 'Request validation failed');
return res.status(400).json({
message: 'The request data is invalid.',
errors: err.errors.map((e) => ({ path: e.path, message: e.message })),
});
}
// --- 2. Log Based on Final Status Code ---
// Log the full error details for debugging, especially for server errors.
if (statusCode >= 500) {
errorId = crypto.randomBytes(4).toString('hex');
// The request-scoped logger already contains user, IP, and request_id.
// We add the full error and the request object itself.
// Pino's `redact` config will automatically sanitize sensitive fields in `req`.
log.error(
{
err,
errorId,
req: { method: req.method, url: req.originalUrl, headers: req.headers, body: req.body },
},
`Unhandled API Error (ID: ${errorId})`,
);
} else {
// For 4xx errors, log at a lower level (e.g., 'warn') to avoid flooding error trackers.
// We include the validation errors in the log context if they exist.
log.warn(
{
err,
validationErrors: validationIssues, // Add validation issues to the log object
statusCode,
},
`Client Error on ${req.method} ${req.path}: ${message}`,
);
// --- Handle Custom Operational Errors ---
if (err instanceof NotFoundError) {
log.info({ err }, 'Resource not found');
return res.status(404).json({ message: err.message });
}
// --- TEST ENVIRONMENT DEBUGGING ---
if (process.env.NODE_ENV === 'test') {
console.error('--- [TEST] UNHANDLED ERROR ---', err);
if (err instanceof UniqueConstraintError || err instanceof ValidationError) {
log.warn({ err }, 'Constraint or validation error occurred');
return res.status(400).json({ message: err.message });
}
// --- 3. Send Response ---
// In production, send a generic message for 5xx errors.
// In dev/test, send the actual error message for easier debugging.
const responseMessage =
statusCode >= 500 && process.env.NODE_ENV === 'production'
? `An unexpected server error occurred. Please reference error ID: ${errorId}`
: message;
// --- Handle Generic Errors ---
// Log the full error object for debugging. The pino logger will handle redaction.
log.error({ err }, 'An unhandled error occurred in an Express route');
res.status(statusCode).json({
message: responseMessage,
...(validationIssues && { errors: validationIssues }), // Conditionally add the 'errors' array if it exists
});
};
// In production, send a generic message to avoid leaking implementation details.
if (process.env.NODE_ENV === 'production') {
return res.status(500).json({ message: 'An internal server error occurred.' });
}
// In development, send more details for easier debugging.
return res.status(500).json({ message: err.message, stack: err.stack });
};

View File

@@ -375,7 +375,6 @@ router.put(
const updatedUser = await db.adminRepo.updateUserRole(params.id, body.role, req.log);
res.json(updatedUser);
} catch (error) {
logger.error({ error }, `Error updating user ${params.id}:`);
next(error);
}
},
@@ -421,7 +420,6 @@ router.post(
'Daily deal check job has been triggered successfully. It will run in the background.',
});
} catch (error) {
logger.error({ error }, '[Admin] Failed to trigger daily deal check job.');
next(error);
}
},
@@ -450,7 +448,6 @@ router.post(
message: `Analytics report generation job has been enqueued successfully. Job ID: ${job.id}`,
});
} catch (error) {
logger.error({ error }, '[Admin] Failed to enqueue analytics report job.');
next(error);
}
},
@@ -522,7 +519,6 @@ router.post(
message: `Successfully cleared the geocode cache. ${keysDeleted} keys were removed.`,
});
} catch (error) {
logger.error({ error }, '[Admin] Failed to clear geocode cache.');
next(error);
}
},

View File

@@ -134,8 +134,7 @@ router.post(
// If the email is a duplicate, return a 409 Conflict status.
return res.status(409).json({ message: error.message });
}
// The createUser method now handles its own transaction logging, so we just log the route failure.
logger.error({ error }, `User registration route failed for email: ${email}.`);
// Pass the error to the centralized handler
return next(error);
}
},
@@ -199,10 +198,6 @@ router.post('/login', (req: Request, res: Response, next: NextFunction) => {
// Return the full user profile object on login to avoid a second fetch on the client.
return res.json({ userprofile: userProfile, token: accessToken });
} catch (tokenErr) {
req.log.error(
{ error: tokenErr },
`Failed to save refresh token during login for user: ${userProfile.user.email}`,
);
return next(tokenErr);
}
},
@@ -307,7 +302,6 @@ router.post(
res.status(200).json({ message: 'Password has been reset successfully.' });
} catch (error) {
req.log.error({ error }, `An error occurred during password reset.`);
next(error);
}
},
@@ -331,7 +325,6 @@ router.post('/refresh-token', async (req: Request, res: Response, next: NextFunc
res.json({ token: newAccessToken });
} catch (error) {
req.log.error({ error }, 'An error occurred during /refresh-token.');
next(error);
}
});

View File

@@ -46,7 +46,6 @@ router.get('/', async (req: Request, res: Response, next: NextFunction) => {
const budgets = await budgetRepo.getBudgetsForUser(userProfile.user.user_id, req.log);
res.json(budgets);
} catch (error) {
req.log.error({ error, userId: userProfile.user.user_id }, 'Error fetching budgets');
next(error);
}
});
@@ -65,7 +64,6 @@ router.post(
const newBudget = await budgetRepo.createBudget(userProfile.user.user_id, body, req.log);
res.status(201).json(newBudget);
} catch (error: unknown) {
req.log.error({ error, userId: userProfile.user.user_id, body }, 'Error creating budget');
next(error);
}
},
@@ -90,10 +88,6 @@ router.put(
);
res.json(updatedBudget);
} catch (error: unknown) {
req.log.error(
{ error, userId: userProfile.user.user_id, budgetId: params.id },
'Error updating budget',
);
next(error);
}
},
@@ -113,10 +107,6 @@ router.delete(
await budgetRepo.deleteBudget(params.id, userProfile.user.user_id, req.log);
res.status(204).send(); // No Content
} catch (error: unknown) {
req.log.error(
{ error, userId: userProfile.user.user_id, budgetId: params.id },
'Error deleting budget',
);
next(error);
}
},
@@ -145,10 +135,6 @@ router.get(
);
res.json(spendingData);
} catch (error) {
req.log.error(
{ error, userId: userProfile.user.user_id, startDate, endDate },
'Error fetching spending analysis',
);
next(error);
}
},

View File

@@ -57,7 +57,6 @@ router.get('/', validateRequest(getFlyersSchema), async (req, res, next): Promis
const flyers = await db.flyerRepo.getFlyers(req.log, limit, offset);
res.json(flyers);
} catch (error) {
req.log.error({ error }, 'Error fetching flyers in /api/flyers:');
next(error);
}
});
@@ -72,7 +71,6 @@ router.get('/:id', validateRequest(flyerIdParamSchema), async (req, res, next):
const flyer = await db.flyerRepo.getFlyerById(params.id);
res.json(flyer);
} catch (error) {
req.log.error({ error, flyerId: params.id }, 'Error fetching flyer by ID:');
next(error);
}
});
@@ -89,7 +87,6 @@ router.get(
const items = await db.flyerRepo.getFlyerItems(params.id, req.log);
res.json(items);
} catch (error) {
req.log.error({ error }, 'Error fetching flyer items in /api/flyers/:id/items:');
next(error);
}
},

View File

@@ -38,7 +38,6 @@ router.get('/', async (req, res, next: NextFunction) => {
const achievements = await gamificationRepo.getAllAchievements(req.log);
res.json(achievements);
} catch (error) {
logger.error({ error }, 'Error fetching all achievements in /api/achievements:');
next(error);
}
});
@@ -61,7 +60,6 @@ router.get(
const leaderboard = await gamificationRepo.getLeaderboard(limit, req.log);
res.json(leaderboard);
} catch (error) {
logger.error({ error }, 'Error fetching leaderboard:');
next(error);
}
},
@@ -85,10 +83,6 @@ router.get(
);
res.json(userAchievements);
} catch (error) {
logger.error(
{ error, userId: userProfile.user.user_id },
'Error fetching user achievements:',
);
next(error);
}
},
@@ -122,10 +116,6 @@ adminGamificationRouter.post(
res.status(400).json({ message: error.message });
return;
}
logger.error(
{ error, userId: body.userId, achievementName: body.achievementName },
'Error awarding achievement via admin endpoint:',
);
next(error);
}
},

View File

@@ -39,10 +39,6 @@ router.get('/db-schema', validateRequest(emptySchema), async (req, res, next: Ne
}
return res.status(200).json({ success: true, message: 'All required database tables exist.' });
} catch (error: unknown) {
logger.error(
{ error: error instanceof Error ? error.message : error },
'Error during DB schema check:',
);
next(error);
}
});
@@ -62,10 +58,6 @@ router.get('/storage', validateRequest(emptySchema), async (req, res, next: Next
message: `Storage directory '${storagePath}' is accessible and writable.`,
});
} catch (error: unknown) {
logger.error(
{ error: error instanceof Error ? error.message : error },
`Storage check failed for path: ${storagePath}`,
);
next(
new Error(
`Storage check failed. Ensure the directory '${storagePath}' exists and is writable by the application.`,
@@ -96,10 +88,6 @@ router.get(
.json({ success: false, message: `Pool may be under stress. ${message}` });
}
} catch (error: unknown) {
logger.error(
{ error: error instanceof Error ? error.message : error },
'Error during DB pool health check:',
);
next(error);
}
},

View File

@@ -41,7 +41,6 @@ router.get(
const recipes = await db.recipeRepo.getRecipesBySalePercentage(query.minPercentage!, req.log);
res.json(recipes);
} catch (error) {
req.log.error({ error }, 'Error fetching recipes in /api/recipes/by-sale-percentage:');
next(error);
}
},
@@ -63,7 +62,6 @@ router.get(
);
res.json(recipes);
} catch (error) {
req.log.error({ error }, 'Error fetching recipes in /api/recipes/by-sale-ingredients:');
next(error);
}
},
@@ -85,7 +83,6 @@ router.get(
);
res.json(recipes);
} catch (error) {
req.log.error({ error }, 'Error fetching recipes in /api/recipes/by-ingredient-and-tag:');
next(error);
}
},
@@ -101,7 +98,6 @@ router.get('/:recipeId/comments', validateRequest(recipeIdParamsSchema), async (
const comments = await db.recipeRepo.getRecipeComments(params.recipeId, req.log);
res.json(comments);
} catch (error) {
req.log.error({ error }, `Error fetching comments for recipe ID ${req.params.recipeId}:`);
next(error);
}
});
@@ -116,7 +112,6 @@ router.get('/:recipeId', validateRequest(recipeIdParamsSchema), async (req, res,
const recipe = await db.recipeRepo.getRecipeById(params.recipeId, req.log);
res.json(recipe);
} catch (error) {
req.log.error({ error }, `Error fetching recipe ID ${req.params.recipeId}:`);
next(error);
}
});

View File

@@ -35,10 +35,6 @@ router.get(
const items = await db.adminRepo.getMostFrequentSaleItems(days!, limit!, req.log);
res.json(items);
} catch (error) {
req.log.error(
{ error },
'Error fetching most frequent sale items in /api/stats/most-frequent-sales:',
);
next(error);
}
},

View File

@@ -38,17 +38,11 @@ router.get(
message: 'Application process is not running under PM2.',
});
}
logger.error(
{ error: stderr || error.message },
'[API /pm2-status] Error executing pm2 describe:',
);
return next(error);
}
// Check if there was output to stderr, even if the exit code was 0 (success).
// This handles warnings or non-fatal errors that should arguably be treated as failures in this context.
if (stderr && stderr.trim().length > 0) {
logger.error({ stderr }, '[API /pm2-status] PM2 executed but produced stderr:');
return next(new Error(`PM2 command produced an error: ${stderr}`));
}

View File

@@ -77,7 +77,7 @@ router.use(passport.authenticate('jwt', { session: false }));
// Ensure the directory for avatar uploads exists.
const avatarUploadDir = path.join(process.cwd(), 'public', 'uploads', 'avatars');
fs.mkdir(avatarUploadDir, { recursive: true }).catch((err) => {
logger.error('Failed to create avatar upload directory:', err);
logger.error({ err }, 'Failed to create avatar upload directory');
});
// Define multer storage configuration. The `req.user` object will be available
@@ -214,7 +214,6 @@ router.get('/profile', validateRequest(emptySchema), async (req, res, next: Next
);
res.json(fullUserProfile);
} catch (error) {
logger.error({ error }, `[ROUTE] GET /api/users/profile - ERROR`);
next(error);
}
});
@@ -239,7 +238,6 @@ router.put(
);
res.json(updatedProfile);
} catch (error) {
logger.error({ error }, `[ROUTE] PUT /api/users/profile - ERROR`);
next(error);
}
},
@@ -264,7 +262,6 @@ router.put(
await db.userRepo.updateUserPassword(userProfile.user.user_id, hashedPassword, req.log);
res.status(200).json({ message: 'Password updated successfully.' });
} catch (error) {
logger.error({ error }, `[ROUTE] PUT /api/users/profile/password - ERROR`);
next(error);
}
},
@@ -300,7 +297,6 @@ router.delete(
await db.userRepo.deleteUserById(userProfile.user.user_id, req.log);
res.status(200).json({ message: 'Account deleted successfully.' });
} catch (error) {
logger.error({ error }, `[ROUTE] DELETE /api/users/account - ERROR`);
next(error);
}
},
@@ -316,7 +312,6 @@ router.get('/watched-items', validateRequest(emptySchema), async (req, res, next
const items = await db.personalizationRepo.getWatchedItems(userProfile.user.user_id, req.log);
res.json(items);
} catch (error) {
logger.error({ error }, `[ROUTE] GET /api/users/watched-items - ERROR`);
next(error);
}
});
@@ -345,11 +340,6 @@ router.post(
if (error instanceof ForeignKeyConstraintError) {
return res.status(400).json({ message: error.message });
}
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
logger.error({
errorMessage,
body: req.body,
});
next(error);
}
},
@@ -376,7 +366,6 @@ router.delete(
);
res.status(204).send();
} catch (error) {
logger.error({ error }, `[ROUTE] DELETE /api/users/watched-items/:masterItemId - ERROR`);
next(error);
}
},
@@ -395,7 +384,6 @@ router.get(
const lists = await db.shoppingRepo.getShoppingLists(userProfile.user.user_id, req.log);
res.json(lists);
} catch (error) {
logger.error({ error }, `[ROUTE] GET /api/users/shopping-lists - ERROR`);
next(error);
}
},
@@ -421,10 +409,6 @@ router.get(
);
res.json(list);
} catch (error) {
logger.error(
{ error, listId: params.listId },
`[ROUTE] GET /api/users/shopping-lists/:listId - ERROR`,
);
next(error);
}
},
@@ -453,11 +437,6 @@ router.post(
if (error instanceof ForeignKeyConstraintError) {
return res.status(400).json({ message: error.message });
}
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
logger.error({
errorMessage,
body: req.body,
});
next(error);
}
},
@@ -478,11 +457,6 @@ router.delete(
await db.shoppingRepo.deleteShoppingList(params.listId, userProfile.user.user_id, req.log);
res.status(204).send();
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
logger.error(
{ errorMessage, params: req.params },
`[ROUTE] DELETE /api/users/shopping-lists/:listId - ERROR`,
);
next(error);
}
},
@@ -516,12 +490,6 @@ router.post(
if (error instanceof ForeignKeyConstraintError) {
return res.status(400).json({ message: error.message });
}
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
logger.error({
errorMessage,
params: req.params,
body: req.body,
});
next(error);
}
},
@@ -556,10 +524,6 @@ router.put(
);
res.json(updatedItem);
} catch (error: unknown) {
logger.error(
{ error, params: req.params, body: req.body },
`[ROUTE] PUT /api/users/shopping-lists/items/:itemId - ERROR`,
);
next(error);
}
},
@@ -581,10 +545,6 @@ router.delete(
await db.shoppingRepo.removeShoppingListItem(params.itemId, req.log);
res.status(204).send();
} catch (error: unknown) {
logger.error(
{ error, params: req.params },
`[ROUTE] DELETE /api/users/shopping-lists/items/:itemId - ERROR`,
);
next(error);
}
},
@@ -613,7 +573,6 @@ router.put(
);
res.json(updatedProfile);
} catch (error) {
logger.error({ error }, `[ROUTE] PUT /api/users/profile/preferences - ERROR`);
next(error);
}
},
@@ -632,7 +591,6 @@ router.get(
);
res.json(restrictions);
} catch (error) {
logger.error({ error }, `[ROUTE] GET /api/users/me/dietary-restrictions - ERROR`);
next(error);
}
},
@@ -661,11 +619,6 @@ router.put(
if (error instanceof ForeignKeyConstraintError) {
return res.status(400).json({ message: error.message });
}
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
logger.error({
errorMessage,
body: req.body,
});
next(error);
}
},
@@ -681,7 +634,6 @@ router.get('/me/appliances', validateRequest(emptySchema), async (req, res, next
);
res.json(appliances);
} catch (error) {
logger.error({ error }, `[ROUTE] GET /api/users/me/appliances - ERROR`);
next(error);
}
});
@@ -709,11 +661,6 @@ router.put(
if (error instanceof ForeignKeyConstraintError) {
return res.status(400).json({ message: error.message });
}
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
logger.error({
errorMessage,
body: req.body,
});
next(error);
}
},
@@ -803,10 +750,6 @@ router.delete(
await db.recipeRepo.deleteRecipe(params.recipeId, userProfile.user.user_id, false, req.log);
res.status(204).send();
} catch (error) {
logger.error(
{ error, params: req.params },
`[ROUTE] DELETE /api/users/recipes/:recipeId - ERROR`,
);
next(error);
}
},
@@ -847,10 +790,6 @@ router.put(
);
res.json(updatedRecipe);
} catch (error) {
logger.error(
{ error, params: req.params, body: req.body },
`[ROUTE] PUT /api/users/recipes/:recipeId - ERROR`,
);
next(error);
}
},

View File

@@ -51,9 +51,7 @@ export class AiAnalysisService {
// Normalize sources to a consistent format.
const mappedSources = (response.sources || []).map(
(s: RawSource) =>
(s.web
? { uri: s.web.uri || '', title: 'Untitled' }
: { uri: '', title: 'Untitled' }) as Source,
(s.web ? { uri: s.web.uri || '', title: s.web.title || 'Untitled' } : { uri: '', title: 'Untitled' }) as Source,
);
return { ...response, sources: mappedSources };
}
@@ -84,9 +82,7 @@ export class AiAnalysisService {
// Normalize sources to a consistent format.
const mappedSources = (response.sources || []).map(
(s: RawSource) =>
(s.web
? { uri: s.web.uri || '', title: 'Untitled' }
: { uri: '', title: 'Untitled' }) as Source,
(s.web ? { uri: s.web.uri || '', title: s.web.title || 'Untitled' } : { uri: '', title: 'Untitled' }) as Source,
);
return { ...response, sources: mappedSources };
}

View File

@@ -239,8 +239,8 @@ describe('AI Service (Server)', () => {
expect(mockGenerateContent).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(
`[AIService Adapter] Model 'gemini-2.5-flash' failed with a non-retriable error.`,
{ error: nonRetriableError },
`[AIService Adapter] Model 'gemini-2.5-flash' failed with a non-retriable error.`,
);
});
@@ -281,8 +281,8 @@ describe('AI Service (Server)', () => {
});
expect(logger.error).toHaveBeenCalledWith(
'[AIService Adapter] All AI models failed. Throwing last known error.',
{ lastError: quotaError3 },
'[AIService Adapter] All AI models failed. Throwing last known error.',
);
});
});

View File

@@ -186,13 +186,13 @@ export class AIService {
return result;
} catch (error: unknown) {
lastError = error instanceof Error ? error : new Error(String(error));
const errorMessage = lastError.message || '';
const errorMessage = (lastError.message || '').toLowerCase(); // Make case-insensitive
// Check for specific error messages indicating quota issues or model unavailability.
if (
errorMessage.includes('quota') ||
errorMessage.includes('429') || // HTTP 429 Too Many Requests
errorMessage.includes('RESOURCE_EXHAUSTED') ||
errorMessage.includes('resource_exhausted') || // Make case-insensitive
errorMessage.includes('model is overloaded')
) {
this.logger.warn(

View File

@@ -1,6 +1,7 @@
// src/services/apiClient.ts
import { Profile, ShoppingListItem, SearchQuery, Budget, Address } from '../types';
import { logger } from './logger.client';
import { eventBus } from './eventBus';
// This constant should point to your backend API.
// It's often a good practice to store this in an environment variable.
@@ -62,12 +63,12 @@ const refreshToken = async (): Promise<string> => {
logger.info('Successfully refreshed access token.');
return data.token;
} catch (error) {
logger.error('Failed to refresh token. User will be logged out.', { error });
logger.error({ error }, 'Failed to refresh token. User session has expired.');
// Only perform browser-specific actions if in the browser environment.
if (typeof window !== 'undefined') {
localStorage.removeItem('authToken');
// A hard redirect is a simple way to reset the app state to logged-out.
// window.location.href = '/'; // Removed to allow the caller to handle session expiry.
// Dispatch a global event that the UI layer can listen for to handle session expiry.
eventBus.dispatch('sessionExpired');
}
throw error;
}
@@ -144,9 +145,8 @@ export const apiFetch = async (
// --- DEBUG LOGGING for failed requests ---
if (!response.ok) {
const responseText = await response.clone().text();
logger.error(
`apiFetch: Request to ${fullUrl} failed with status ${response.status}. Response body:`,
responseText,
logger.error({ url: fullUrl, status: response.status, body: responseText },
'apiFetch: Request failed',
);
}
// --- END DEBUG LOGGING ---

31
src/services/eventBus.ts Normal file
View File

@@ -0,0 +1,31 @@
// src/services/eventBus.ts
/**
* A simple, generic event bus for cross-component communication without direct coupling.
* This is particularly useful for broadcasting application-wide events, such as session expiry.
*/
type EventCallback = (data?: any) => void;
class EventBus {
private listeners: { [key: string]: EventCallback[] } = {};
on(event: string, callback: EventCallback): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
off(event: string, callback: EventCallback): void {
if (!this.listeners[event]) return;
this.listeners[event] = this.listeners[event].filter((l) => l !== callback);
}
dispatch(event: string, data?: any): void {
if (!this.listeners[event]) return;
this.listeners[event].forEach((callback) => callback(data));
}
}
export const eventBus = new EventBus();

View File

@@ -25,10 +25,7 @@ export class GeocodingService {
return JSON.parse(cached);
}
} catch (error) {
logger.error(
{ err: error instanceof Error ? error.message : error, cacheKey },
'Redis GET or JSON.parse command failed. Proceeding without cache.',
);
logger.error({ err: error, cacheKey }, 'Redis GET or JSON.parse command failed. Proceeding without cache.');
}
if (process.env.GOOGLE_MAPS_API_KEY) {
@@ -44,8 +41,8 @@ export class GeocodingService {
);
} catch (error) {
logger.error(
{ err: error instanceof Error ? error.message : error },
'An error occurred while calling the Google Maps Geocoding API. Falling back to Nominatim.',
{ err: error },
'An error occurred while calling the Google Maps Geocoding API. Falling back to Nominatim.'
);
}
} else {
@@ -72,10 +69,7 @@ export class GeocodingService {
try {
await redis.set(cacheKey, JSON.stringify(result), 'EX', 60 * 60 * 24 * 30); // Cache for 30 days
} catch (error) {
logger.error(
{ err: error instanceof Error ? error.message : error, cacheKey },
'Redis SET command failed. Result will not be cached.',
);
logger.error({ err: error, cacheKey }, 'Redis SET command failed. Result will not be cached.');
}
}
@@ -98,10 +92,7 @@ export class GeocodingService {
logger.info(`Successfully deleted ${totalDeleted} geocode cache entries.`);
return totalDeleted;
} catch (error) {
logger.error(
{ err: error instanceof Error ? error.message : error },
'Failed to clear geocode cache from Redis.',
);
logger.error({ err: error }, 'Failed to clear geocode cache from Redis.');
throw error;
}
}

View File

@@ -224,13 +224,9 @@ export const emailWorker = new Worker<EmailJobData>(
try {
await emailService.sendEmail(job.data, jobLogger);
} catch (error: unknown) {
// Standardize error logging to capture the full error object, including the stack trace.
// This provides more context for debugging than just logging the message.
logger.error(
{
// Log the full error object for better diagnostics. // The patch requested this specific error handling.
err: error instanceof Error ? error : new Error(String(error)),
// Also include the job data for context.
err: error,
jobData: job.data,
},
`[EmailWorker] Job ${job.id} failed. Attempt ${job.attemptsMade}/${job.opts.attempts}.`,
@@ -266,11 +262,7 @@ export const analyticsWorker = new Worker<AnalyticsJobData>(
logger.info(`[AnalyticsWorker] Successfully generated report for ${reportDate}.`);
} catch (error: unknown) {
// Standardize error logging.
logger.error(
{
err: error instanceof Error ? error : new Error(String(error)),
jobData: job.data,
},
logger.error({ err: error, jobData: job.data },
`[AnalyticsWorker] Job ${job.id} failed. Attempt ${job.attemptsMade}/${job.opts.attempts}.`,
);
throw error; // Re-throw to let BullMQ handle the failure and retry.
@@ -332,9 +324,7 @@ export const cleanupWorker = new Worker<CleanupJobData>(
} catch (error: unknown) {
// Standardize error logging.
logger.error(
{
err: error instanceof Error ? error : new Error(String(error)),
},
{ err: error },
`[CleanupWorker] Job ${job.id} for flyer ${flyerId} failed. Attempt ${job.attemptsMade}/${job.opts.attempts}.`,
);
throw error; // Re-throw to let BullMQ handle the failure and retry.
@@ -367,10 +357,7 @@ export const weeklyAnalyticsWorker = new Worker<WeeklyAnalyticsJobData>(
} catch (error: unknown) {
// Standardize error logging.
logger.error(
{
err: error instanceof Error ? error : new Error(String(error)),
jobData: job.data,
},
{ err: error, jobData: job.data },
`[WeeklyAnalyticsWorker] Job ${job.id} failed. Attempt ${job.attemptsMade}/${job.opts.attempts}.`,
);
throw error; // Re-throw to let BullMQ handle the failure and retry.