changemaker.lite/api/dist/modules/media/routes/video-tracking.routes.js

207 lines
7.0 KiB
JavaScript

"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