2024-11-13T08:09:50.706Z [ERROR]: Get Analytics Error: Cannot read properties of null (reading 'toFixed')
"""
const { z } = require('zod');
const mongoose = require('mongoose');
const User = require('../models/User');
const Log = require('../models/Log');
const monitorService = require('../services/monitorService');
const logger = require('../utils/logger');

/**
 * Zod Schemas for Validation
 */

// Schema for updating user settings
const updateSettingsSchema = z.object({
    listOfUrls: z.array(z.string().url()).optional(),
    timePeriod: z.enum(['30s', '1m', '3m', '5m', '10m', '1h']).optional(),
    emailSend: z.boolean().optional(),
});

// Schema for getting user logs (pagination)
const getLogsQuerySchema = z.object({
    page: z
        .string()
        .optional()
        .refine((val) => !val || /^\d+$/.test(val), {
            message: 'page must be a positive integer',
        })
        .default('1'),
    limit: z
        .string()
        .optional()
        .refine((val) => !val || (/^\d+$/.test(val) && parseInt(val) <= 1000), {
            message: 'limit must be a positive integer not exceeding 1000',
        })
        .default('20'),
});

/**
 * Zod schema for analytics parameters
 */
const analyticsSchema = z.object({
    startDate: z.string().optional().refine((date) => !date || !isNaN(Date.parse(date)), {
        message: 'startDate must be a valid ISO date string',
    }),
    endDate: z.string().optional().refine((date) => !date || !isNaN(Date.parse(date)), {
        message: 'endDate must be a valid ISO date string',
    }),
    urls: z.string().optional().refine((urls) => !urls || urls.split(',').every((url) => {
        try { new URL(url.trim()); return true; } catch { return false; }
    }), { message: 'urls must be a comma-separated list of valid URLs' }),
    statusCodes: z.string().optional().refine((codes) => !codes || codes.split(',').every(code => /^\d+$/.test(code.trim())), {
        message: 'statusCodes must be a comma-separated list of numbers',
    }),
    minResponseTime: z.string().optional().refine(rt => !rt || /^\d+$/.test(rt), {
        message: 'minResponseTime must be a number representing milliseconds',
    }),
    maxResponseTime: z.string().optional().refine(rt => !rt || /^\d+$/.test(rt), {
        message: 'maxResponseTime must be a number representing milliseconds',
    }),
    groupBy: z.enum(['hour', 'day', 'week', 'month']).optional().default('day'),
    page: z.string().optional().refine((val) => !val || /^\d+$/.test(val), {
        message: 'page must be a positive integer',
    }).default('1'),
    limit: z.string().optional().refine((val) => !val || (/^\d+$/.test(val) && parseInt(val) <= 1000), {
        message: 'limit must be an integer between 1 and 1000',
    }).default('100'),
});

/**
 * Controller Methods
 */

/**
 * Update User Settings
 * PATCH /api/user/settings
 */
exports.updateSettings = async (req, res) => {
    try {
        // Validate input
        const parsedData = updateSettingsSchema.parse(req.body);

        // Update user
        const updatedUser = await User.findByIdAndUpdate(
            req.user.id,
            { $set: parsedData },
            { new: true, runValidators: true, context: 'query' }
        );

        if (!updatedUser) return res.status(404).json({ message: 'User not found' });

        // **Check if user is verified before restarting monitoring**
        if (updatedUser.verified) {
            try {
                monitorService.restartMonitoring(updatedUser);
            } catch (monitorError) {
                // Handle monitoring errors, possibly due to unverified status
                logger.error('Restart Monitoring Error:', monitorError);
                return res.status(400).json({ message: monitorError.message });
            }
        } else {
            logger.warn(`User ${updatedUser.username} is not verified. Monitoring not started.`);
            return res.status(403).json({ message: 'Email not verified. Please verify your email to start monitoring.' });
        }

        res.status(200).json({ message: 'Settings updated successfully', user: updatedUser });
    } catch (error) {
        if (error instanceof z.ZodError) {
            return res.status(400).json({ errors: error.errors });
        }
        // Handle duplicate key error (e.g., email or username already exists)
        if (error.code === 11000) {
            const field = Object.keys(error.keyValue)[0];
            return res.status(400).json({ message: `${field.charAt(0).toUpperCase() + field.slice(1)} already exists` });
        }
        logger.error('Update Settings Error:', error);
        res.status(500).json({ message: 'Server Error' });
    }
};

/**
 * Get User Logs
 * GET /api/user/logs
 */
exports.getUserLogs = async (req, res) => {
    try {
        // Validate query parameters
        const parsedQuery = getLogsQuerySchema.parse(req.query);
        const page = parseInt(parsedQuery.page);
        const limit = parseInt(parsedQuery.limit);
        const skip = (page - 1) * limit;

        // Fetch logs from the database, sorted by most recent
        const logs = await Log.find({ user: req.user.id })
                              .sort({ checkedAt: -1 })
                              .limit(limit)
                              .skip(skip)
                              .exec();

        // Get total count for pagination
        const totalLogs = await Log.countDocuments({ user: req.user.id });

        res.status(200).json({
            totalPages: Math.ceil(totalLogs / limit),
            currentPage: page,
            logs,
        });
    } catch (error) {
        if (error instanceof z.ZodError) {
            return res.status(400).json({ errors: error.errors });
        }
        logger.error('Get User Logs Error:', error);
        res.status(500).json({ message: 'Server Error' });
    }
};

/**
 * Start Monitoring
 * POST /api/user/monitor/start
 */
exports.startMonitoring = async (req, res) => {
    try {
        const user = await User.findById(req.user.id);

        if (!user) {
            return res.status(404).json({ message: 'User not found' });
        }

        if (!user.verified) {
            return res.status(403).json({ message: 'Email not verified. Please verify your email to start monitoring.' });
        }

        if (!user.listOfUrls || user.listOfUrls.length === 0) {
            return res.status(400).json({ message: 'No URLs configured for monitoring. Please add URLs to your settings.' });
        }

        // Check if monitoring is already active
        const isMonitoringActive = monitorService.isMonitoringActive(user._id.toString());
        if (isMonitoringActive) {
            return res.status(400).json({ message: 'Monitoring is already active for your account.' });
        }

        // Start monitoring
        monitorService.startMonitoring(user);

        res.status(200).json({ message: 'Monitoring started successfully.' });
    } catch (error) {
        logger.error('Start Monitoring Error:', error);
        res.status(500).json({ message: 'Internal server error' });
    }
};

/**
 * Stop Monitoring
 * POST /api/user/monitor/stop
 */
exports.stopMonitoring = async (req, res) => {
    try {
        const user = await User.findById(req.user.id);

        if (!user) {
            return res.status(404).json({ message: 'User not found' });
        }

        // Check if monitoring is active
        const isMonitoringActive = monitorService.isMonitoringActive(user._id.toString());
        if (!isMonitoringActive) {
            return res.status(400).json({ message: 'Monitoring is not active for your account.' });
        }

        // Stop monitoring
        monitorService.stopMonitoring(user);

        res.status(200).json({ message: 'Monitoring stopped successfully.' });
    } catch (error) {
        logger.error('Stop Monitoring Error:', error);
        res.status(500).json({ message: 'Internal server error' });
    }
};

/**
 * Get Analytics
 * GET /api/user/analytics
 */
exports.getAnalytics = async (req, res) => {
    try {
        const userId = req.user.id;

        // Validate and parse query parameters
        const parsed = analyticsSchema.parse(req.query);
        const { startDate, endDate, urls, statusCodes, minResponseTime, maxResponseTime, groupBy, page, limit } = parsed;

        // Build the match criteria for MongoDB aggregation
        const matchCriteria = { user: new mongoose.Types.ObjectId(userId) };

        if (startDate || endDate) {
            matchCriteria.checkedAt = {};
            if (startDate) {
                matchCriteria.checkedAt.$gte = new Date(startDate);
            }
            if (endDate) {
                matchCriteria.checkedAt.$lte = new Date(endDate);
            }
        }

        if (urls) {
            const urlsArray = urls.split(',').map(url => url.trim());
            matchCriteria.url = { $in: urlsArray };
        }

        if (statusCodes) {
            const statusCodesArray = statusCodes.split(',').map(code => parseInt(code.trim()));
            matchCriteria.statusCode = { $in: statusCodesArray };
        }

        if (minResponseTime || maxResponseTime) {
            matchCriteria.responseTime = {};
            if (minResponseTime) {
                matchCriteria.responseTime.$gte = parseInt(minResponseTime);
            }
            if (maxResponseTime) {
                matchCriteria.responseTime.$lte = parseInt(maxResponseTime);
            }
        }

        /**
         * Aggregation Pipelines
         */

        // Total Checks
        const totalChecksPromise = Log.countDocuments(matchCriteria).exec();

        // Successful Checks (2xx)
        const successfulChecksPromise = Log.countDocuments({
            ...matchCriteria,
            statusCode: { $gte: 200, $lt: 300 },
        }).exec();

        // Failed Checks (4xx and 5xx)
        const failedChecksPromise = Log.countDocuments({
            ...matchCriteria,
            statusCode: { $gte: 400 },
        }).exec();

        // Status Code Distribution
        const statusCodeDistributionPromise = Log.aggregate([
            { $match: matchCriteria },
            {
                $group: {
                    _id: '$statusCode',
                    count: { $sum: 1 },
                },
            },
            { $sort: { count: -1 } },
        ]).exec();

        // Uptime Percentage per URL
        const uptimePerUrlPromise = Log.aggregate([
            { $match: { ...matchCriteria, statusCode: { $gte: 200, $lt: 300 } } },
            {
                $group: {
                    _id: '$url',
                    successCount: { $sum: 1 },
                },
            },
            {
                $lookup: {
                    from: 'logs',
                    let: { url: '$_id' },
                    pipeline: [
                        { $match: { 
                            $expr: { $eq: ['$url', '$$url'] },
                            ...(startDate || endDate ? {
                                ...(startDate ? { $gte: ['$checkedAt', new Date(startDate)] } : {}),
                                ...(endDate ? { $lte: ['$checkedAt', new Date(endDate)] } : {}),
                            } : {})
                        } },
                        { $count: 'totalCount' },
                    ],
                    as: 'total',
                },
            },
            {
                $addFields: {
                    totalCount: { $arrayElemAt: ['$total.totalCount', 0] },
                },
            },
            {
                $project: {
                    _id: 1,
                    successCount: 1,
                    totalCount: { $ifNull: ['$totalCount', 0] },
                    uptimePercentage: {
                        $cond: [
                            { $eq: ['$totalCount', 0] },
                            0,
                            { $multiply: [{ $divide: ['$successCount', '$totalCount'] }, 100] },
                        ],
                    },
                },
            },
        ]).exec();

        // Checks Over Time with Average Response Time
        const checksOverTimePromise = Log.aggregate([
            { $match: matchCriteria },
            {
                $group: {
                    _id: {
                        year: { $year: '$checkedAt' },
                        month: { $month: '$checkedAt' },
                        week: { $week: '$checkedAt' },
                        day: { $dayOfMonth: '$checkedAt' },
                        hour: { $hour: '$checkedAt' },
                    },
                    count: { $sum: 1 },
                    averageResponseTime: { $avg: '$responseTime' },
                },
            },
            {
                $project: {
                    _id: 0,
                    period: getPeriodExpression(groupBy),
                    count: 1,
                    averageResponseTime: 1,
                },
            },
            {
                $group: {
                    _id: '$period',
                    total: { $sum: '$count' },
                    averageResponseTime: { $avg: '$averageResponseTime' },
                },
            },
            { $sort: { '_id': 1 } },
        ]).exec();

        // Response Time Distribution
        const responseTimeDistributionPromise = Log.aggregate([
            { $match: matchCriteria },
            {
                $bucket: {
                    groupBy: '$responseTime',
                    boundaries: [0, 100, 300, 500, 1000, 3000, 5000, Infinity],
                    default: '5000+',
                    output: {
                        count: { $sum: 1 },
                        averageResponseTime: { $avg: '$responseTime' },
                    },
                },
            },
            { $sort: { _id: 1 } },
        ]).exec();

        // Top Slowest URLs
        const topSlowestUrlsPromise = Log.aggregate([
            { $match: matchCriteria },
            {
                $group: {
                    _id: '$url',
                    averageResponseTime: { $avg: '$responseTime' },
                    totalChecks: { $sum: 1 },
                },
            },
            { $sort: { averageResponseTime: -1 } },
            { $limit: 10 },
        ]).exec();

        // Recent Issues
        const recentIssuesPromise = Log.find({
            ...matchCriteria,
            statusCode: { $gte: 400 }, // Client and Server errors
        })
            .sort({ checkedAt: -1 })
            .limit(10)
            .select('url statusCode checkedAt screenshotUrl responseTime')
            .exec();

        // Execute all aggregation promises concurrently
        const [
            totalChecks,
            successfulChecks,
            failedChecks,
            statusCodeDistribution,
            uptimePerUrl,
            checksOverTime,
            responseTimeDistribution,
            topSlowestUrls,
            recentIssues,
        ] = await Promise.all([
            totalChecksPromise,
            successfulChecksPromise,
            failedChecksPromise,
            statusCodeDistributionPromise,
            uptimePerUrlPromise,
            checksOverTimePromise,
            responseTimeDistributionPromise,
            topSlowestUrlsPromise,
            recentIssuesPromise,
        ]);

        // Calculate success and failure percentages
        const successPercentage = totalChecks > 0 ? ((successfulChecks / totalChecks) * 100).toFixed(2) : '0';
        const failurePercentage = totalChecks > 0 ? ((failedChecks / totalChecks) * 100).toFixed(2) : '0';

        // Structure the analytics response
        const analytics = {
            totalChecks,
            successfulChecks,
            successPercentage: `${successPercentage}%`,
            failedChecks,
            failurePercentage: `${failurePercentage}%`,
            statusCodeDistribution: statusCodeDistribution.map(item => ({
                statusCode: item._id,
                count: item.count,
                percentage: totalChecks > 0 ? `${((item.count / totalChecks) * 100).toFixed(2)}%` : '0%',
            })),
            uptimePerUrl: uptimePerUrl.map(item => ({
                url: item._id,
                successCount: item.successCount,
                totalCount: item.totalCount,
                uptimePercentage: `${item.uptimePercentage.toFixed(2)}%`,
            })),
            checksOverTime: checksOverTime.map(item => ({
                period: formatPeriod(item._id, groupBy),
                count: item.total,
                averageResponseTime: `${item.averageResponseTime.toFixed(2)}ms`,
            })),
            responseTimeDistribution: responseTimeDistribution.map(item => ({
                range: formatResponseTimeRange(item._id),
                count: item.count,
                averageResponseTime: item.averageResponseTime ? `${item.averageResponseTime.toFixed(2)}ms` : 'N/A',
            })),
            topSlowestUrls: topSlowestUrls.map(item => ({
                url: item._id,
                averageResponseTime: `${item.averageResponseTime.toFixed(2)}ms`,
                totalChecks: item.totalChecks,
            })),
            recentIssues,
        };

        res.status(200).json({ analytics });
    } catch (error) {
        if (error instanceof z.ZodError) {
            return res.status(400).json({
                message: 'Invalid query parameters',
                errors: error.errors.map((err) => ({
                    field: err.path.join('.'),
                    message: err.message,
                })),
            });
        }

        logger.error('Get Analytics Error:', error);
        res.status(500).json({ message: 'Internal server error' });
    }
};

/**
 * Helper functions for formatting
 */
const formatPeriod = (periodObj, groupBy) => {
    const { year, month, week, day, hour } = periodObj;
    switch (groupBy) {
        case 'hour': return `${month}/${day}/${year} ${hour}:00`;
        case 'day': return `${month}/${day}/${year}`;
        case 'week': return `Week ${week}, ${year}`;
        case 'month': return `${month}/${year}`;
        default: return `${month}/${day}/${year}`;
    }
};

const formatResponseTimeRange = (range) => {
    if (range === 'Infinity') return '5000+ms';
    return `${range[0]}-${range[1]}ms`;
};

const getPeriodExpression = (groupBy) => ({
    hour: { year: { $year: '$checkedAt' }, month: { $month: '$checkedAt' }, day: { $dayOfMonth: '$checkedAt' }, hour: { $hour: '$checkedAt' } },
    day: { year: { $year: '$checkedAt' }, month: { $month: '$checkedAt' }, day: { $dayOfMonth: '$checkedAt' } },
    week: { year: { $year: '$checkedAt' }, week: { $week: '$checkedAt' } },
    month: { year: { $year: '$checkedAt' }, month: { $month: '$checkedAt' } }
}[groupBy]);


"""
// services/monitorService.js

