From d323c1e51efac880394036d75bb3b8218d4e220d Mon Sep 17 00:00:00 2001 From: Emile Ndagijimana Date: Wed, 21 Sep 2022 14:33:17 +0200 Subject: [PATCH] feat(metrics KPI): Added some metrics KPI Datas --- models/server/metrics.js | 145 +++++++++++++++++++++++++++ models/users.js | 189 ++++++++++++++++++++++------------- server/publications/users.js | 74 +++++++++++++- 3 files changed, 335 insertions(+), 73 deletions(-) create mode 100644 models/server/metrics.js diff --git a/models/server/metrics.js b/models/server/metrics.js new file mode 100644 index 000000000..5e9404e87 --- /dev/null +++ b/models/server/metrics.js @@ -0,0 +1,145 @@ +import { Meteor } from 'meteor/meteor'; +import Users from '../users'; + +function acceptedIpAdress(ipAdress) { + //return true if a given ipAdress was setted by an admin user + console.log('idpAdress', ipAdress); + + //Check if ipAdress is accepted + console.log( + 'process.env.WEKAN_METRICS_ACCEPTED_IP_ADRESS', + process.env.WEKAN_METRICS_ACCEPTED_IP_ADRESS, + ); + //console.log("process.env", process.env); + const trustedIpAdress = process.env.WEKAN_METRICS_ACCEPTED_IP_ADRESS; + //console.log("trustedIpAdress", trustedIpAdress); + //console.log("trustedIpAdress !== undefined && trustedIpAdress.split(",").includes(ipAdress)", trustedIpAdress !== undefined && trustedIpAdress.split(",").includes(ipAdress)); + return ( + trustedIpAdress !== undefined && + trustedIpAdress.split(',').includes(ipAdress) + ); +} + +Meteor.startup(() => { + WebApp.connectHandlers.use('/metrics', (req, res, next) => { + try { + const ipAdress = + req.headers['x-forwarded-for'] || req.socket.remoteAddress; + // if(process.env.TRUST_PROXY_FORXARD) + // { + // const ipAdress = req.headers['x-forwarded-for'] || req.socket.remoteAddress + // }else{ + // const ipAdress = req.socket.remoteAddress + // } + + // List of trusted ip adress will be found in environment variable "WEKAN_METRICS_ACCEPTED_IP_ADRESS" (separeted with commas) + if (acceptedIpAdress(ipAdress)) { + let metricsRes = ''; + let resCount = 0; + //connected users + metricsRes += '# Number of connected users\n'; + + // To Do: Get number of connected user by using meteor socketJs + const allOpenedSockets = Meteor.server.stream_server.open_sockets; + let connectedUserIds = []; + allOpenedSockets.forEach( + (socket) => + //console.log('meteor session', socket._meteorSession.userId) + socket._meteorSession.userId !== null && + connectedUserIds.push(socket._meteorSession.userId), + ); + resCount = connectedUserIds.length; // KPI 1 + metricsRes += 'connectedUsers ' + resCount + '\n'; + + //registered users + metricsRes += '# Number of registered users\n'; + + // To Do: Get number of registered user + resCount = Users.find({}).count(); // KPI 2 + metricsRes += 'registeredUsers ' + resCount + '\n'; + resCount = 0; + + //board numbers + metricsRes += '# Number of registered boards\n'; + + // To Do: Get number of registered boards + resCount = Boards.find({ archived: false, type: 'board' }).count(); // KPI 3 + metricsRes += 'registeredboards ' + resCount + '\n'; + resCount = 0; + + //board numbers by registered users + metricsRes += '# Number of registered boards by registered users\n'; + + // To Do: Get number of registered boards by registered users + resCount = + Boards.find({ archived: false, type: 'board' }).count() / + Users.find({}).count(); // KPI 4 + metricsRes += 'registeredboardsBysRegisteredUsers ' + resCount + '\n'; + resCount = 0; + + //board numbers with only one member + metricsRes += '# Number of registered boards\n'; + + // To Do: Get board numbers with only one member + resCount = Boards.find({ + archived: false, + type: 'board', + members: { $size: 1 }, + }).count(); // KPI 5 + metricsRes += 'registeredboardsWithOnlyOneMember ' + resCount + '\n'; + resCount = 0; + + // KPI 6 : - stocker la date de dernière connexion + // KPI 6 = count where date de dernière connexion > x jours + // Découpe en label since 5 jours / 10 jours / 20 Jours / 30 jours + + //Number of users with last connection dated 5 days ago + metricsRes += + '# Number of users with last connection dated 5 days ago\n'; + + // To Do: Get number of users with last connection dated 5 days ago + let xdays = 5; + let dateWithXdaysAgo = new Date( + new Date() - xdays * 24 * 60 * 60 * 1000, + ); + console.log({ dateWithXdaysAgo }); + resCount = Users.find({ + lastConnectionDate: { $gte: dateWithXdaysAgo }, + }).count(); // KPI 5 + metricsRes += 'usersWithLastConnectionDated5DaysAgo ' + resCount + '\n'; + resCount = 0; + + metricsRes += + '# Number of users with last connection dated 10 days ago\n'; + + // To Do: Get number of users with last connection dated 10 days ago + xdays = 10; + dateWithXdaysAgo = new Date(new Date() - xdays * 24 * 60 * 60 * 1000); + console.log({ dateWithXdaysAgo }); + resCount = Users.find({ + lastConnectionDate: { $gte: dateWithXdaysAgo }, + }).count(); // KPI 5 + metricsRes += + 'usersWithLastConnectionDated10DaysAgo ' + resCount + '\n'; + resCount = 0; + + // TO DO: + // moyenne de connexion : ((date de déconnexion - date de dernière connexion) + (dernière moyenne)) / 2 + // KPI 7 : somme des moyenne de connexion / nombre d'utilisateur (à ignore les utilisateur avec 0 moyenne) + + res.writeHead(200); // HTTP status + res.end(metricsRes); + } else { + res.writeHead(401); // HTTP status + res.end( + 'IpAdress: ' + + ipAdress + + ' is not authorized to perform this action !!\n', + ); + } + } catch (e) { + res.writeHead(500); // HTTP status + res.end(e.toString()); + } + }); +}); diff --git a/models/users.js b/models/users.js index bb0f949dd..7889a48b5 100644 --- a/models/users.js +++ b/models/users.js @@ -2,7 +2,7 @@ import { SyncedCron } from 'meteor/percolate:synced-cron'; import { TAPi18n } from '/imports/i18n'; import ImpersonatedUsers from './impersonatedUsers'; -import { Index, MongoDBEngine } from 'meteor/easy:search' +import { Index, MongoDBEngine } from 'meteor/easy:search'; // Sandstorm context is detected using the METEOR_SETTINGS environment variable // in the package definition. @@ -45,39 +45,39 @@ Users.attachSchema( /** * the list of organizations that a user belongs to */ - type: [Object], - optional: true, + type: [Object], + optional: true, }, - 'orgs.$.orgId':{ + 'orgs.$.orgId': { /** * The uniq ID of the organization */ - type: String, + type: String, }, - 'orgs.$.orgDisplayName':{ + 'orgs.$.orgDisplayName': { /** * The display name of the organization */ - type: String, + type: String, }, teams: { /** * the list of teams that a user belongs to */ - type: [Object], - optional: true, + type: [Object], + optional: true, }, - 'teams.$.teamId':{ + 'teams.$.teamId': { /** * The uniq ID of the team */ - type: String, + type: String, }, - 'teams.$.teamDisplayName':{ + 'teams.$.teamDisplayName': { /** * The display name of the team */ - type: String, + type: String, }, emails: { /** @@ -228,7 +228,7 @@ Users.attachSchema( type: String, optional: true, }, - 'profile.moveAndCopyDialog' : { + 'profile.moveAndCopyDialog': { /** * move and copy card dialog */ @@ -254,7 +254,7 @@ Users.attachSchema( */ type: String, }, - 'profile.moveChecklistDialog' : { + 'profile.moveChecklistDialog': { /** * move checklist dialog */ @@ -286,7 +286,7 @@ Users.attachSchema( */ type: String, }, - 'profile.copyChecklistDialog' : { + 'profile.copyChecklistDialog': { /** * copy checklist dialog */ @@ -494,6 +494,10 @@ Users.attachSchema( type: [String], optional: true, }, + lastConnectionDate: { + type: Date, + optional: true, + }, }), ); @@ -542,13 +546,13 @@ UserSearchIndex = new Index({ fields: ['username', 'profile.fullname', 'profile.avatarUrl'], allowedFields: ['username', 'profile.fullname', 'profile.avatarUrl'], engine: new MongoDBEngine({ - fields: function(searchObject, options) { + fields: function (searchObject, options) { return { - 'username': 1, + username: 1, 'profile.fullname': 1, - 'profile.avatarUrl': 1 + 'profile.avatarUrl': 1, }; - } + }, }), }); @@ -561,6 +565,7 @@ Users.safeFields = { orgs: 1, teams: 1, authenticationMethod: 1, + lastConnectionDate: 1, }; if (Meteor.isClient) { @@ -630,43 +635,65 @@ Users.helpers({ teamIds() { if (this.teams) { // TODO: Should the Team collection be queried to determine if the team isActive? - return this.teams.map(team => { return team.teamId }); + return this.teams.map((team) => { + return team.teamId; + }); } return []; }, orgIds() { if (this.orgs) { // TODO: Should the Org collection be queried to determine if the organization isActive? - return this.orgs.map(org => { return org.orgId }); + return this.orgs.map((org) => { + return org.orgId; + }); } return []; }, orgsUserBelongs() { if (this.orgs) { - return this.orgs.map(function(org){return org.orgDisplayName}).sort().join(','); + return this.orgs + .map(function (org) { + return org.orgDisplayName; + }) + .sort() + .join(','); } return ''; }, orgIdsUserBelongs() { if (this.orgs) { - return this.orgs.map(function(org){return org.orgId}).join(','); + return this.orgs + .map(function (org) { + return org.orgId; + }) + .join(','); } return ''; }, teamsUserBelongs() { if (this.teams) { - return this.teams.map(function(team){ return team.teamDisplayName}).sort().join(','); + return this.teams + .map(function (team) { + return team.teamDisplayName; + }) + .sort() + .join(','); } return ''; }, teamIdsUserBelongs() { if (this.teams) { - return this.teams.map(function(team){ return team.teamId}).join(','); + return this.teams + .map(function (team) { + return team.teamId; + }) + .join(','); } return ''; }, boards() { - return Boards.userBoards(this._id, null, {}, { sort: { sort: 1 } }) + return Boards.userBoards(this._id, null, {}, { sort: { sort: 1 } }); }, starredBoards() { @@ -675,7 +702,7 @@ Users.helpers({ this._id, false, { _id: { $in: starredBoards } }, - { sort: { sort: 1 } } + { sort: { sort: 1 } }, ); }, @@ -690,7 +717,7 @@ Users.helpers({ this._id, false, { _id: { $in: invitedBoards } }, - { sort: { sort: 1 } } + { sort: { sort: 1 } }, ); }, @@ -728,7 +755,7 @@ Users.helpers({ *
  • the board, swimlane and list id is stored for each board */ getMoveAndCopyDialogOptions() { - let _ret = {} + let _ret = {}; if (this.profile && this.profile.moveAndCopyDialog) { _ret = this.profile.moveAndCopyDialog; } @@ -739,7 +766,7 @@ Users.helpers({ *
  • the board, swimlane, list and card id is stored for each board */ getMoveChecklistDialogOptions() { - let _ret = {} + let _ret = {}; if (this.profile && this.profile.moveChecklistDialog) { _ret = this.profile.moveChecklistDialog; } @@ -750,7 +777,7 @@ Users.helpers({ *
  • the board, swimlane, list and card id is stored for each board */ getCopyChecklistDialogOptions() { - let _ret = {} + let _ret = {}; if (this.profile && this.profile.copyChecklistDialog) { _ret = this.profile.copyChecklistDialog; } @@ -811,7 +838,7 @@ Users.helpers({ return profile.hiddenMinicardLabelText || false; }, - hasRescuedCardDescription(){ + hasRescuedCardDescription() { const profile = this.profile || {}; return profile.rescueCardDescription || false; }, @@ -1430,17 +1457,30 @@ if (Meteor.isServer) { } try { - const fullName = inviter.profile !== undefined && inviter.profile.fullname !== undefined ? inviter.profile.fullname : ""; - const userFullName = user.profile !== undefined && user.profile.fullname !== undefined ? user.profile.fullname : ""; + const fullName = + inviter.profile !== undefined && + inviter.profile.fullname !== undefined + ? inviter.profile.fullname + : ''; + const userFullName = + user.profile !== undefined && user.profile.fullname !== undefined + ? user.profile.fullname + : ''; const params = { - user: userFullName != "" ? userFullName + " (" + user.username + " )" : user.username, - inviter: fullName != "" ? fullName + " (" + inviter.username + " )" : inviter.username, + user: + userFullName != '' + ? userFullName + ' (' + user.username + ' )' + : user.username, + inviter: + fullName != '' + ? fullName + ' (' + inviter.username + ' )' + : inviter.username, board: board.title, url: board.absoluteUrl(), }; const lang = user.getLanguage(); -/* + /* if (process.env.MAIL_SERVICE !== '') { let transporter = nodemailer.createTransport({ service: process.env.MAIL_SERVICE, @@ -1486,7 +1526,11 @@ if (Meteor.isServer) { if (!Meteor.user().isAdmin) throw new Meteor.Error(403, 'Permission denied'); - ImpersonatedUsers.insert({ adminId: Meteor.user()._id, userId: userId, reason: 'clickedImpersonate' }); + ImpersonatedUsers.insert({ + adminId: Meteor.user()._id, + userId: userId, + reason: 'clickedImpersonate', + }); this.setUserId(userId); }, isImpersonated(userId) { @@ -1502,19 +1546,22 @@ if (Meteor.isServer) { if (Meteor.user() && Meteor.user().isAdmin) { Users.find({ teams: { - $elemMatch: {teamId: teamId} - } - }).forEach(user => { - Users.update({ - _id: user._id, - teams: { - $elemMatch: {teamId: teamId} - } - }, { - $set: { - 'teams.$.teamDisplayName': teamDisplayName - } - }); + $elemMatch: { teamId: teamId }, + }, + }).forEach((user) => { + Users.update( + { + _id: user._id, + teams: { + $elemMatch: { teamId: teamId }, + }, + }, + { + $set: { + 'teams.$.teamDisplayName': teamDisplayName, + }, + }, + ); }); } }, @@ -1524,19 +1571,22 @@ if (Meteor.isServer) { if (Meteor.user() && Meteor.user().isAdmin) { Users.find({ orgs: { - $elemMatch: {orgId: orgId} - } - }).forEach(user => { - Users.update({ - _id: user._id, - orgs: { - $elemMatch: {orgId: orgId} - } - }, { - $set: { - 'orgs.$.orgDisplayName': orgDisplayName - } - }); + $elemMatch: { orgId: orgId }, + }, + }).forEach((user) => { + Users.update( + { + _id: user._id, + orgs: { + $elemMatch: { orgId: orgId }, + }, + }, + { + $set: { + 'orgs.$.orgDisplayName': orgDisplayName, + }, + }, + ); }); } }, @@ -1699,7 +1749,7 @@ if (Meteor.isServer) { Users._collection.createIndex({ modifiedAt: -1, }); -/* Commented out extra index because of IndexOptionsConflict. + /* Commented out extra index because of IndexOptionsConflict. Users._collection.createIndex( { username: 1, @@ -1918,14 +1968,13 @@ if (Meteor.isServer) { // TODO : pay attention if ldap field in the user model change to another content ex : ldap field to connection_type if (doc.authenticationMethod !== 'ldap' && disableRegistration) { let invitationCode = null; - if(doc.authenticationMethod.toLowerCase() == 'oauth2') - { // OIDC authentication mode + if (doc.authenticationMethod.toLowerCase() == 'oauth2') { + // OIDC authentication mode invitationCode = InvitationCodes.findOne({ email: doc.emails[0].address.toLowerCase(), valid: true, }); - } - else{ + } else { invitationCode = InvitationCodes.findOne({ code: doc.profile.icode, valid: true, diff --git a/server/publications/users.js b/server/publications/users.js index df50f7be8..c764bf9d6 100644 --- a/server/publications/users.js +++ b/server/publications/users.js @@ -1,4 +1,4 @@ -Meteor.publish('user-miniprofile', function(usernames) { +Meteor.publish('user-miniprofile', function (usernames) { check(usernames, Array); // eslint-disable-next-line no-console @@ -19,7 +19,7 @@ Meteor.publish('user-miniprofile', function(usernames) { ); }); -Meteor.publish('user-admin', function() { +Meteor.publish('user-admin', function () { return Meteor.users.find(this.userId, { fields: { isAdmin: 1, @@ -30,7 +30,7 @@ Meteor.publish('user-admin', function() { }); }); -Meteor.publish('user-authenticationMethod', function(match) { +Meteor.publish('user-authenticationMethod', function (match) { check(match, String); return Users.find( { $or: [{ _id: match }, { email: match }, { username: match }] }, @@ -43,3 +43,71 @@ Meteor.publish('user-authenticationMethod', function(match) { }, ); }); + +// update last connection date and last connection average time (in seconds) for a user +// function UpdateLastConnectionDateAndLastConnectionAverageTime(lstUsers) { +// let lastConnectionAverageTime; +// lstUsers.forEach((currentUser) => { +// lastConnectionAverageTime = +// currentUser.lastConnectionAverageTimeInSecs !== undefined +// ? currentUser.lastConnectionAverageTimeInSecs +// : 0; +// lastConnectionAverageTime = +// currentUser.lastConnectionDate !== undefined +// ? ((new Date().getTime() - currentUser.lastConnectionDate.getTime()) / +// 1000 + +// lastConnectionAverageTime) / +// 2 +// : 0; + +// Users.update(currentUser._id, { +// $set: { +// lastConnectionDate: new Date(), +// lastConnectionAverageTimeInSecs: parseInt(lastConnectionAverageTime), +// }, +// }); +// }); +// } + +if (Meteor.isServer) { + Meteor.onConnection(function (connection) { + // console.log( + // 'Meteor.server.stream_server.open_sockets', + // Meteor.server.stream_server.open_sockets, + // ); + //console.log('connection.Id on connection...', connection.id); + // connection.onClose(() => { + // console.log('connection.Id on close...', connection.id); + // // Get all user that were connected to this socket + // // And update last connection date and last connection average time (in seconds) for each user + // let lstOfUserThatWasConnectedToThisSocket = Users.find({ + // lastconnectedSocketId: connection.id, + // }).fetch(); + // if ( + // lstOfUserThatWasConnectedToThisSocket !== undefined && + // lstOfUserThatWasConnectedToThisSocket.length > 0 + // ) { + // console.log({ lstOfUserThatWasConnectedToThisSocket }); + // UpdateLastConnectionDateAndLastConnectionAverageTime( + // lstOfUserThatWasConnectedToThisSocket, + // ); + // } + // }); + + // Meteor.server.stream_server.open_sockets.forEach((socket) => + // console.log('meteor session', socket._meteorSession.userId), + // ); + + // update last connected user date (neddeed for one of the KPI) + Meteor.server.stream_server.open_sockets.forEach( + (socket) => + //console.log('meteor session', socket._meteorSession.userId), + socket._meteorSession?.userId !== null && + Users.update(socket._meteorSession.userId, { + $set: { + lastConnectionDate: new Date(), + }, + }), + ); + }); +}