Browse Source

feat(metrics KPI): Added some metrics KPI Datas

Emile Ndagijimana 2 năm trước cách đây
mục cha
commit
d323c1e51e
3 tập tin đã thay đổi với 335 bổ sung73 xóa
  1. 145 0
      models/server/metrics.js
  2. 119 70
      models/users.js
  3. 71 3
      server/publications/users.js

+ 145 - 0
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());
+    }
+  });
+});

+ 119 - 70
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({
    * <li> 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({
    * <li> 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({
    * <li> 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,

+ 71 - 3
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(),
+          },
+        }),
+    );
+  });
+}