const axios = require('axios');
const cron = require('node-cron');
const Log = require('../models/Log');
const screenshotService = require('./screenshotService');
const emailService = require('./emailService');
const logger = require('../utils/logger');

// Helper: Convert time period to cron expression
const timePeriodToCron = (timePeriod) => {
    switch (timePeriod) {
        case '1m':
            return '*/1 * * * *';
        case '3m':
            return '*/3 * * * *';
        case '5m':
            return '*/5 * * * *';
        case '10m':
            return '*/10 * * * *';
        case '1h':
            return '0 * * * *';
        default:
            return '*/1 * * * *'; // Default to every minute
    }
};

// In-memory storage for cron jobs and intervals
const userJobs = new Map();
const userIntervals = new Map();

// In-memory storage for notification timers
// Structure: { userId: { url: timeoutObject } }
const notificationTimers = new Map();

/**
 * Function to perform checks on user's URLs.
 * Logs all status codes, handles notifications for non-2xx responses,
 * and captures screenshots for error notifications.
 *
 * @param {Object} user - The user object containing monitoring details.
 */
const performChecks = async (user) => {
    for (const url of user.listOfUrls) {
        let startTime, endTime, responseTime;
        try {
            startTime = Date.now();
            const response = await axios.get(url, { timeout: 10000 });
            endTime = Date.now();
            responseTime = endTime - startTime;

            const statusCode = response.status;

            // Log successful check with response time
            await logStatus(user._id, url, statusCode, "None", responseTime);

            if (statusCode < 200 || statusCode >= 300) {
                // Handle non-2xx status codes
                await handleFailure(user, url, statusCode);
            } else {
                // Clear any existing notification timer on success
                clearNotificationTimer(user._id.toString(), url);
            }
        } catch (error) {
            endTime = Date.now();
            responseTime = endTime - startTime;

            let statusCode = error.response ? error.response.status : 0;
            let screenshotUrl = 'None';

            try {
                screenshotUrl = await screenshotService.takeScreenshot(url);
                logger.info(`Screenshot captured for ${url}`);
            } catch (screenshotError) {
                logger.error(`Failed to take screenshot for ${url}: ${screenshotError.message}`);
            }

            // Log failed check with response time and screenshot
            await logStatus(user._id, url, statusCode, screenshotUrl, responseTime);

            if (!isNotificationPending(user._id.toString(), url)) {
                await emailService.sendErrorNotification(user.email, url, statusCode, screenshotUrl);
                setNotificationTimer(user._id.toString(), url, 10 * 60 * 1000); // 10 minutes
            }
        }
    }
};

