Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4bbf5c251 | ||
| 32a9e6732b | |||
| e7c076e2ed | |||
|
|
dbe8e72efe | ||
| 38bd193042 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.6",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// src/middleware/errorHandler.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ZodError } from 'zod';
|
||||
import { NotFoundError, UniqueConstraintError, ValidationError } from '../services/db/errors.db';
|
||||
import {
|
||||
ForeignKeyConstraintError,
|
||||
NotFoundError,
|
||||
UniqueConstraintError,
|
||||
ValidationError,
|
||||
} from '../services/db/errors.db';
|
||||
import { logger } from '../services/logger.server';
|
||||
|
||||
/**
|
||||
@@ -24,7 +29,7 @@ export const errorHandler = (err: Error, req: Request, res: Response, next: Next
|
||||
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 })),
|
||||
errors: err.issues.map((e) => ({ path: e.path, message: e.message })),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,8 +39,18 @@ export const errorHandler = (err: Error, req: Request, res: Response, next: Next
|
||||
return res.status(404).json({ message: err.message });
|
||||
}
|
||||
|
||||
if (err instanceof UniqueConstraintError || err instanceof ValidationError) {
|
||||
log.warn({ err }, 'Constraint or validation error occurred');
|
||||
if (err instanceof ValidationError) {
|
||||
log.warn({ err }, 'Validation error occurred');
|
||||
return res.status(400).json({ message: err.message, errors: err.validationErrors });
|
||||
}
|
||||
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
log.warn({ err }, 'Constraint error occurred');
|
||||
return res.status(409).json({ message: err.message }); // Use 409 Conflict for unique constraints
|
||||
}
|
||||
|
||||
if (err instanceof ForeignKeyConstraintError) {
|
||||
log.warn({ err }, 'Foreign key constraint violation');
|
||||
return res.status(400).json({ message: err.message });
|
||||
}
|
||||
|
||||
|
||||
@@ -176,15 +176,13 @@ describe('API Client', () => {
|
||||
// We expect the promise to still resolve with the bad response, but log an error.
|
||||
await apiClient.apiFetch('/some/failing/endpoint');
|
||||
|
||||
// FIX: Use stringContaining to be resilient to port numbers (e.g., localhost:3001)
|
||||
// This checks for the essential parts of the log message without being brittle.
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining('apiFetch: Request to http://'),
|
||||
'Internal Server Error',
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/api/some/failing/endpoint failed with status 500'),
|
||||
'Internal Server Error',
|
||||
expect.objectContaining({
|
||||
status: 500,
|
||||
body: 'Internal Server Error',
|
||||
url: expect.stringContaining('/some/failing/endpoint'),
|
||||
}),
|
||||
'apiFetch: Request failed',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -242,10 +240,6 @@ describe('API Client', () => {
|
||||
expect(logger.warn).toHaveBeenCalledWith('Failed to track flyer item interaction', {
|
||||
error: apiError,
|
||||
});
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith('Failed to track flyer item interaction', {
|
||||
error: apiError,
|
||||
});
|
||||
});
|
||||
|
||||
it('logSearchQuery should log a warning on failure', async () => {
|
||||
@@ -259,8 +253,6 @@ describe('API Client', () => {
|
||||
was_successful: false,
|
||||
});
|
||||
expect(logger.warn).toHaveBeenCalledWith('Failed to log search query', { error: apiError });
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith('Failed to log search query', { error: apiError });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('Geocoding Service', () => {
|
||||
// Assert
|
||||
expect(result).toEqual(coordinates);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ err: 'Redis down', cacheKey: expect.any(String) },
|
||||
{ err: expect.any(Error), cacheKey: expect.any(String) },
|
||||
'Redis GET or JSON.parse command failed. Proceeding without cache.',
|
||||
);
|
||||
expect(mockGoogleService.geocode).toHaveBeenCalled(); // Should still proceed to fetch
|
||||
@@ -107,7 +107,7 @@ describe('Geocoding Service', () => {
|
||||
expect(mocks.mockRedis.get).toHaveBeenCalledWith(cacheKey);
|
||||
// The service should log the JSON parsing error and continue
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ err: expect.any(String), cacheKey: expect.any(String) },
|
||||
{ err: expect.any(SyntaxError), cacheKey: expect.any(String) },
|
||||
'Redis GET or JSON.parse command failed. Proceeding without cache.',
|
||||
);
|
||||
expect(mockGoogleService.geocode).toHaveBeenCalledTimes(1);
|
||||
@@ -185,7 +185,7 @@ describe('Geocoding Service', () => {
|
||||
// Assert
|
||||
expect(result).toEqual(coordinates);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ err: 'Network Error' },
|
||||
{ err: expect.any(Error) },
|
||||
expect.stringContaining('An error occurred while calling the Google Maps Geocoding API'),
|
||||
);
|
||||
expect(mockNominatimService.geocode).toHaveBeenCalledWith(address, logger);
|
||||
@@ -223,7 +223,7 @@ describe('Geocoding Service', () => {
|
||||
expect(mockGoogleService.geocode).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.mockRedis.set).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ err: 'Redis SET failed', cacheKey: expect.any(String) },
|
||||
{ err: expect.any(Error), cacheKey: expect.any(String) },
|
||||
'Redis SET command failed. Result will not be cached.',
|
||||
);
|
||||
});
|
||||
@@ -271,7 +271,7 @@ describe('Geocoding Service', () => {
|
||||
// Act & Assert
|
||||
await expect(geocodingService.clearGeocodeCache(logger)).rejects.toThrow(redisError);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ err: redisError.message },
|
||||
{ err: expect.any(Error) },
|
||||
'Failed to clear geocode cache from Redis.',
|
||||
);
|
||||
expect(mocks.mockRedis.del).not.toHaveBeenCalled();
|
||||
|
||||
@@ -164,6 +164,14 @@ const flyerProcessingService = new FlyerProcessingService(
|
||||
new FlyerDataTransformer(), // Inject the new transformer
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper to ensure that an unknown error is normalized to an Error object.
|
||||
* This ensures consistent logging structure and stack traces.
|
||||
*/
|
||||
const normalizeError = (error: unknown): Error => {
|
||||
return error instanceof Error ? error : new Error(String(error));
|
||||
};
|
||||
|
||||
/**
|
||||
* A generic function to attach logging event listeners to any worker.
|
||||
* This centralizes logging for job completion and final failure.
|
||||
@@ -189,16 +197,17 @@ export const flyerWorker = new Worker<FlyerJobData>(
|
||||
try {
|
||||
// The processJob method creates its own job-specific logger internally.
|
||||
return await flyerProcessingService.processJob(job);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
const wrappedError = normalizeError(error);
|
||||
// Check for quota errors or other unrecoverable errors from the AI service
|
||||
const errorMessage = error?.message || '';
|
||||
const errorMessage = wrappedError.message || '';
|
||||
if (
|
||||
errorMessage.includes('quota') ||
|
||||
errorMessage.includes('429') ||
|
||||
errorMessage.includes('RESOURCE_EXHAUSTED')
|
||||
) {
|
||||
logger.error(
|
||||
{ err: error, jobId: job.id },
|
||||
{ err: wrappedError, jobId: job.id },
|
||||
'[FlyerWorker] Unrecoverable quota error detected. Failing job immediately.',
|
||||
);
|
||||
throw new UnrecoverableError(errorMessage);
|
||||
@@ -224,15 +233,16 @@ export const emailWorker = new Worker<EmailJobData>(
|
||||
try {
|
||||
await emailService.sendEmail(job.data, jobLogger);
|
||||
} catch (error: unknown) {
|
||||
const wrappedError = normalizeError(error);
|
||||
logger.error(
|
||||
{
|
||||
err: error,
|
||||
err: wrappedError,
|
||||
jobData: job.data,
|
||||
},
|
||||
`[EmailWorker] Job ${job.id} failed. Attempt ${job.attemptsMade}/${job.opts.attempts}.`,
|
||||
);
|
||||
// Re-throw to let BullMQ handle the failure and retry.
|
||||
throw error;
|
||||
throw wrappedError;
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -261,11 +271,12 @@ export const analyticsWorker = new Worker<AnalyticsJobData>(
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000)); // Simulate a 10-second task
|
||||
logger.info(`[AnalyticsWorker] Successfully generated report for ${reportDate}.`);
|
||||
} catch (error: unknown) {
|
||||
const wrappedError = normalizeError(error);
|
||||
// Standardize error logging.
|
||||
logger.error({ err: error, jobData: job.data },
|
||||
logger.error({ err: wrappedError, 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.
|
||||
throw wrappedError; // Re-throw to let BullMQ handle the failure and retry.
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -322,12 +333,13 @@ export const cleanupWorker = new Worker<CleanupJobData>(
|
||||
`[CleanupWorker] Successfully cleaned up ${paths.length} file(s) for flyer ${flyerId}.`,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const wrappedError = normalizeError(error);
|
||||
// Standardize error logging.
|
||||
logger.error(
|
||||
{ err: error },
|
||||
{ err: wrappedError },
|
||||
`[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.
|
||||
throw wrappedError; // Re-throw to let BullMQ handle the failure and retry.
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -355,12 +367,13 @@ export const weeklyAnalyticsWorker = new Worker<WeeklyAnalyticsJobData>(
|
||||
`[WeeklyAnalyticsWorker] Successfully generated weekly report for week ${reportWeek}, ${reportYear}.`,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const wrappedError = normalizeError(error);
|
||||
// Standardize error logging.
|
||||
logger.error(
|
||||
{ err: error, jobData: job.data },
|
||||
{ err: wrappedError, 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.
|
||||
throw wrappedError; // Re-throw to let BullMQ handle the failure and retry.
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -382,8 +395,9 @@ export const tokenCleanupWorker = new Worker<TokenCleanupJobData>(
|
||||
jobLogger.info(`[TokenCleanupWorker] Successfully deleted ${deletedCount} expired tokens.`);
|
||||
return { deletedCount };
|
||||
} catch (error: unknown) {
|
||||
jobLogger.error({ err: error }, `[TokenCleanupWorker] Job ${job.id} failed.`);
|
||||
throw error;
|
||||
const wrappedError = normalizeError(error);
|
||||
jobLogger.error({ err: wrappedError }, `[TokenCleanupWorker] Job ${job.id} failed.`);
|
||||
throw wrappedError;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -175,7 +175,7 @@ describe('Queue Workers', () => {
|
||||
const emailError = 'SMTP server is down'; // Reject with a string
|
||||
mocks.sendEmail.mockRejectedValue(emailError);
|
||||
|
||||
await expect(emailProcessor(job)).rejects.toBe(emailError);
|
||||
await expect(emailProcessor(job)).rejects.toThrow(emailError);
|
||||
|
||||
// The worker should wrap the string in an Error object for logging
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
|
||||
Reference in New Issue
Block a user