feat(phase-2): implement enhanced redirect tracking with database persistence

🚀 Core Features:
- Complete database-persisted redirect tracking system
- Enhanced hop analysis with timing, headers, and metadata
- Intelligent redirect type detection (301, 302, 307, 308, meta, JS, final)
- Automatic redirect loop detection and prevention
- Comprehensive status tracking (OK, ERROR, TIMEOUT, LOOP)
- Real-time latency measurement per hop

🔧 Technical Implementation:
- Production-grade RedirectTrackerService with Prisma integration
- Type-safe request/response handling with Zod validation
- Advanced rate limiting (200/hour authenticated, 50/hour anonymous)
- Flexible authentication (optional auth for broader access)
- Robust error handling and structured logging
- Comprehensive input validation and sanitization

🌐 API Endpoints:
- POST /api/v2/track - Enhanced tracking with database persistence
- GET /api/v2/track/:checkId - Retrieve specific check with full hop details
- GET /api/v2/projects/:projectId/checks - List project checks with pagination
- GET /api/v2/checks/recent - Recent checks for authenticated users
- POST /api/v2/track/bulk - Placeholder for Phase 6 bulk processing

📊 Enhanced Data Model:
- Persistent check records with complete metadata
- Detailed hop tracking with response headers and timing
- SSL scheme detection and protocol analysis
- Content-Type extraction and analysis
- Comprehensive redirect chain preservation

🔒 Security & Performance:
- User-based rate limiting for authenticated requests
- IP-based rate limiting for anonymous requests
- Configurable timeouts and hop limits (1-20 hops, 1-30s timeout)
- Request validation prevents malicious input
- Structured error responses for API consistency

🔄 Backward Compatibility:
- All existing endpoints preserved and functional
- Legacy response formats maintained exactly
- Zero breaking changes to existing integrations
- Enhanced features available only in v2 endpoints

📋 Database Schema:
- Checks table for persistent tracking records
- Hops table for detailed redirect chain analysis
- Foreign key relationships for data integrity
- Optimized indexes for performance queries

🧪 Quality Assurance:
- Comprehensive test suite for all endpoints
- Authentication flow testing
- Rate limiting verification
- Error handling validation
- Legacy compatibility verification

Ready for Phase 3: SSL/SEO/Security analysis integration
This commit is contained in:
Andrei
2025-08-18 07:47:39 +00:00
parent 459eda89fe
commit db03d5713d
5 changed files with 1335 additions and 2 deletions

View File

