RocketChat/Rocket.Chat

View on GitHub
apps/meteor/app/statistics/server/lib/statistics.ts

Summary

Maintainability
F
5 days
Test Coverage
import { log } from 'console';
import os from 'os';

import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services';
import type { IRoom, IStats } from '@rocket.chat/core-typings';
import { UserStatus } from '@rocket.chat/core-typings';
import {
    NotificationQueue,
    Rooms,
    Statistics,
    Sessions,
    Integrations,
    Invites,
    Uploads,
    LivechatDepartment,
    LivechatVisitors,
    EmailInbox,
    LivechatBusinessHours,
    Messages,
    Roles as RolesRaw,
    InstanceStatus,
    Settings,
    LivechatTrigger,
    LivechatCustomField,
    Subscriptions,
    Users,
    LivechatRooms,
} from '@rocket.chat/models';
import { MongoInternals } from 'meteor/mongo';
import moment from 'moment';

import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server/getStatistics';
import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred';
import { isRunningMs } from '../../../../server/lib/isRunningMs';
import { getControl } from '../../../../server/lib/migrations';
import { getSettingsStatistics } from '../../../../server/lib/statistics/getSettingsStatistics';
import { getMatrixFederationStatistics } from '../../../../server/services/federation/infrastructure/rocket-chat/adapters/Statistics';
import { getStatistics as federationGetStatistics } from '../../../federation/server/functions/dashboard';
import { settings } from '../../../settings/server';
import { Info } from '../../../utils/rocketchat.info';
import { getMongoInfo } from '../../../utils/server/functions/getMongoInfo';
import { getAppsStatistics } from './getAppsStatistics';
import { getImporterStatistics } from './getImporterStatistics';
import { getServicesStatistics } from './getServicesStatistics';

const getUserLanguages = async (totalUsers: number): Promise<{ [key: string]: number }> => {
    const result = await Users.getUserLanguages();

    const languages: { [key: string]: number } = {
        none: totalUsers,
    };

    result.forEach(({ _id, total }: { _id: string; total: number }) => {
        if (!_id) {
            return;
        }
        languages[_id] = total;
        languages.none -= total;
    });

    return languages;
};

const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo;

