"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.videoTrackingRoutes = videoTrackingRoutes; const auth_1 = require("../middleware/auth"); const video_analytics_service_1 = require("../services/video-analytics.service"); const logger_1 = require("../../../utils/logger"); const zod_1 = require("zod"); // Rate limiting: 100 requests per minute per IP for tracking const trackingRateLimit = { max: 100, timeWindow: '1 minute', }; // Validation schemas const recordViewSchema = zod_1.z.object({ videoId: zod_1.z.number(), referer: zod_1.z.string().optional(), }); const recordEventSchema = zod_1.z.object({ videoId: zod_1.z.number(), viewId: zod_1.z.number().optional(), eventType: zod_1.z.enum(['play', 'pause', 'seek', 'complete']), timestamp: zod_1.z.number().min(0), }); const updateWatchTimeSchema = zod_1.z.object({ viewId: zod_1.z.number(), watchTimeSeconds: zod_1.z.number().min(0), }); async function videoTrackingRoutes(fastify) { /** * POST /track/view * Record a new video view (called when video starts loading) * Public endpoint - no auth required, but optionally uses auth if available */ fastify.post('/view', { preHandler: auth_1.optionalAuth, config: { rateLimit: trackingRateLimit, }, }, async (request, reply) => { const { videoId, referer } = request.body; // Validate input try { recordViewSchema.parse(request.body); } catch (error) { return reply.code(400).send({ message: 'Invalid input', error }); } try { // Get user ID if authenticated const userId = request.user?.id; // Get IP address and user agent from request const ipAddress = request.ip; const userAgent = request.headers['user-agent']; // Record the view const viewId = await video_analytics_service_1.videoAnalyticsService.recordView({ videoId, userId, ipAddress, userAgent, referer, }); return { success: true, viewId, }; } catch (error) { logger_1.logger.error('Failed to record view', { error, videoId }); // Don't fail the request - we don't want analytics failures to break playback return { success: false, viewId: null, }; } }); /** * POST /track/event * Record a video event (play, pause, seek, complete) * Public endpoint - no auth required */ fastify.post('/event', { config: { rateLimit: trackingRateLimit, }, }, async (request, reply) => { const { videoId, viewId, eventType, timestamp } = request.body; // Validate input try { recordEventSchema.parse(request.body); } catch (error) { return reply.code(400).send({ message: 'Invalid input', error }); } try { await video_analytics_service_1.videoAnalyticsService.recordEvent({ videoId, viewId, eventType, timestamp, }); return { success: true, }; } catch (error) { logger_1.logger.error('Failed to record event', { error, videoId, eventType }); // Don't fail the request - analytics failures shouldn't break playback return { success: false, }; } }); /** * POST /track/heartbeat * Update watch time for a view (called every 10 seconds during playback) * Public endpoint - no auth required */ fastify.post('/heartbeat', { config: { rateLimit: { max: 200, // Higher limit for heartbeats (every 10s) timeWindow: '1 minute', }, }, }, async (request, reply) => { const { viewId, watchTimeSeconds } = request.body; // Validate input try { updateWatchTimeSchema.parse(request.body); } catch (error) { return reply.code(400).send({ message: 'Invalid input', error }); } try { await video_analytics_service_1.videoAnalyticsService.updateWatchTime(viewId, watchTimeSeconds); return { success: true, }; } catch (error) { logger_1.logger.error('Failed to update watch time', { error, viewId }); // Don't fail the request return { success: false, }; } }); /** * POST /track/batch * Batch record multiple events (useful for reducing requests) * Public endpoint - no auth required */ fastify.post('/batch', { config: { rateLimit: { max: 50, timeWindow: '1 minute', }, }, }, async (request, reply) => { const { events } = request.body; if (!Array.isArray(events) || events.length === 0) { return reply.code(400).send({ message: 'Events array is required' }); } if (events.length > 50) { return reply.code(400).send({ message: 'Maximum 50 events per batch' }); } try { const results = await Promise.allSettled(events.map(async (event) => { switch (event.type) { case 'view': return video_analytics_service_1.videoAnalyticsService.recordView(event.data); case 'event': return video_analytics_service_1.videoAnalyticsService.recordEvent(event.data); case 'heartbeat': return video_analytics_service_1.videoAnalyticsService.updateWatchTime(event.data.viewId, event.data.watchTimeSeconds); default: throw new Error(`Unknown event type: ${event.type}`); } })); const successful = results.filter((r) => r.status === 'fulfilled').length; const failed = results.filter((r) => r.status === 'rejected').length; return { success: true, total: events.length, successful, failed, }; } catch (error) { logger_1.logger.error('Failed to process batch tracking', { error }); return reply.code(500).send({ message: 'Failed to process batch' }); } }); /** * GET /track/health * Health check for tracking endpoints */ fastify.get('/health', async (request, reply) => { return { status: 'ok', service: 'video-tracking', timestamp: new Date().toISOString(), }; }); } //# sourceMappingURL=video-tracking.routes.js.map