/**
 * Handles failed checks by sending notifications and logging.
 */
const handleFailure = async (user, url, statusCode) => {
    logger.warn(`URL check failed for ${url} with status code ${statusCode}`);

    if (isNotificationPending(user._id.toString(), url)) {
        logger.info(`Notification already pending for ${url}. Skipping email.`);
        return;
    }

    try {
        const screenshotUrl = await screenshotService.takeScreenshot(url);
        await emailService.sendErrorNotification(user.email, url, statusCode, screenshotUrl);
        await logStatus(user._id, url, statusCode, screenshotUrl);
    } catch (error) {
        logger.error(`Failed to handle failure for ${url}: ${error.message}`);
    }

    setNotificationTimer(user._id.toString(), url, 10 * 60 * 1000); // 10 minutes
};

/**
 * Logs the status of the URL check to the database.
 *
 * @param {ObjectId} userId - The ID of the user.
 * @param {string} url - The URL that was checked.
 * @param {number} statusCode - The HTTP status code returned.
 */
const logStatus = async (userId, url, statusCode, screenshotUrl, responseTime = 0) => {
    try {
        const log = new Log({
            user: userId,
            url,
            statusCode,
            screenshotUrl,
            checkedAt: new Date(),
            responseTime,
        });
        await log.save();
        logger.info(`Logged status for ${url} with status code ${statusCode}, response time: ${responseTime}ms, and screenshot URL: ${screenshotUrl}`);
    } catch (logError) {
        logger.error(`Failed to log status for ${url}: ${logError.message}`);
    }
};