@@ -11,6 +11,7 @@ export const ApiResponseSchema = z.object({
data: z.any().optional(),
error: z.string().optional(),
message: z.string().optional(),
meta: z.record(z.any()).optional(),
});
export type ApiResponse<T = any> = {
@@ -19,6 +20,7 @@ export type ApiResponse<T = any> = {
data?: T;
error?: string;
message?: string;
meta?: Record<string, any>;
};
// Legacy redirect result (for backward compatibility)
@@ -38,7 +40,7 @@ export const LegacyRedirectSchema = z.object({
export type LegacyRedirect = z.infer<typeof LegacyRedirectSchema>;
// Track request schemas
// Legacy track request (v1)
export const TrackRequestSchema = z.object({
url: z.string().url(),
method: z.enum(['GET', 'HEAD', 'POST']).default('GET'),
@@ -47,7 +49,7 @@ export const TrackRequestSchema = z.object({
export type TrackRequest = z.infer<typeof TrackRequestSchema>;
// Track response schema
// Legacy track response (v1)
export const TrackResponseSchema = z.object({
url: z.string(),
method: z.string(),
@@ -58,3 +60,166 @@ export const TrackResponseSchema = z.object({
});
export type TrackResponse = z.infer<typeof TrackResponseSchema>;
// ============================================================================
// V2 ENHANCED TYPES
// ============================================================================
// Enhanced track request (v2)
export const TrackRequestV2Schema = z.object({
url: z.string().url(),
method: z.enum(['GET', 'POST', 'HEAD']).default('GET'),
userAgent: z.string().optional(),
headers: z.record(z.string()).optional(),
projectId: z.string().optional(),
followJS: z.boolean().default(false),
maxHops: z.number().min(1).max(20).default(10),
timeout: z.number().min(1000).max(30000).default(15000),
});
export type TrackRequestV2 = z.infer<typeof TrackRequestV2Schema>;
// Hop result (v2)
export const HopResultSchema = z.object({
hopIndex: z.number(),
url: z.string(),
scheme: z.string().optional(),
statusCode: z.number().optional(),
redirectType: z.enum(['HTTP_301', 'HTTP_302', 'HTTP_307', 'HTTP_308', 'META_REFRESH', 'JS', 'FINAL', 'OTHER']),
latencyMs: z.number().optional(),
contentType: z.string().optional(),
reason: z.string().optional(),
responseHeaders: z.record(z.string()),
});
export type HopResult = z.infer<typeof HopResultSchema>;
// Check result (v2)
export const CheckResultSchema = z.object({
id: z.string(),
inputUrl: z.string(),
method: z.string(),
status: z.enum(['OK', 'ERROR', 'TIMEOUT', 'LOOP']),
finalUrl: z.string().optional(),
totalTimeMs: z.number(),
startedAt: z.date(),
finishedAt: z.date(),
hops: z.array(HopResultSchema),
redirectCount: z.number(),
loopDetected: z.boolean().optional(),
error: z.string().optional(),
});
export type CheckResult = z.infer<typeof CheckResultSchema>;
// Enhanced track response (v2)
export const TrackResponseV2Schema = z.object({
success: z.boolean(),
status: z.number(),
data: z.object({
check: CheckResultSchema,
url: z.string(),
method: z.string(),
redirectCount: z.number(),
finalUrl: z.string().optional(),
finalStatusCode: z.number().optional(),
}),
meta: z.object({
version: z.literal('v2'),
enhanced: z.boolean(),
persisted: z.boolean(),
checkId: z.string(),
}),
});
export type TrackResponseV2 = z.infer<typeof TrackResponseV2Schema>;
// ============================================================================
// AUTHENTICATION TYPES
// ============================================================================
export const AuthUserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
memberships: z.array(z.object({
orgId: z.string(),
role: z.string(),
organization: z.object({
name: z.string(),
plan: z.string(),
}),
})),
});
export type AuthUser = z.infer<typeof AuthUserSchema>;
export const LoginRequestSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export type LoginRequest = z.infer<typeof LoginRequestSchema>;
export const RegisterRequestSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
password: z.string().min(8),
organizationName: z.string().min(2).optional(),
});
export type RegisterRequest = z.infer<typeof RegisterRequestSchema>;
export const AuthResponseSchema = z.object({
success: z.boolean(),
status: z.number(),
data: z.object({
user: AuthUserSchema,
token: z.string().optional(),
}),
message: z.string().optional(),
});
export type AuthResponse = z.infer<typeof AuthResponseSchema>;
// ============================================================================
// PROJECT & ORGANIZATION TYPES
// ============================================================================
export const ProjectSchema = z.object({
id: z.string(),
name: z.string(),
orgId: z.string(),
settingsJson: z.record(z.any()),
createdAt: z.date(),
});
export type Project = z.infer<typeof ProjectSchema>;
export const OrganizationSchema = z.object({
id: z.string(),
name: z.string(),
plan: z.string(),
createdAt: z.date(),
});
export type Organization = z.infer<typeof OrganizationSchema>;
// ============================================================================
// UTILITY TYPES
// ============================================================================
export type CheckStatus = 'OK' | 'ERROR' | 'TIMEOUT' | 'LOOP';
export type RedirectType = 'HTTP_301' | 'HTTP_302' | 'HTTP_307' | 'HTTP_308' | 'META_REFRESH' | 'JS' | 'FINAL' | 'OTHER';
export type UserRole = 'OWNER' | 'ADMIN' | 'MEMBER';
// Error response
export const ErrorResponseSchema = z.object({
success: z.literal(false),
error: z.string(),
message: z.string(),
status: z.number(),
details: z.any().optional(),
});
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;