aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/server/Client.ts13
-rw-r--r--src/server/DashStats.ts198
-rw-r--r--src/server/Message.ts2
-rw-r--r--src/server/stats/userLoginStats.csv82
-rw-r--r--src/server/websocket.ts23
5 files changed, 193 insertions, 125 deletions
diff --git a/src/server/Client.ts b/src/server/Client.ts
index be1ffc2ba..e6f953712 100644
--- a/src/server/Client.ts
+++ b/src/server/Client.ts
@@ -1,22 +1,11 @@
-import { action, computed } from "mobx";
+import { computed } from "mobx";
export class Client {
private _guid: string;
- // private operations: number;
constructor(guid: string) {
this._guid = guid;
- // this.operations = 0;
}
- // @computed public get OPERATIONS(): number {
- // return this.operations;
- // }
-
- // @action
- // public setOperations(newOperations: number): void {
- // this.operations = newOperations;
- // }
-
@computed public get GUID(): string { return this._guid; }
} \ No newline at end of file
diff --git a/src/server/DashStats.ts b/src/server/DashStats.ts
index a10b28608..8d341db63 100644
--- a/src/server/DashStats.ts
+++ b/src/server/DashStats.ts
@@ -1,35 +1,72 @@
import { cyan, magenta } from 'colors';
-import { Request, Response } from 'express';
-import { Server } from 'http';
+import { Response } from 'express';
import SocketIO from 'socket.io';
import { timeMap } from './ApiManagers/UserManager';
import { WebSocket } from './websocket';
const fs = require('fs');
-const csv = require('csv-parser');
+/**
+ * DashStats focuses on tracking user data for each session.
+ *
+ * This includes time connected, number of operations, and
+ * the rate of their operations
+ */
export namespace DashStats {
+ export const SAMPLING_INTERVAL = 1000; // in milliseconds (ms) - Time interval to update the frontend.
+ export const RATE_INTERVAL = 10; // in seconds (s) - Used to calculate rate
+
const statsCSVFilename = './src/server/stats/userLoginStats.csv';
const columns = ['USERNAME', 'ACTION', 'TIME'];
- interface SocketPair {
+ /**
+ * UserStats holds the stats associated with a particular user.
+ */
+ interface UserStats {
socketId: string;
username: string;
time: string;
operations: number;
+ rate: number;
+ }
+
+ /**
+ * UserLastOperations is the queue object for each user
+ * storing their past operations.
+ */
+ interface UserLastOperations {
+ sampleOperations: number; // stores how many operations total are in this rate section (10 sec, for example)
+ lastSampleOperations: number; // stores how many total operations were recorded at the last sample
+ previousOperationsQueue: number[]; // stores the operations to calculate rate.
}
+ /**
+ * StatsDataBundle represents an object that will be sent to the frontend view
+ * on each websocket update.
+ */
+ interface StatsDataBundle {
+ connectedUsers: UserStats[];
+ }
+
+ /**
+ * CSVStore represents how objects will be stored in the CSV
+ */
interface CSVStore {
USERNAME: string;
ACTION: string;
TIME: string;
}
+ /**
+ * ServerTraffic describes the current traffic going to the backend.
+ */
enum ServerTraffic {
NOT_BUSY,
BUSY,
VERY_BUSY
}
+ // These values can be changed after further testing how many
+ // users correspond to each traffic level in Dash.
const BUSY_SERVER_BOUND = 2;
const VERY_BUSY_SERVER_BOUND = 3;
@@ -39,31 +76,47 @@ export namespace DashStats {
"Very Busy"
]
+ // lastUserOperations maps each username to a UserLastOperations
+ // structure
+ export const lastUserOperations = new Map<string, UserLastOperations>();
+
+ /**
+ * handleStats is called when the /stats route is called, providing a JSON
+ * object with relevant stats. In this case, we return the number of
+ * current connections and
+ * @param res Response object from Express
+ */
export function handleStats(res: Response) {
- let current = getCurrentConnections();
+ let current = getCurrentStats();
const results: CSVStore[] = [];
res.json({
- message: 'welcome to stats',
currentConnections: current.length,
socketMap: current,
});
+ }
+
+ /**
+ * getUpdatedStatesBundle() sends an updated copy of the current stats to the
+ * frontend /statsview route via websockets.
+ *
+ * @returns a StatsDataBundle that is sent to the frontend view on each websocket update
+ */
+ export function getUpdatedStatsBundle(): StatsDataBundle {
+ let current = getCurrentStats();
- // fs.createReadStream(statsCSVFilename)
- // .pipe(csv())
- // .on('data', (data: any) => results.push(data))
- // .on('end', () => {
- // console.log(results);
- // res.json({
- // message: 'welcome to stats',
- // currentConnections: current.length,
- // socketMap: current,
- // results: results,
- // });
- // });
+ return {
+ connectedUsers: current,
+ }
}
+ /**
+ * handleStatsView() is called when the /statsview route is called. This
+ * will use pug to render a frontend view of the current stats
+ *
+ * @param res
+ */
export function handleStatsView(res: Response) {
- let current = getCurrentConnections();
+ let current = getCurrentStats();
let connectedUsers = current.map((socketPair) => {
return socketPair.time + " - " + socketPair.username + " Operations: " + socketPair.operations;
@@ -80,17 +133,23 @@ export namespace DashStats {
res.render("stats.pug", {
title: "Dash Stats",
- numConnections: current.length,
+ numConnections: connectedUsers.length,
serverTraffic: serverTraffic,
serverTrafficMessage : serverTrafficMessages[serverTraffic],
connectedUsers: connectedUsers
});
}
+ /**
+ * logUserLogin() writes a login event to the CSV file.
+ *
+ * @param username the username in the format of "username@domain.com logged in"
+ * @param socket the websocket associated with the current connection
+ */
export function logUserLogin(username: string | undefined, socket: SocketIO.Socket) {
if (!(username === undefined)) {
let currentDate = new Date();
- // console.log(magenta(`User ${username.split(' ')[0]} logged in at: ${currentDate.toISOString()}`));
+ console.log(magenta(`User ${username.split(' ')[0]} logged in at: ${currentDate.toISOString()}`));
let toWrite: CSVStore = {
USERNAME : username,
@@ -105,10 +164,15 @@ export namespace DashStats {
}
}
+ /**
+ * logUserLogout() writes a logout event to the CSV file.
+ *
+ * @param username the username in the format of "username@domain.com logged in"
+ * @param socket the websocket associated with the current connection.
+ */
export function logUserLogout(username: string | undefined, socket: SocketIO.Socket) {
if (!(username === undefined)) {
let currentDate = new Date();
- // console.log(magenta(`User ${username.split(' ')[0]} logged out at: ${currentDate.toISOString()}`));
let statsFile = fs.createWriteStream(statsCSVFilename, { flags: "a"});
let toWrite: CSVStore = {
@@ -121,10 +185,79 @@ export namespace DashStats {
}
}
- function getCurrentConnections(): SocketPair[] {
- console.log("timeMap: " + timeMap);
- console.log("clients:" + WebSocket.clients);
- let socketPairs: SocketPair[] = [];
+ /**
+ * getLastOperationsOrDefault() is a helper method that will attempt
+ * to query the lastUserOperations map for a specified username. If the
+ * username is not in the map, an empty UserLastOperations object is returned.
+ * @param username
+ * @returns the user's UserLastOperations structure or an empty
+ * UserLastOperations object (All values set to 0) if the username is not found.
+ */
+ function getLastOperationsOrDefault(username: string): UserLastOperations {
+ if(lastUserOperations.get(username) === undefined) {
+ let initializeOperationsQueue = [];
+ for(let i = 0; i < RATE_INTERVAL; i++) {
+ initializeOperationsQueue.push(0);
+ }
+ return {
+ sampleOperations: 0,
+ lastSampleOperations: 0,
+ previousOperationsQueue: initializeOperationsQueue
+ }
+ }
+ return lastUserOperations.get(username)!;
+ }
+
+ /**
+ * updateLastOperations updates a specific user's UserLastOperations information
+ * for the current sampling cycle. The method removes old/outdated counts for
+ * operations from the queue and adds new data for the current sampling
+ * cycle to the queue, updating the total count as it goes.
+ * @param lastOperationData the old UserLastOperations data that must be updated
+ * @param currentOperations the total number of operations measured for this sampling cycle.
+ * @returns the udpated UserLastOperations structure.
+ */
+ function updateLastOperations(lastOperationData: UserLastOperations, currentOperations: number): UserLastOperations {
+ // create a copy of the UserLastOperations to modify
+ let newLastOperationData: UserLastOperations = {
+ sampleOperations: lastOperationData.sampleOperations,
+ lastSampleOperations: lastOperationData.lastSampleOperations,
+ previousOperationsQueue: lastOperationData.previousOperationsQueue.slice()
+ }
+
+ let newSampleOperations = newLastOperationData.sampleOperations;
+ newSampleOperations -= newLastOperationData.previousOperationsQueue.shift()!; // removes and returns the first element of the queue
+ let operationsThisCycle = currentOperations - lastOperationData.lastSampleOperations;
+ newSampleOperations += operationsThisCycle; // add the operations this cycle to find out what our count for the interval should be (e.g operations in the last 10 seconds)
+
+ // update values for the copy object
+ newLastOperationData.sampleOperations = newSampleOperations;
+
+ newLastOperationData.previousOperationsQueue.push(operationsThisCycle);
+ newLastOperationData.lastSampleOperations = currentOperations;
+
+ return newLastOperationData;
+ }
+
+ /**
+ * getUserOperationsOrDefault() is a helper method to get the user's total
+ * operations for the CURRENT sampling interval. The method will return 0
+ * if the username is not in the userOperations map.
+ * @param username the username to search the map for
+ * @returns the total number of operations recorded up to this sampling cycle.
+ */
+ function getUserOperationsOrDefault(username: string): number {
+ return WebSocket.userOperations.get(username) === undefined ? 0 : WebSocket.userOperations.get(username)!
+ }
+
+ /**
+ * getCurrentStats() calculates the total stats for this cycle. In this case,
+ * getCurrentStats() returns an Array of UserStats[] objects describing
+ * the stats for each user
+ * @returns an array of UserStats storing data for each user at the current moment.
+ */
+ function getCurrentStats(): UserStats[] {
+ let socketPairs: UserStats[] = [];
for (let [key, value] of WebSocket.socketMap) {
let username = value.split(' ')[0];
let connectionTime = new Date(timeMap[username]);
@@ -132,19 +265,28 @@ export namespace DashStats {
let connectionTimeString = connectionTime.toLocaleDateString() + " " + connectionTime.toLocaleTimeString();
if (!key.disconnected) {
+ let lastRecordedOperations = getLastOperationsOrDefault(username);
+ let currentUserOperationCount = getUserOperationsOrDefault(username);
+
socketPairs.push({
socketId: key.id,
username: username,
time: connectionTimeString.includes("Invalid Date") ? "" : connectionTimeString,
operations : WebSocket.userOperations.get(username) ? WebSocket.userOperations.get(username)! : 0,
+ rate: lastRecordedOperations.sampleOperations
});
+ lastUserOperations.set(username, updateLastOperations(lastRecordedOperations,currentUserOperationCount));
}
}
- console.log(socketPairs);
- // console.log([...WebSocket.clients.entries()]);
return socketPairs;
}
+ /**
+ * convertToCSV() is a helper method that stringifies a CSVStore object
+ * that can be written to the CSV file later.
+ * @param dataObject the object to stringify
+ * @returns the object as a string.
+ */
function convertToCSV(dataObject: CSVStore): string {
return `${dataObject.USERNAME},${dataObject.ACTION},${dataObject.TIME}\n`;
}
diff --git a/src/server/Message.ts b/src/server/Message.ts
index d87ae5027..8f0af08bc 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -94,4 +94,6 @@ export namespace MessageStore {
export const YoutubeApiQuery = new Message<YoutubeQueryInput>("Youtube Api Query");
export const DeleteField = new Message<string>("Delete field");
export const DeleteFields = new Message<string[]>("Delete fields");
+
+ export const UpdateStats = new Message<string>("updatestats");
}
diff --git a/src/server/stats/userLoginStats.csv b/src/server/stats/userLoginStats.csv
index d5e56502c..23bcef885 100644
--- a/src/server/stats/userLoginStats.csv
+++ b/src/server/stats/userLoginStats.csv
@@ -1,75 +1,7 @@
-USER,ACTION,TIME
-aaa@gmail.com,loggedIn,2023-03-18T20:10:29.928Z
-guest,loggedIn,2023-03-18T20:10:47.384Z
-aaa@gmail.com,loggedOut,2023-03-18T20:10:55.364Z
-aaa@gmail.com,loggedIn,2023-03-18T20:11:15.879Z
-guest,loggedIn,2023-03-18T20:18:03.724Z
-aaa@gmail.com,loggedIn,2023-03-18T20:18:03.860Z
-guest,loggedOut,2023-03-18T20:19:00.211Z
-guest,loggedIn,2023-03-18T20:19:03.087Z
-guest,loggedOut,2023-03-18T20:19:50.668Z
-boo15869@gmail.com,loggedIn,2023-03-18T20:20:17.890Z
-boo15869@gmail.com,loggedOut,2023-03-18T20:21:03.542Z
-boo15869@gmail.com,loggedIn,2023-03-18T20:21:06.149Z
-boo15869@gmail.com,loggedOut,2023-03-18T20:21:51.874Z
-a@gmail.com,loggedIn,2023-03-18T20:22:02.122Z
-aaa@gmail.com,loggedOut,2023-03-18T20:22:42.882Z
-aaa@gmail.com,loggedIn,2023-03-18T20:22:45.631Z
-aaa@gmail.com,loggedIn,2023-03-18T20:25:34.658Z
-a@gmail.com,loggedIn,2023-03-18T20:25:34.681Z
-aaa@gmail.com,loggedIn,2023-03-18T20:29:04.297Z
-a@gmail.com,loggedIn,2023-03-18T20:29:08.701Z
-a@gmail.com,loggedIn,2023-03-18T20:29:18.565Z
-aaa@gmail.com,loggedIn,2023-03-18T20:29:21.974Z
-aaa@gmail.com,loggedIn,2023-03-18T20:29:51.477Z
-a@gmail.com,loggedIn,2023-03-18T20:29:51.489Z
-aaa@gmail.com,loggedIn,2023-03-18T20:30:15.011Z
-aaa@gmail.com,loggedIn,2023-03-18T20:30:57.818Z
-a@gmail.com,loggedIn,2023-03-18T20:30:57.838Z
-aaa@gmail.com,loggedIn,2023-03-18T20:31:12.061Z
-a@gmail.com,loggedIn,2023-03-18T20:31:12.080Z
-a@gmail.com,loggedIn,2023-03-18T20:31:19.447Z
-aaa@gmail.com,loggedIn,2023-03-18T20:31:22.738Z
-aaa@gmail.com,loggedIn,2023-03-18T20:32:36.919Z
-a@gmail.com,loggedIn,2023-03-18T20:32:36.929Z
-a@gmail.com,loggedIn,2023-03-18T20:32:56.212Z
-aaa@gmail.com,loggedIn,2023-03-18T20:32:59.300Z
-aaa@gmail.com,loggedIn,2023-03-18T20:34:27.543Z
-a@gmail.com,loggedIn,2023-03-18T20:34:27.570Z
-a@gmail.com,loggedIn,2023-03-18T20:34:35.299Z
-aaa@gmail.com,loggedIn,2023-03-18T20:34:35.302Z
-a@gmail.com,loggedIn,2023-03-18T20:34:51.579Z
-aaa@gmail.com,loggedIn,2023-03-18T20:34:52.392Z
-a@gmail.com,loggedIn,2023-03-18T20:35:08.509Z
-aaa@gmail.com,loggedIn,2023-03-18T20:35:15.202Z
-a@gmail.com,loggedIn,2023-03-18T20:36:46.796Z
-aaa@gmail.com,loggedIn,2023-03-18T20:36:51.756Z
-a@gmail.com,loggedIn,2023-03-18T20:36:55.286Z
-a@gmail.com,loggedIn,2023-03-18T20:40:06.226Z
-a@gmail.com,loggedIn,2023-03-18T20:40:18.474Z
-aaa@gmail.com,loggedIn,2023-03-18T20:40:31.894Z
-a@gmail.com,loggedIn,2023-03-18T20:40:31.903Z
-a@gmail.com,loggedIn,2023-03-18T20:42:25.301Z
-aaa@gmail.com,loggedIn,2023-03-18T20:42:31.182Z
-aaa@gmail.com,loggedOut,2023-03-18T20:43:05.741Z
-aaa@gmail.com,loggedIn,2023-03-18T20:43:09.203Z
-a@gmail.com,loggedOut,2023-03-18T20:45:24.343Z
-a@gmail.com,loggedIn,2023-03-18T20:45:27.207Z
-aaa@gmail.com,loggedIn,2023-03-18T20:46:15.618Z
-a@gmail.com,loggedIn,2023-03-18T20:46:56.163Z
-a@gmail.com,loggedOut,2023-03-18T20:48:37.464Z
-a@gmail.com,loggedIn,2023-03-18T20:48:40.562Z
-a@gmail.com,loggedOut,2023-03-18T20:50:32.478Z
-a@gmail.com,loggedIn,2023-03-18T20:50:40.384Z
-a@gmail.com,loggedOut,2023-03-18T20:51:34.159Z
-a@gmail.com,loggedIn,2023-03-18T20:51:49.206Z
-a@gmail.com,loggedOut,2023-03-18T20:52:04.673Z
-qw@gmail.com,loggedIn,2023-03-18T20:52:36.270Z
-qw@gmail.com,loggedIn,2023-03-18T20:53:58.175Z
-aaa@gmail.com,loggedIn,2023-03-18T20:53:58.204Z
-aaa@gmail.com,loggedIn,2023-03-18T20:54:20.518Z
-qw@gmail.com,loggedIn,2023-03-18T20:54:24.225Z
-qw@gmail.com,loggedIn,2023-03-18T20:54:37.007Z
-aaa@gmail.com,loggedIn,2023-03-18T20:54:39.959Z
-aaa@gmail.com,loggedOut,2023-03-18T20:55:55.199Z
-qw@gmail.com,loggedOut,2023-03-18T20:55:59.010Z
+USER,ACTION,TIMEaaa@gmail.com,loggedIn,2023-04-08T20:08:17.533Z
+aaa@gmail.com,loggedIn,2023-04-08T20:14:32.460Z
+aaa@gmail.com,loggedIn,2023-04-08T20:14:44.884Z
+aaa@gmail.com,loggedIn,2023-04-08T20:14:56.854Z
+aaa@gmail.com,loggedIn,2023-04-08T20:16:59.747Z
+aaa@gmail.com,loggedIn,2023-04-08T20:56:50.759Z
+aaa@gmail.com,loggedIn,2023-04-08T20:56:58.175Z
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index 54944e944..5d1390d37 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -1,4 +1,4 @@
-import { blue, magenta, yellow } from 'colors';
+import { blue } from 'colors';
import * as express from 'express';
import { createServer, Server } from 'https';
import { networkInterfaces } from 'os';
@@ -22,7 +22,6 @@ import { resolvedPorts } from './server_Initialization';
export namespace WebSocket {
export let _socket: Socket;
export const clients: { [key: string]: Client } = {};
- // export const clients = new Map<string, Client>();
export const socketMap = new Map<SocketIO.Socket, string>();
export const userOperations = new Map<string, number>();
export let disconnect: Function;
@@ -52,6 +51,8 @@ export namespace WebSocket {
next();
});
+ socket.emit(MessageStore.UpdateStats.Message, DashStats.getUpdatedStatsBundle())
+
// convenience function to log server messages on the client
function log(message?: any, ...optionalParams: any[]) {
socket.emit('log', ['Message from server:', message, ...optionalParams]);
@@ -109,6 +110,8 @@ export namespace WebSocket {
}
});
+
+
Utils.Emit(socket, MessageStore.Foo, 'handshooken');
Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
@@ -142,6 +145,12 @@ export namespace WebSocket {
socket.disconnect(true);
};
});
+
+ setInterval(function() {
+ // Utils.Emit(socket, MessageStore.UpdateStats, DashStats.getUpdatedStatsBundle());
+
+ io.emit(MessageStore.UpdateStats.Message, DashStats.getUpdatedStatsBundle())
+ }, DashStats.SAMPLING_INTERVAL);
}
function processGesturePoints(socket: Socket, content: GestureContent) {
@@ -185,13 +194,9 @@ export namespace WebSocket {
function barReceived(socket: SocketIO.Socket, userEmail: string) {
clients[userEmail] = new Client(userEmail.toString());
- // clients.set(userEmail, new Client(userEmail.toString()));
const currentdate = new Date();
const datetime = currentdate.getDate() + '/' + (currentdate.getMonth() + 1) + '/' + currentdate.getFullYear() + ' @ ' + currentdate.getHours() + ':' + currentdate.getMinutes() + ':' + currentdate.getSeconds();
console.log(blue(`user ${userEmail} has connected to the web socket at: ${datetime}`));
- // console.log(magenta(`currently connected: ${[...clients.entries()]}`));
- // console.log(magenta('socket map below'));
- // console.log([...socketMap.entries()]);
printActiveUsers();
timeMap[userEmail] = Date.now();
@@ -321,7 +326,8 @@ export namespace WebSocket {
const remListItems = diff.diff.$set[updatefield].fields;
const curList = (curListItems as any)?.fields?.[updatefield.replace('fields.', '')]?.fields.filter((f: any) => f !== null) || [];
diff.diff.$set[updatefield].fields = curList?.filter(
- (curItem: any) => !remListItems.some((remItem: any) => (remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : remItem === curItem))
+ (curItem: any) => !remListItems.some((remItem: any) => (remItem.fieldId ? remItem.fieldId === curItem.fieldId :
+ remItem.heading ? remItem.heading === curItem.heading : remItem === curItem))
);
const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length;
delete diff.diff.length;
@@ -367,11 +373,8 @@ export namespace WebSocket {
var CurUser: string | undefined = undefined;
function UpdateField(socket: Socket, diff: Diff) {
- console.log(magenta(`1 OP ${socketMap.get(socket)}`));
-
let currentUsername = socketMap.get(socket)!.split(' ')[0];
userOperations.set(currentUsername, userOperations.get(currentUsername) !== undefined ? userOperations.get(currentUsername)! + 1 : 0);
- console.log(yellow("Total Operations: " + userOperations.get(currentUsername)));
if (CurUser !== socketMap.get(socket)) {
CurUser = socketMap.get(socket);