// Function to start monitoring for a user
const startMonitoring = (user) => {
    if (!user.verified) {
        logger.warn(`Attempted to start monitoring for unverified user: ${user.username}`);
        throw new Error('Email not verified. Please verify your email before starting monitoring.');
    }

    if (!user.listOfUrls || user.listOfUrls.length === 0) return;

    // Clear existing jobs or intervals to prevent duplicates
    stopMonitoring(user);

    if (user.timePeriod === '30s') {
        // node-cron doesn't support seconds intervals, use setInterval
        const interval = setInterval(() => {
            performChecks(user);
        }, 30000); // 30 seconds

        userIntervals.set(user._id.toString(), interval);
        logger.info(`Started 30s interval monitoring for user ${user.username}`);
    } else {
        const cronExpression = timePeriodToCron(user.timePeriod);
        if (!cronExpression) return;

        const job = cron.schedule(cronExpression, () => {
            performChecks(user);
        }, {
            scheduled: true,
            timezone: "UTC",
        });

        userJobs.set(user._id.toString(), job);
        logger.info(`Started cron job (${cronExpression}) monitoring for user ${user.username}`);
    }
};

// Function to stop monitoring for a user
const stopMonitoring = (user) => {
    const userId = user._id.toString();

    if (userJobs.has(userId)) {
        const job = userJobs.get(userId);
        job.stop();
        userJobs.delete(userId);
        logger.info(`Stopped cron job monitoring for user ${user.username}`);
    }

    if (userIntervals.has(userId)) {
        clearInterval(userIntervals.get(userId));
        userIntervals.delete(userId);
        logger.info(`Stopped interval monitoring for user ${user.username}`);
    }

    // Also clear any pending notification timers
    if (notificationTimers.has(userId)) {
        Object.values(notificationTimers.get(userId)).forEach(timeout => clearTimeout(timeout));
        notificationTimers.delete(userId);
        logger.info(`Cleared notification timers for user ${user.username}`);
    }
};

