doc updates and test fixin
This commit is contained in:
478
docs/development/CODE-PATTERNS.md
Normal file
478
docs/development/CODE-PATTERNS.md
Normal file
@@ -0,0 +1,478 @@
|
||||
# Code Patterns
|
||||
|
||||
Common code patterns extracted from Architecture Decision Records (ADRs). Use these as templates when writing new code.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Error Handling](#error-handling)
|
||||
- [Repository Patterns](#repository-patterns)
|
||||
- [API Response Patterns](#api-response-patterns)
|
||||
- [Transaction Management](#transaction-management)
|
||||
- [Input Validation](#input-validation)
|
||||
- [Authentication](#authentication)
|
||||
- [Caching](#caching)
|
||||
- [Background Jobs](#background-jobs)
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
**ADR**: [ADR-001](../adr/0001-standardized-error-handling-for-database-operations.md)
|
||||
|
||||
### Repository Layer Error Handling
|
||||
|
||||
```typescript
|
||||
import { handleDbError, NotFoundError } from '../services/db/errors.db';
|
||||
import { PoolClient } from 'pg';
|
||||
|
||||
export async function getFlyerById(id: number, client?: PoolClient): Promise<Flyer> {
|
||||
const db = client || pool;
|
||||
|
||||
try {
|
||||
const result = await db.query('SELECT * FROM flyers WHERE id = $1', [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new NotFoundError('Flyer', id);
|
||||
}
|
||||
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
throw handleDbError(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Route Layer Error Handling
|
||||
|
||||
```typescript
|
||||
import { sendError } from '../utils/apiResponse';
|
||||
|
||||
app.get('/api/flyers/:id', async (req, res) => {
|
||||
try {
|
||||
const flyer = await flyerDb.getFlyerById(parseInt(req.params.id));
|
||||
return sendSuccess(res, flyer);
|
||||
} catch (error) {
|
||||
return sendError(res, error);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Error Types
|
||||
|
||||
```typescript
|
||||
// NotFoundError - Entity not found
|
||||
throw new NotFoundError('Flyer', id);
|
||||
|
||||
// ValidationError - Invalid input
|
||||
throw new ValidationError('Invalid email format');
|
||||
|
||||
// DatabaseError - Database operation failed
|
||||
throw new DatabaseError('Failed to insert flyer', originalError);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Repository Patterns
|
||||
|
||||
**ADR**: [ADR-034](../adr/0034-repository-layer-method-naming-conventions.md)
|
||||
|
||||
### Method Naming Conventions
|
||||
|
||||
| Prefix | Returns | Not Found Behavior | Use Case |
|
||||
| ------- | -------------- | -------------------- | ------------------------- |
|
||||
| `get*` | Entity | Throws NotFoundError | When entity must exist |
|
||||
| `find*` | Entity \| null | Returns null | When entity may not exist |
|
||||
| `list*` | Array | Returns [] | When returning multiple |
|
||||
|
||||
### Get Method (Must Exist)
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Get a flyer by ID. Throws NotFoundError if not found.
|
||||
*/
|
||||
export async function getFlyerById(id: number, client?: PoolClient): Promise<Flyer> {
|
||||
const db = client || pool;
|
||||
|
||||
try {
|
||||
const result = await db.query('SELECT * FROM flyers WHERE id = $1', [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new NotFoundError('Flyer', id);
|
||||
}
|
||||
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
throw handleDbError(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Find Method (May Not Exist)
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Find a flyer by ID. Returns null if not found.
|
||||
*/
|
||||
export async function findFlyerById(id: number, client?: PoolClient): Promise<Flyer | null> {
|
||||
const db = client || pool;
|
||||
|
||||
try {
|
||||
const result = await db.query('SELECT * FROM flyers WHERE id = $1', [id]);
|
||||
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
throw handleDbError(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### List Method (Multiple Results)
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* List all active flyers. Returns empty array if none found.
|
||||
*/
|
||||
export async function listActiveFlyers(client?: PoolClient): Promise<Flyer[]> {
|
||||
const db = client || pool;
|
||||
|
||||
try {
|
||||
const result = await db.query(
|
||||
'SELECT * FROM flyers WHERE end_date >= CURRENT_DATE ORDER BY start_date DESC',
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
throw handleDbError(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Response Patterns
|
||||
|
||||
**ADR**: [ADR-028](../adr/0028-consistent-api-response-format.md)
|
||||
|
||||
### Success Response
|
||||
|
||||
```typescript
|
||||
import { sendSuccess } from '../utils/apiResponse';
|
||||
|
||||
app.post('/api/flyers', async (req, res) => {
|
||||
const flyer = await flyerService.createFlyer(req.body);
|
||||
return sendSuccess(res, flyer, 'Flyer created successfully', 201);
|
||||
});
|
||||
```
|
||||
|
||||
### Paginated Response
|
||||
|
||||
```typescript
|
||||
import { sendPaginated } from '../utils/apiResponse';
|
||||
|
||||
app.get('/api/flyers', async (req, res) => {
|
||||
const { page = 1, pageSize = 20 } = req.query;
|
||||
const { items, total } = await flyerService.listFlyers(page, pageSize);
|
||||
|
||||
return sendPaginated(res, {
|
||||
items,
|
||||
total,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
```typescript
|
||||
import { sendError } from '../utils/apiResponse';
|
||||
|
||||
app.get('/api/flyers/:id', async (req, res) => {
|
||||
try {
|
||||
const flyer = await flyerDb.getFlyerById(parseInt(req.params.id));
|
||||
return sendSuccess(res, flyer);
|
||||
} catch (error) {
|
||||
return sendError(res, error); // Automatically maps error to correct status
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transaction Management
|
||||
|
||||
**ADR**: [ADR-002](../adr/0002-transaction-management-pattern.md)
|
||||
|
||||
### Basic Transaction
|
||||
|
||||
```typescript
|
||||
import { withTransaction } from '../services/db/transaction.db';
|
||||
|
||||
export async function createFlyerWithItems(
|
||||
flyerData: FlyerInput,
|
||||
items: FlyerItemInput[],
|
||||
): Promise<Flyer> {
|
||||
return withTransaction(async (client) => {
|
||||
// Create flyer
|
||||
const flyer = await flyerDb.createFlyer(flyerData, client);
|
||||
|
||||
// Create items
|
||||
const createdItems = await flyerItemDb.createItems(
|
||||
items.map((item) => ({ ...item, flyer_id: flyer.id })),
|
||||
client,
|
||||
);
|
||||
|
||||
// Automatically commits on success, rolls back on error
|
||||
return { ...flyer, items: createdItems };
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Transactions
|
||||
|
||||
```typescript
|
||||
export async function bulkImportFlyers(flyersData: FlyerInput[]): Promise<ImportResult> {
|
||||
return withTransaction(async (client) => {
|
||||
const results = [];
|
||||
|
||||
for (const flyerData of flyersData) {
|
||||
try {
|
||||
// Each flyer import is atomic
|
||||
const flyer = await createFlyerWithItems(
|
||||
flyerData,
|
||||
flyerData.items,
|
||||
client, // Pass transaction client
|
||||
);
|
||||
results.push({ success: true, flyer });
|
||||
} catch (error) {
|
||||
results.push({ success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Input Validation
|
||||
|
||||
**ADR**: [ADR-003](../adr/0003-input-validation-framework.md)
|
||||
|
||||
### Zod Schema Definition
|
||||
|
||||
```typescript
|
||||
// src/schemas/flyer.schemas.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createFlyerSchema = z.object({
|
||||
store_id: z.number().int().positive(),
|
||||
image_url: z
|
||||
.string()
|
||||
.url()
|
||||
.regex(/^https?:\/\/.*/),
|
||||
start_date: z.string().datetime(),
|
||||
end_date: z.string().datetime(),
|
||||
items: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
price: z.number().positive(),
|
||||
quantity: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.min(1),
|
||||
});
|
||||
|
||||
export type CreateFlyerInput = z.infer<typeof createFlyerSchema>;
|
||||
```
|
||||
|
||||
### Route Validation Middleware
|
||||
|
||||
```typescript
|
||||
import { validateRequest } from '../middleware/validation';
|
||||
import { createFlyerSchema } from '../schemas/flyer.schemas';
|
||||
|
||||
app.post('/api/flyers', validateRequest(createFlyerSchema), async (req, res) => {
|
||||
// req.body is now type-safe and validated
|
||||
const flyer = await flyerService.createFlyer(req.body);
|
||||
return sendSuccess(res, flyer, 'Flyer created successfully', 201);
|
||||
});
|
||||
```
|
||||
|
||||
### Manual Validation
|
||||
|
||||
```typescript
|
||||
import { createFlyerSchema } from '../schemas/flyer.schemas';
|
||||
|
||||
export async function processFlyer(data: unknown): Promise<Flyer> {
|
||||
// Validate and parse input
|
||||
const validated = createFlyerSchema.parse(data);
|
||||
|
||||
// Type-safe from here on
|
||||
return flyerDb.createFlyer(validated);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
**ADR**: [ADR-048](../adr/0048-authentication-strategy.md)
|
||||
|
||||
### Protected Route with JWT
|
||||
|
||||
```typescript
|
||||
import { authenticateJWT } from '../middleware/auth';
|
||||
|
||||
app.get(
|
||||
'/api/profile',
|
||||
authenticateJWT, // Middleware adds req.user
|
||||
async (req, res) => {
|
||||
// req.user is guaranteed to exist
|
||||
const user = await userDb.getUserById(req.user.id);
|
||||
return sendSuccess(res, user);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Optional Authentication
|
||||
|
||||
```typescript
|
||||
import { optionalAuth } from '../middleware/auth';
|
||||
|
||||
app.get(
|
||||
'/api/flyers',
|
||||
optionalAuth, // req.user may or may not exist
|
||||
async (req, res) => {
|
||||
const flyers = req.user
|
||||
? await flyerDb.listFlyersForUser(req.user.id)
|
||||
: await flyerDb.listPublicFlyers();
|
||||
|
||||
return sendSuccess(res, flyers);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Generate JWT Token
|
||||
|
||||
```typescript
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { env } from '../config/env';
|
||||
|
||||
export function generateToken(user: User): string {
|
||||
return jwt.sign({ id: user.id, email: user.email }, env.JWT_SECRET, { expiresIn: '7d' });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Caching
|
||||
|
||||
**ADR**: [ADR-029](../adr/0029-redis-caching-strategy.md)
|
||||
|
||||
### Cache Pattern
|
||||
|
||||
```typescript
|
||||
import { cacheService } from '../services/cache.server';
|
||||
|
||||
export async function getFlyer(id: number): Promise<Flyer> {
|
||||
// Try cache first
|
||||
const cached = await cacheService.get<Flyer>(`flyer:${id}`);
|
||||
if (cached) return cached;
|
||||
|
||||
// Cache miss - fetch from database
|
||||
const flyer = await flyerDb.getFlyerById(id);
|
||||
|
||||
// Store in cache (1 hour TTL)
|
||||
await cacheService.set(`flyer:${id}`, flyer, 3600);
|
||||
|
||||
return flyer;
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Invalidation
|
||||
|
||||
```typescript
|
||||
export async function updateFlyer(id: number, data: UpdateFlyerInput): Promise<Flyer> {
|
||||
const flyer = await flyerDb.updateFlyer(id, data);
|
||||
|
||||
// Invalidate cache
|
||||
await cacheService.delete(`flyer:${id}`);
|
||||
await cacheService.invalidatePattern('flyers:list:*');
|
||||
|
||||
return flyer;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Background Jobs
|
||||
|
||||
**ADR**: [ADR-036](../adr/0036-background-job-processing-architecture.md)
|
||||
|
||||
### Queue Job
|
||||
|
||||
```typescript
|
||||
import { flyerProcessingQueue } from '../services/queues.server';
|
||||
|
||||
export async function enqueueFlyerProcessing(flyerId: number): Promise<void> {
|
||||
await flyerProcessingQueue.add(
|
||||
'process-flyer',
|
||||
{
|
||||
flyerId,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 2000,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Process Job
|
||||
|
||||
```typescript
|
||||
// src/services/workers.server.ts
|
||||
import { Worker } from 'bullmq';
|
||||
|
||||
const flyerWorker = new Worker(
|
||||
'flyer-processing',
|
||||
async (job) => {
|
||||
const { flyerId } = job.data;
|
||||
|
||||
try {
|
||||
// Process flyer
|
||||
const result = await aiService.extractFlyerData(flyerId);
|
||||
await flyerDb.updateFlyerWithData(flyerId, result);
|
||||
|
||||
// Update progress
|
||||
await job.updateProgress(100);
|
||||
|
||||
return { success: true, itemCount: result.items.length };
|
||||
} catch (error) {
|
||||
logger.error('Flyer processing failed', { flyerId, error });
|
||||
throw error; // Will retry automatically
|
||||
}
|
||||
},
|
||||
{
|
||||
connection: redisConnection,
|
||||
concurrency: 5,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [ADR Index](../adr/index.md) - All architecture decision records
|
||||
- [TESTING.md](TESTING.md) - Testing patterns
|
||||
- [DEBUGGING.md](DEBUGGING.md) - Debugging strategies
|
||||
- [Database Guide](../subagents/DATABASE-GUIDE.md) - Database patterns
|
||||
- [Coder Reference](../SUBAGENT-CODER-REFERENCE.md) - Quick reference for AI agents
|
||||
668
docs/development/DEBUGGING.md
Normal file
668
docs/development/DEBUGGING.md
Normal file
@@ -0,0 +1,668 @@
|
||||
# Debugging Guide
|
||||
|
||||
Common debugging strategies and troubleshooting patterns for Flyer Crawler.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Debugging Checklist](#quick-debugging-checklist)
|
||||
- [Container Issues](#container-issues)
|
||||
- [Database Issues](#database-issues)
|
||||
- [Test Failures](#test-failures)
|
||||
- [API Errors](#api-errors)
|
||||
- [Authentication Problems](#authentication-problems)
|
||||
- [Background Job Issues](#background-job-issues)
|
||||
- [Frontend Issues](#frontend-issues)
|
||||
- [Performance Problems](#performance-problems)
|
||||
- [Debugging Tools](#debugging-tools)
|
||||
|
||||
---
|
||||
|
||||
## Quick Debugging Checklist
|
||||
|
||||
When something breaks, check these first:
|
||||
|
||||
1. **Are containers running?**
|
||||
|
||||
```bash
|
||||
podman ps
|
||||
```
|
||||
|
||||
2. **Is the database accessible?**
|
||||
|
||||
```bash
|
||||
podman exec flyer-crawler-postgres pg_isready -U postgres
|
||||
```
|
||||
|
||||
3. **Are environment variables set?**
|
||||
|
||||
```bash
|
||||
# Check .env.local exists
|
||||
cat .env.local
|
||||
```
|
||||
|
||||
4. **Are there recent errors in logs?**
|
||||
|
||||
```bash
|
||||
# Application logs
|
||||
podman logs -f flyer-crawler-dev
|
||||
|
||||
# PM2 logs (production)
|
||||
pm2 logs flyer-crawler-api
|
||||
```
|
||||
|
||||
5. **Is Redis accessible?**
|
||||
```bash
|
||||
podman exec flyer-crawler-redis redis-cli ping
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Container Issues
|
||||
|
||||
### Container Won't Start
|
||||
|
||||
**Symptom**: `podman start` fails or container exits immediately
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# Check container status
|
||||
podman ps -a
|
||||
|
||||
# View container logs
|
||||
podman logs flyer-crawler-postgres
|
||||
podman logs flyer-crawler-redis
|
||||
podman logs flyer-crawler-dev
|
||||
|
||||
# Inspect container
|
||||
podman inspect flyer-crawler-dev
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- Port already in use
|
||||
- Insufficient resources
|
||||
- Configuration error
|
||||
|
||||
**Solutions**:
|
||||
|
||||
```bash
|
||||
# Check port usage
|
||||
netstat -an | findstr "5432"
|
||||
netstat -an | findstr "6379"
|
||||
|
||||
# Remove and recreate container
|
||||
podman stop flyer-crawler-postgres
|
||||
podman rm flyer-crawler-postgres
|
||||
# ... recreate with podman run ...
|
||||
```
|
||||
|
||||
### "Unable to connect to Podman socket"
|
||||
|
||||
**Symptom**: `Error: unable to connect to Podman socket`
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Start Podman machine
|
||||
podman machine start
|
||||
|
||||
# Verify it's running
|
||||
podman machine list
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
**Symptom**: `Error: port 5432 is already allocated`
|
||||
|
||||
**Solutions**:
|
||||
|
||||
**Option 1**: Stop conflicting service
|
||||
|
||||
```bash
|
||||
# Find process using port
|
||||
netstat -ano | findstr "5432"
|
||||
|
||||
# Stop the service or kill process
|
||||
```
|
||||
|
||||
**Option 2**: Use different port
|
||||
|
||||
```bash
|
||||
# Run container on different host port
|
||||
podman run -d --name flyer-crawler-postgres -p 5433:5432 ...
|
||||
|
||||
# Update .env.local
|
||||
DB_PORT=5433
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Issues
|
||||
|
||||
### Connection Refused
|
||||
|
||||
**Symptom**: `Error: connect ECONNREFUSED 127.0.0.1:5432`
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# 1. Check if PostgreSQL container is running
|
||||
podman ps | grep postgres
|
||||
|
||||
# 2. Check if PostgreSQL is ready
|
||||
podman exec flyer-crawler-postgres pg_isready -U postgres
|
||||
|
||||
# 3. Test connection
|
||||
podman exec flyer-crawler-postgres psql -U postgres -d flyer_crawler_dev -c "SELECT 1;"
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- Container not running
|
||||
- PostgreSQL still initializing
|
||||
- Wrong credentials in `.env.local`
|
||||
|
||||
**Solutions**:
|
||||
|
||||
```bash
|
||||
# Start container
|
||||
podman start flyer-crawler-postgres
|
||||
|
||||
# Wait for initialization (check logs)
|
||||
podman logs -f flyer-crawler-postgres
|
||||
|
||||
# Verify credentials match .env.local
|
||||
cat .env.local | grep DB_
|
||||
```
|
||||
|
||||
### Schema Out of Sync
|
||||
|
||||
**Symptom**: Tests fail with missing column or table errors
|
||||
|
||||
**Cause**: `master_schema_rollup.sql` not in sync with migrations
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Reset database with current schema
|
||||
podman exec -i flyer-crawler-postgres psql -U postgres -d flyer_crawler_dev < sql/drop_tables.sql
|
||||
podman exec -i flyer-crawler-postgres psql -U postgres -d flyer_crawler_dev < sql/master_schema_rollup.sql
|
||||
|
||||
# Verify schema
|
||||
podman exec flyer-crawler-postgres psql -U postgres -d flyer_crawler_dev -c "\dt"
|
||||
```
|
||||
|
||||
### Query Performance Issues
|
||||
|
||||
**Debug**:
|
||||
|
||||
```sql
|
||||
-- Enable query logging
|
||||
ALTER DATABASE flyer_crawler_dev SET log_statement = 'all';
|
||||
|
||||
-- Check slow queries
|
||||
SELECT query, mean_exec_time, calls
|
||||
FROM pg_stat_statements
|
||||
WHERE mean_exec_time > 100
|
||||
ORDER BY mean_exec_time DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- Analyze query plan
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM flyers WHERE store_id = 1;
|
||||
```
|
||||
|
||||
**Solutions**:
|
||||
|
||||
- Add missing indexes
|
||||
- Optimize WHERE clauses
|
||||
- Use connection pooling
|
||||
- See [ADR-034](../adr/0034-repository-layer-method-naming-conventions.md)
|
||||
|
||||
---
|
||||
|
||||
## Test Failures
|
||||
|
||||
### Tests Pass on Windows, Fail in Container
|
||||
|
||||
**Cause**: Platform-specific behavior (ADR-014)
|
||||
|
||||
**Rule**: Container results are authoritative. Windows results are unreliable.
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Always run tests in container
|
||||
podman exec -it flyer-crawler-dev npm test
|
||||
|
||||
# For specific test
|
||||
podman exec -it flyer-crawler-dev npm test -- --run src/path/to/test.test.ts
|
||||
```
|
||||
|
||||
### Integration Tests Fail
|
||||
|
||||
**Common Issues**:
|
||||
|
||||
**1. Vitest globalSetup Context Isolation**
|
||||
|
||||
**Symptom**: Mocks or spies don't work in integration tests
|
||||
|
||||
**Cause**: `globalSetup` runs in separate Node.js context
|
||||
|
||||
**Solutions**:
|
||||
|
||||
- Mark test as `.todo()` and document limitation
|
||||
- Create test-only API endpoints
|
||||
- Use Redis-based mock flags
|
||||
|
||||
See [CLAUDE.md#integration-test-issues](../../CLAUDE.md#integration-test-issues) for details.
|
||||
|
||||
**2. Cache Stale After Direct SQL**
|
||||
|
||||
**Symptom**: Test reads stale data after direct database insert
|
||||
|
||||
**Cause**: Cache not invalidated
|
||||
|
||||
**Solution**:
|
||||
|
||||
```typescript
|
||||
// After direct SQL insert
|
||||
await cacheService.invalidateFlyers();
|
||||
```
|
||||
|
||||
**3. Queue Interference**
|
||||
|
||||
**Symptom**: Cleanup worker processes test data before assertions
|
||||
|
||||
**Solution**:
|
||||
|
||||
```typescript
|
||||
const { cleanupQueue } = await import('../../services/queues.server');
|
||||
await cleanupQueue.drain();
|
||||
await cleanupQueue.pause();
|
||||
// ... test ...
|
||||
await cleanupQueue.resume();
|
||||
```
|
||||
|
||||
### Type Check Failures
|
||||
|
||||
**Symptom**: `npm run type-check` fails
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# Run type check in container
|
||||
podman exec -it flyer-crawler-dev npm run type-check
|
||||
|
||||
# Check specific file
|
||||
podman exec -it flyer-crawler-dev npx tsc --noEmit src/path/to/file.ts
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- Missing type definitions
|
||||
- Incorrect imports
|
||||
- Type mismatch in function calls
|
||||
|
||||
---
|
||||
|
||||
## API Errors
|
||||
|
||||
### 404 Not Found
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# Check route registration
|
||||
grep -r "router.get" src/routes/
|
||||
|
||||
# Check route path matches request
|
||||
# Verify middleware order
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- Route not registered in `server.ts`
|
||||
- Typo in route path
|
||||
- Middleware blocking request
|
||||
|
||||
### 500 Internal Server Error
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# Check application logs
|
||||
podman logs -f flyer-crawler-dev
|
||||
|
||||
# Check Bugsink for errors
|
||||
# Visit: http://localhost:8443 (dev) or https://bugsink.projectium.com (prod)
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- Unhandled exception
|
||||
- Database error
|
||||
- Missing environment variable
|
||||
|
||||
**Solution Pattern**:
|
||||
|
||||
```typescript
|
||||
// Always wrap route handlers
|
||||
app.get('/api/endpoint', async (req, res) => {
|
||||
try {
|
||||
const result = await service.doSomething();
|
||||
return sendSuccess(res, result);
|
||||
} catch (error) {
|
||||
return sendError(res, error); // Handles error types automatically
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# Check JWT token in request
|
||||
# Verify token is valid and not expired
|
||||
|
||||
# Test token decoding
|
||||
node -e "console.log(require('jsonwebtoken').decode('YOUR_TOKEN_HERE'))"
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- Token expired
|
||||
- Invalid token format
|
||||
- Missing Authorization header
|
||||
- Wrong JWT_SECRET
|
||||
|
||||
---
|
||||
|
||||
## Authentication Problems
|
||||
|
||||
### OAuth Not Working
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# 1. Verify OAuth credentials
|
||||
cat .env.local | grep GOOGLE_CLIENT
|
||||
|
||||
# 2. Check OAuth routes are registered
|
||||
grep -r "passport.authenticate" src/routes/
|
||||
|
||||
# 3. Verify redirect URI matches Google Console
|
||||
# Should be: http://localhost:3001/api/auth/google/callback
|
||||
```
|
||||
|
||||
**Common Issues**:
|
||||
|
||||
- Redirect URI mismatch in Google Console
|
||||
- OAuth not enabled (commented out in config)
|
||||
- Wrong client ID/secret
|
||||
|
||||
See [AUTHENTICATION.md](../architecture/AUTHENTICATION.md) for setup.
|
||||
|
||||
### JWT Token Invalid
|
||||
|
||||
**Debug**:
|
||||
|
||||
```typescript
|
||||
// Decode token to inspect
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const decoded = jwt.decode(token);
|
||||
console.log('Token payload:', decoded);
|
||||
console.log('Expired:', decoded.exp < Date.now() / 1000);
|
||||
```
|
||||
|
||||
**Solutions**:
|
||||
|
||||
- Regenerate token
|
||||
- Check JWT_SECRET matches between environments
|
||||
- Verify token hasn't expired
|
||||
|
||||
---
|
||||
|
||||
## Background Job Issues
|
||||
|
||||
### Jobs Not Processing
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# Check if worker is running
|
||||
pm2 list
|
||||
|
||||
# Check worker logs
|
||||
pm2 logs flyer-crawler-worker
|
||||
|
||||
# Check Redis connection
|
||||
podman exec flyer-crawler-redis redis-cli ping
|
||||
|
||||
# Check queue status
|
||||
node -e "
|
||||
const { flyerProcessingQueue } = require('./dist/services/queues.server.js');
|
||||
flyerProcessingQueue.getJobCounts().then(console.log);
|
||||
"
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- Worker not running
|
||||
- Redis connection lost
|
||||
- Queue paused
|
||||
- Job stuck in failed state
|
||||
|
||||
**Solutions**:
|
||||
|
||||
```bash
|
||||
# Restart worker
|
||||
pm2 restart flyer-crawler-worker
|
||||
|
||||
# Clear failed jobs
|
||||
node -e "
|
||||
const { flyerProcessingQueue } = require('./dist/services/queues.server.js');
|
||||
flyerProcessingQueue.clean(0, 1000, 'failed');
|
||||
"
|
||||
```
|
||||
|
||||
### Jobs Failing
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# Check failed jobs
|
||||
node -e "
|
||||
const { flyerProcessingQueue } = require('./dist/services/queues.server.js');
|
||||
flyerProcessingQueue.getFailed().then(jobs => {
|
||||
jobs.forEach(job => console.log(job.failedReason));
|
||||
});
|
||||
"
|
||||
|
||||
# Check worker logs for stack traces
|
||||
pm2 logs flyer-crawler-worker --lines 100
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- Gemini API errors
|
||||
- Database errors
|
||||
- Invalid job data
|
||||
|
||||
---
|
||||
|
||||
## Frontend Issues
|
||||
|
||||
### Hot Reload Not Working
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# Check Vite is running
|
||||
curl http://localhost:5173
|
||||
|
||||
# Check for port conflicts
|
||||
netstat -an | findstr "5173"
|
||||
```
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Restart dev server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### API Calls Failing (CORS)
|
||||
|
||||
**Symptom**: `CORS policy: No 'Access-Control-Allow-Origin' header`
|
||||
|
||||
**Debug**:
|
||||
|
||||
```typescript
|
||||
// Check CORS configuration in server.ts
|
||||
import cors from 'cors';
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: env.FRONTEND_URL, // Should match http://localhost:5173 in dev
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
**Solution**: Verify `FRONTEND_URL` in `.env.local` matches the frontend URL
|
||||
|
||||
---
|
||||
|
||||
## Performance Problems
|
||||
|
||||
### Slow API Responses
|
||||
|
||||
**Debug**:
|
||||
|
||||
```typescript
|
||||
// Add timing logs
|
||||
const start = Date.now();
|
||||
const result = await slowOperation();
|
||||
console.log(`Operation took ${Date.now() - start}ms`);
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- N+1 query problem
|
||||
- Missing database indexes
|
||||
- Large payload size
|
||||
- No caching
|
||||
|
||||
**Solutions**:
|
||||
|
||||
- Use JOINs instead of multiple queries
|
||||
- Add indexes: `CREATE INDEX idx_name ON table(column);`
|
||||
- Implement pagination
|
||||
- Add Redis caching
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
**Debug**:
|
||||
|
||||
```bash
|
||||
# Check PM2 memory usage
|
||||
pm2 monit
|
||||
|
||||
# Check container memory
|
||||
podman stats flyer-crawler-dev
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
|
||||
- Memory leak
|
||||
- Large in-memory cache
|
||||
- Unbounded array growth
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tools
|
||||
|
||||
### VS Code Debugger
|
||||
|
||||
**Launch Configuration** (`.vscode/launch.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Tests",
|
||||
"program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
|
||||
"args": ["--run", "${file}"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
```typescript
|
||||
import { logger } from './utils/logger';
|
||||
|
||||
// Structured logging
|
||||
logger.info('Processing flyer', { flyerId, userId });
|
||||
logger.error('Failed to process', { error, context });
|
||||
logger.debug('Cache hit', { key, ttl });
|
||||
```
|
||||
|
||||
### Database Query Logging
|
||||
|
||||
```typescript
|
||||
// In development, log all queries
|
||||
if (env.NODE_ENV === 'development') {
|
||||
pool.on('connect', () => {
|
||||
console.log('Database connected');
|
||||
});
|
||||
|
||||
// Log slow queries
|
||||
const originalQuery = pool.query.bind(pool);
|
||||
pool.query = async (...args) => {
|
||||
const start = Date.now();
|
||||
const result = await originalQuery(...args);
|
||||
const duration = Date.now() - start;
|
||||
if (duration > 100) {
|
||||
console.log(`Slow query (${duration}ms):`, args[0]);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Redis Debugging
|
||||
|
||||
```bash
|
||||
# Monitor Redis commands
|
||||
podman exec -it flyer-crawler-redis redis-cli monitor
|
||||
|
||||
# Check keys
|
||||
podman exec flyer-crawler-redis redis-cli keys "*"
|
||||
|
||||
# Get key value
|
||||
podman exec flyer-crawler-redis redis-cli get "flyer:123"
|
||||
|
||||
# Check cache stats
|
||||
podman exec flyer-crawler-redis redis-cli info stats
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [TESTING.md](TESTING.md) - Testing strategies
|
||||
- [CODE-PATTERNS.md](CODE-PATTERNS.md) - Common patterns
|
||||
- [MONITORING.md](../operations/MONITORING.md) - Production monitoring
|
||||
- [Bugsink Setup](../tools/BUGSINK-SETUP.md) - Error tracking
|
||||
- [DevOps Guide](../subagents/DEVOPS-GUIDE.md) - Container debugging
|
||||
@@ -187,6 +187,17 @@ const mockStoreWithLocations = createMockStoreWithLocations({
|
||||
});
|
||||
```
|
||||
|
||||
### Test Assets
|
||||
|
||||
Test images and other assets are located in `src/tests/assets/`:
|
||||
|
||||
| File | Purpose |
|
||||
| ---------------------- | ---------------------------------------------- |
|
||||
| `test-flyer-image.jpg` | Sample flyer image for upload/processing tests |
|
||||
| `test-flyer-icon.png` | Sample flyer icon (64x64) for thumbnail tests |
|
||||
|
||||
These images are copied to `public/flyer-images/` by the seed script (`npm run seed`) and served via NGINX at `/flyer-images/`.
|
||||
|
||||
## Known Integration Test Issues
|
||||
|
||||
See `CLAUDE.md` for documentation of common integration test issues and their solutions, including:
|
||||
|
||||
Reference in New Issue
Block a user