export const statistics = {
    get: async (): Promise<IStats> => {
        const readPreference = readSecondaryPreferred(db);

        const statistics = {} as IStats;
        const statsPms = [];

        const fetchWizardSettingValue = async <T>(settingName: string): Promise<T | undefined> => {
            return ((await Settings.findOne(settingName))?.value as T | undefined) ?? undefined;
        };

        // Setup Wizard
        const [organizationType, industry, size, country, language, serverType, registerServer] = await Promise.all([
            fetchWizardSettingValue<string>('Organization_Type'),
            fetchWizardSettingValue<string>('Industry'),
            fetchWizardSettingValue<string>('Size'),
            fetchWizardSettingValue<string>('Country'),
            fetchWizardSettingValue<string>('Language'),
            fetchWizardSettingValue<string>('Server_Type'),
            fetchWizardSettingValue<boolean>('Register_Server'),
        ]);
        statistics.wizard = {
            organizationType,
            industry,
            size,
            country,
            language,
            serverType,
            registerServer,
        };

        // Version
        const uniqueID = await Settings.findOne('uniqueID');
        statistics.uniqueId = settings.get('uniqueID');
        if (uniqueID) {
            statistics.installedAt = uniqueID.createdAt.toISOString();
        }

        statistics.deploymentFingerprintHash = settings.get('Deployment_FingerPrint_Hash');
        statistics.deploymentFingerprintVerified = settings.get('Deployment_FingerPrint_Verified');

        if (Info) {
            statistics.version = Info.version;
            statistics.tag = Info.tag;
            statistics.branch = Info.branch;
        }

        // User statistics
        statistics.totalUsers = await Users.col.countDocuments({});
        statistics.activeUsers = await Users.getActiveLocalUserCount();
        statistics.activeGuests = await Users.getActiveLocalGuestCount();
        statistics.nonActiveUsers = await Users.col.countDocuments({ active: false });
        statistics.appUsers = await Users.col.countDocuments({ type: 'app' });
        statistics.onlineUsers = await Users.col.countDocuments({ status: UserStatus.ONLINE });
        statistics.awayUsers = await Users.col.countDocuments({ status: UserStatus.AWAY });
        statistics.busyUsers = await Users.col.countDocuments({ status: UserStatus.BUSY });
        statistics.totalConnectedUsers = statistics.onlineUsers + statistics.awayUsers;
        statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers - statistics.awayUsers - statistics.busyUsers;
        statsPms.push(
            getUserLanguages(statistics.totalUsers).then((total) => {
                statistics.userLanguages = total;
            }),
        );

        // Room statistics
        statistics.totalRooms = await Rooms.col.countDocuments({});
        statistics.totalChannels = await Rooms.findByType('c').count();
        statistics.totalPrivateGroups = await Rooms.findByType('p').count();
        statistics.totalDirect = await Rooms.findByType('d').count();
        statistics.totalLivechat = await Rooms.findByType('l').count();
        statistics.totalDiscussions = await Rooms.countDiscussions();
        statistics.totalThreads = await Messages.countThreads();

        // livechat visitors
        statistics.totalLivechatVisitors = await LivechatVisitors.estimatedDocumentCount();

        // livechat agents
        statistics.totalLivechatAgents = await Users.countAgents();
        statistics.totalLivechatManagers = await Users.countDocuments({ roles: 'livechat-manager' });

        // livechat enabled
        statistics.livechatEnabled = settings.get('Livechat_enabled');

        // Count and types of omnichannel rooms
        statsPms.push(
            Rooms.allRoomSourcesCount()
                .toArray()
                .then((roomSources) => {
                    statistics.omnichannelSources = roomSources.map(({ _id: { id, alias, type }, count }) => ({
                        id,
                        alias,
                        type,
                        count,
                    }));
                }),
        );

        // Number of departments
        statsPms.push(
            LivechatDepartment.estimatedDocumentCount().then((count) => {
                statistics.departments = count;
            }),
        );

        // Number of archived departments
        statsPms.push(
            LivechatDepartment.countArchived().then((count) => {
                statistics.archivedDepartments = count;
            }),
        );

        // Workspace allows dpeartment removal
        statistics.isDepartmentRemovalEnabled = settings.get('Omnichannel_enable_department_removal');

        // Number of triggers
        statsPms.push(
            LivechatTrigger.col.count().then((count) => {
                statistics.totalTriggers = count;
            }),
        );

        // Number of custom fields
        statsPms.push(
            LivechatCustomField.col.count().then((count) => {
                statistics.totalCustomFields = count;
            }),
        );

        // Type of routing algorithm used on omnichannel
        statistics.routingAlgorithm = settings.get('Livechat_Routing_Method') || '';

        // is on-hold active
        statistics.onHoldEnabled = settings.get('Livechat_allow_manual_on_hold');

        // Number of Email Inboxes
        statsPms.push(
            EmailInbox.col.count().then((count) => {
                statistics.emailInboxes = count;
            }),
        );

        statsPms.push(
            LivechatBusinessHours.col.count().then((count) => {
                statistics.BusinessHours = {
                    // Number of Business Hours
                    total: count,
                    // Business Hours strategy
                    strategy: settings.get('Livechat_enable_business_hours') || '',
                };
            }),
        );

        // Type of routing algorithm used on omnichannel
        statistics.routingAlgorithm = settings.get('Livechat_Routing_Method');

        // is on-hold active
        statistics.onHoldEnabled = settings.get('Livechat_allow_manual_on_hold');

        // Last-Chatted Agent Preferred (enabled/disabled)
        statistics.lastChattedAgentPreferred = settings.get('Livechat_last_chatted_agent_routing');

        // Assign new conversations to the contact manager (enabled/disabled)
        statistics.assignNewConversationsToContactManager = settings.get('Omnichannel_contact_manager_routing');

        // How to handle Visitor Abandonment setting
        statistics.visitorAbandonment = settings.get('Livechat_abandoned_rooms_action');

        // Amount of chats placed on hold
        statsPms.push(
            Messages.countRoomsWithMessageType('omnichannel_placed_chat_on_hold', { readPreference }).then((total) => {
                statistics.chatsOnHold = total;
            }),
        );

        // VoIP Enabled
        statistics.voipEnabled = settings.get('VoIP_Enabled');

        // Amount of VoIP Calls
        statsPms.push(
            Rooms.countByType('v').then((count) => {
                statistics.voipCalls = count;
            }),
        );

        // Amount of VoIP Extensions connected
        statsPms.push(
            Users.col.countDocuments({ extension: { $exists: true } }).then((count) => {
                statistics.voipExtensions = count;
            }),
        );

        // Amount of Calls that ended properly
        statsPms.push(
            Messages.countByType('voip-call-wrapup', { readPreference }).then((count) => {
                statistics.voipSuccessfulCalls = count;
            }),
        );

        // Amount of Calls that ended with an error
        statsPms.push(
            Messages.countByType('voip-call-ended-unexpectedly', { readPreference }).then((count) => {
                statistics.voipErrorCalls = count;
            }),
        );
        // Amount of Calls that were put on hold
        statsPms.push(
            Messages.countRoomsWithMessageType('voip-call-on-hold', { readPreference }).then((count) => {
                statistics.voipOnHoldCalls = count;
            }),
        );

        const defaultValue = { contactsCount: 0, conversationsCount: 0, sources: [] };
        const billablePeriod = moment.utc().format('YYYY-MM');
        statsPms.push(
            LivechatRooms.getMACStatisticsForPeriod(billablePeriod).then(([result]) => {
                statistics.omnichannelContactsBySource = result || defaultValue;
            }),
        );

        const monthAgo = moment.utc().subtract(30, 'days').toDate();
        const today = moment.utc().toDate();
        statsPms.push(
            LivechatRooms.getMACStatisticsBetweenDates(monthAgo, today).then(([result]) => {
                statistics.uniqueContactsOfLastMonth = result || defaultValue;
            }),
        );

        const weekAgo = moment.utc().subtract(7, 'days').toDate();
        statsPms.push(
            LivechatRooms.getMACStatisticsBetweenDates(weekAgo, today).then(([result]) => {
                statistics.uniqueContactsOfLastWeek = result || defaultValue;
            }),
        );

        const yesterday = moment.utc().subtract(1, 'days').toDate();
        statsPms.push(
            LivechatRooms.getMACStatisticsBetweenDates(yesterday, today).then(([result]) => {
                statistics.uniqueContactsOfYesterday = result || defaultValue;
            }),
        );

        // Message statistics
        const channels = await Rooms.findByType('c', { projection: { msgs: 1, prid: 1 } }).toArray();
        const totalChannelDiscussionsMessages = await channels.reduce(function _countChannelDiscussionsMessages(num: number, room: IRoom) {
            return num + (room.prid ? room.msgs : 0);
        }, 0);
        statistics.totalChannelMessages =
            (await channels.reduce(function _countChannelMessages(num: number, room: IRoom) {
                return num + room.msgs;
            }, 0)) - totalChannelDiscussionsMessages;

        const privateGroups = await Rooms.findByType('p', { projection: { msgs: 1, prid: 1 } }).toArray();
        const totalPrivateGroupsDiscussionsMessages = await privateGroups.reduce(function _countPrivateGroupsDiscussionsMessages(
            num: number,
            room: IRoom,
        ) {
            return num + (room.prid ? room.msgs : 0);
        },
        0);
        statistics.totalPrivateGroupMessages =
            (await privateGroups.reduce(function _countPrivateGroupMessages(num: number, room: IRoom) {
                return num + room.msgs;
            }, 0)) - totalPrivateGroupsDiscussionsMessages;

        statistics.totalDiscussionsMessages = totalPrivateGroupsDiscussionsMessages + totalChannelDiscussionsMessages;

        statistics.totalDirectMessages = (await Rooms.findByType('d', { projection: { msgs: 1 } }).toArray()).reduce(
            function _countDirectMessages(num: number, room: IRoom) {
                return num + room.msgs;
            },
            0,
        );
        statistics.totalLivechatMessages = (await Rooms.findByType('l', { projection: { msgs: 1 } }).toArray()).reduce(
            function _countLivechatMessages(num: number, room: IRoom) {
                return num + room.msgs;
            },
            0,
        );
        statistics.totalMessages =
            statistics.totalChannelMessages +
            statistics.totalPrivateGroupMessages +
            statistics.totalDiscussionsMessages +
            statistics.totalDirectMessages +
            statistics.totalLivechatMessages;

        // Federation statistics
        statsPms.push(
            federationGetStatistics().then((federationOverviewData) => {
                statistics.federatedServers = federationOverviewData.numberOfServers;
                statistics.federatedUsers = federationOverviewData.numberOfFederatedUsers;
            }),
        );

        statistics.lastLogin = (await Users.getLastLogin())?.toString() || '';
        statistics.lastMessageSentAt = await Messages.getLastTimestamp();
        statistics.lastSeenSubscription = (await Subscriptions.getLastSeen())?.toString() || '';

        statistics.os = {
            type: os.type(),
            platform: os.platform(),
            arch: os.arch(),
            release: os.release(),
            uptime: os.uptime(),
            loadavg: os.loadavg(),
            totalmem: os.totalmem(),
            freemem: os.freemem(),
            cpus: os.cpus(),
        };

        statistics.process = {
            nodeVersion: process.version,
            pid: process.pid,
            uptime: process.uptime(),
        };

        statistics.deploy = {
            method: process.env.DEPLOY_METHOD || 'tar',
            platform: process.env.DEPLOY_PLATFORM || 'selfinstall',
        };

        statistics.readReceiptsEnabled = settings.get('Message_Read_Receipt_Enabled');
        statistics.readReceiptsDetailed = settings.get('Message_Read_Receipt_Store_Users');

        statistics.enterpriseReady = true;
        statsPms.push(
            Uploads.col.estimatedDocumentCount().then((count) => {
                statistics.uploadsTotal = count;
            }),
        );
        statsPms.push(
            Uploads.col
                .aggregate(
                    [
                        {
                            $group: { _id: 'total', total: { $sum: '$size' } },
                        },
                    ],
                    { readPreference },
                )
                .toArray()
                .then((agg) => {
                    const [result] = agg;
                    statistics.uploadsTotalSize = result ? (result as any).total : 0;
                }),
        );

        statistics.migration = await getControl();
        statsPms.push(
            InstanceStatus.col.countDocuments({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }).then((count) => {
                statistics.instanceCount = count;
            }),
        );

        const { oplogEnabled, mongoVersion, mongoStorageEngine } = await getMongoInfo();
        statistics.msEnabled = isRunningMs();
        statistics.oplogEnabled = oplogEnabled;
        statistics.mongoVersion = mongoVersion;
        statistics.mongoStorageEngine = mongoStorageEngine || '';

        statsPms.push(
            Sessions.getUniqueUsersOfYesterday().then((result) => {
                statistics.uniqueUsersOfYesterday = result;
            }),
        );
        statsPms.push(
            Sessions.getUniqueUsersOfLastWeek().then((result) => {
                statistics.uniqueUsersOfLastWeek = result;
            }),
        );
        statsPms.push(
            Sessions.getUniqueUsersOfLastMonth().then((result) => {
                statistics.uniqueUsersOfLastMonth = result;
            }),
        );
        statsPms.push(
            Sessions.getUniqueDevicesOfYesterday().then((result) => {
                statistics.uniqueDevicesOfYesterday = result;
            }),
        );
        statsPms.push(
            Sessions.getUniqueDevicesOfLastWeek().then((result) => {
                statistics.uniqueDevicesOfLastWeek = result;
            }),
        );
        statsPms.push(
            Sessions.getUniqueDevicesOfLastMonth().then((result) => {
                statistics.uniqueDevicesOfLastMonth = result;
            }),
        );
        statsPms.push(
            Sessions.getUniqueOSOfYesterday().then((result) => {
                statistics.uniqueOSOfYesterday = result;
            }),
        );
        statsPms.push(
            Sessions.getUniqueOSOfLastWeek().then((result) => {
                statistics.uniqueOSOfLastWeek = result;
            }),
        );
        statsPms.push(
            Sessions.getUniqueOSOfLastMonth().then((result) => {
                statistics.uniqueOSOfLastMonth = result;
            }),
        );

        statistics.apps = getAppsStatistics();
        statistics.services = await getServicesStatistics();
        statistics.importer = getImporterStatistics();
        statistics.videoConf = await VideoConf.getStatistics();

        // If getSettingsStatistics() returns an error, save as empty object.
        statsPms.push(
            getSettingsStatistics().then((res) => {
                const settingsStatisticsObject = res || {};
                statistics.settings = settingsStatisticsObject;
            }),
        );

        statsPms.push(
            Integrations.find(
                {},
                {
                    projection: {
                        _id: 0,
                        type: 1,
                        enabled: 1,
                        scriptEnabled: 1,
                    },
                    readPreference,
                },
            )
                .toArray()
                .then((found) => {
                    const integrations = found;

                    statistics.integrations = {
                        totalIntegrations: integrations.length,
                        totalIncoming: integrations.filter((integration) => integration.type === 'webhook-incoming').length,
                        totalIncomingActive: integrations.filter(
                            (integration) => integration.enabled === true && integration.type === 'webhook-incoming',
                        ).length,
                        totalOutgoing: integrations.filter((integration) => integration.type === 'webhook-outgoing').length,
                        totalOutgoingActive: integrations.filter(
                            (integration) => integration.enabled === true && integration.type === 'webhook-outgoing',
                        ).length,
                        totalWithScriptEnabled: integrations.filter((integration) => integration.scriptEnabled === true).length,
                    };
                }),
        );

        statsPms.push(
            NotificationQueue.col.estimatedDocumentCount().then((count) => {
                statistics.pushQueue = count;
            }),
        );

        statsPms.push(
            getEnterpriseStatistics().then((result) => {
                statistics.enterprise = result;
            }),
        );

        statsPms.push(
            Team.getStatistics().then((result) => {
                statistics.teams = result;
            }),
        );

        statsPms.push(Analytics.resetSeatRequestCount());

        // TODO: Is that the best way to do this? maybe we should use a promise.all()

        statistics.dashboardCount = settings.get('Engagement_Dashboard_Load_Count');
        statistics.messageAuditApply = settings.get('Message_Auditing_Apply_Count');
        statistics.messageAuditLoad = settings.get('Message_Auditing_Panel_Load_Count');
        statistics.joinJitsiButton = settings.get('Jitsi_Click_To_Join_Count');
        statistics.slashCommandsJitsi = settings.get('Jitsi_Start_SlashCommands_Count');
        statistics.totalOTRRooms = await Rooms.findByCreatedOTR().count();
        statistics.totalOTR = settings.get('OTR_Count');
        statistics.totalBroadcastRooms = await Rooms.findByBroadcast().count();
        statistics.totalRoomsWithActiveLivestream = await Rooms.findByActiveLivestream().count();
        statistics.totalTriggeredEmails = settings.get('Triggered_Emails_Count');
        statistics.totalRoomsWithStarred = await Messages.countRoomsWithStarredMessages({ readPreference });
        statistics.totalRoomsWithPinned = await Messages.countRoomsWithPinnedMessages({ readPreference });
        statistics.totalUserTOTP = await Users.countActiveUsersTOTPEnable({ readPreference });
        statistics.totalUserEmail2fa = await Users.countActiveUsersEmail2faEnable({ readPreference });
        statistics.totalPinned = await Messages.findPinned({ readPreference }).count();
        statistics.totalStarred = await Messages.findStarred({ readPreference }).count();
        statistics.totalLinkInvitation = await Invites.find().count();
        statistics.totalLinkInvitationUses = await Invites.countUses();
        statistics.totalEmailInvitation = settings.get('Invitation_Email_Count');
        statistics.totalE2ERooms = await Rooms.findByE2E({ readPreference }).count();
        statistics.logoChange = Object.keys(settings.get('Assets_logo') || {}).includes('url');
        statistics.showHomeButton = settings.get('Layout_Show_Home_Button');
        statistics.totalEncryptedMessages = await Messages.countByType('e2e', { readPreference });
        statistics.totalManuallyAddedUsers = settings.get('Manual_Entry_User_Count');
        statistics.totalSubscriptionRoles = await RolesRaw.findByScope('Subscriptions').count();
        statistics.totalUserRoles = await RolesRaw.findByScope('Users').count();
        statistics.totalCustomRoles = await RolesRaw.findCustomRoles({ readPreference }).count();
        statistics.totalWebRTCCalls = settings.get('WebRTC_Calls_Count');
        statistics.uncaughtExceptionsCount = settings.get('Uncaught_Exceptions_Count');

        const defaultGateway = (await Settings.findOneById('Push_gateway', { projection: { packageValue: 1 } }))?.packageValue;

        // one bit for each of the following:
        const pushEnabled = settings.get('Push_enable') ? 1 : 0;
        const pushGatewayEnabled = settings.get('Push_enable_gateway') ? 2 : 0;
        const pushGatewayChanged = settings.get('Push_gateway') !== defaultGateway ? 4 : 0;

        statistics.push = pushEnabled | pushGatewayEnabled | pushGatewayChanged;

        const defaultHomeTitle = (await Settings.findOneById('Layout_Home_Title'))?.packageValue;
        statistics.homeTitleChanged = settings.get('Layout_Home_Title') !== defaultHomeTitle;

        const defaultHomeBody = (await Settings.findOneById('Layout_Home_Body'))?.packageValue;
        statistics.homeBodyChanged = settings.get('Layout_Home_Body') !== defaultHomeBody;

        const defaultCustomCSS = (await Settings.findOneById('theme-custom-css'))?.packageValue;
        statistics.customCSSChanged = settings.get('theme-custom-css') !== defaultCustomCSS;

        const defaultOnLogoutCustomScript = (await Settings.findOneById('Custom_Script_On_Logout'))?.packageValue;
        statistics.onLogoutCustomScriptChanged = settings.get('Custom_Script_On_Logout') !== defaultOnLogoutCustomScript;

        const defaultLoggedOutCustomScript = (await Settings.findOneById('Custom_Script_Logged_Out'))?.packageValue;
        statistics.loggedOutCustomScriptChanged = settings.get('Custom_Script_Logged_Out') !== defaultLoggedOutCustomScript;

        const defaultLoggedInCustomScript = (await Settings.findOneById('Custom_Script_Logged_In'))?.packageValue;
        statistics.loggedInCustomScriptChanged = settings.get('Custom_Script_Logged_In') !== defaultLoggedInCustomScript;

        try {
            statistics.dailyPeakConnections = await Presence.getPeakConnections(true);
        } catch {
            statistics.dailyPeakConnections = 0;
        }

        const peak = await Statistics.findMonthlyPeakConnections();
        statistics.maxMonthlyPeakConnections = Math.max(statistics.dailyPeakConnections, peak?.dailyPeakConnections || 0);

        statistics.matrixFederation = await getMatrixFederationStatistics();

        // Omnichannel call stats
        statistics.webRTCEnabled = settings.get('WebRTC_Enabled');
        statistics.webRTCEnabledForOmnichannel = settings.get('Omnichannel_call_provider') === 'WebRTC';
        statistics.omnichannelWebRTCCalls = await Rooms.findCountOfRoomsWithActiveCalls();

        await Promise.all(statsPms).catch(log);

        return statistics;
    },
    async save(): Promise<IStats> {
        const rcStatistics = await statistics.get();
        rcStatistics.createdAt = new Date();
        const { insertedId } = await Statistics.insertOne(rcStatistics);
        rcStatistics._id = insertedId;

        return rcStatistics;
    },
};