// Function to restart monitoring (used when settings are updated)
const restartMonitoring = (user) => {
    if (!user.verified) {
        logger.warn(`Attempted to restart monitoring for unverified user: ${user.username}`);
        throw new Error('Email not verified. Please verify your email before restarting monitoring.');
    }
    startMonitoring(user);
};

// Function to check if monitoring is active for a user
const isMonitoringActive = (userId) => {
    return userJobs.has(userId) || userIntervals.has(userId);
};

// Function to check if a notification is pending for a URL
const isNotificationPending = (userId, url) => {
    if (!notificationTimers.has(userId)) return false;
    const userTimers = notificationTimers.get(userId);
    return userTimers.hasOwnProperty(url);
};

// Function to set a notification timer for a URL
const setNotificationTimer = (userId, url, timeout) => {
    if (!notificationTimers.has(userId)) {
        notificationTimers.set(userId, {});
    }
    const userTimers = notificationTimers.get(userId);
    userTimers[url] = setTimeout(() => {
        // Timer expired, remove the pending notification
        if (notificationTimers.has(userId)) {
            delete notificationTimers.get(userId)[url];
            logger.info(`Notification timer expired for ${url}`);
            // Optionally, notify the user that monitoring is active again
            // Example:
            // emailService.sendRecoveryNotification(user.email, url);
        }
    }, timeout);
};

// Function to clear notification timer (called when URL recovers)
const clearNotificationTimer = (userId, url) => {
    if (!notificationTimers.has(userId)) return;
    const userTimers = notificationTimers.get(userId);
    if (userTimers.hasOwnProperty(url)) {
        clearTimeout(userTimers[url]);
        delete userTimers[url];
        logger.info(`Cleared notification timer for ${url}`);
    }
};

// Initialize monitoring for existing users on server start
const initializeMonitoring = async () => {
    const User = require('../models/User'); // Avoid circular dependency
    try {
        const users = await User.find({ verified: true }); // Only initialize for verified users
        users.forEach(user => {
            startMonitoring(user);
        });
        logger.info('Initialized monitoring for existing verified users');
    } catch (error) {
        logger.error('Error initializing monitoring:', error);
    }
};

module.exports = {
    startMonitoring,
    stopMonitoring,
    restartMonitoring,
    initializeMonitoring,
    isMonitoringActive,
};

fix everything and make it perfect