├── .babelrc ├── .eslintrc.json ├── .gitignore ├── README.md ├── __tests-server__ ├── mock-server.ts └── sampleTestVariables.ts ├── __tests__ ├── allRPSfinished.ts ├── emitPercentage.ts ├── mockServer.ts ├── server.ts └── singleRPSfinished.ts ├── dist ├── README.md ├── client │ ├── 4d65afbeee5c93f19eb78c16a6f541f7.svg │ ├── c82b0acf13f3f94459071d557ecee477.png │ ├── favicon.svg │ ├── index.html │ ├── main.js │ └── main.js.LICENSE.txt ├── middleware │ ├── index.d.ts │ └── index.js ├── package.json ├── server │ ├── app.d.ts │ ├── app.js │ ├── helperFunctions.d.ts │ ├── helperFunctions.js │ ├── helpers │ │ ├── allRPSfinished.d.ts │ │ ├── allRPSfinished.js │ │ ├── emitPercentage.d.ts │ │ ├── emitPercentage.js │ │ ├── processData.d.ts │ │ ├── processData.js │ │ ├── processLastMiddleware.d.ts │ │ ├── processLastMiddleware.js │ │ ├── sendRequests.d.ts │ │ ├── sendRequests.js │ │ ├── sendRequestsAtRPS.d.ts │ │ ├── sendRequestsAtRPS.js │ │ ├── singleRPSfinished.d.ts │ │ └── singleRPSfinished.js │ ├── index.d.ts │ ├── index.js │ ├── interfaces.d.ts │ ├── interfaces.js │ ├── sampleTestVariables.d.ts │ ├── sampleTestVariables.js │ ├── server.d.ts │ ├── server.js │ ├── testrouter.d.ts │ └── testrouter.js └── yarn.lock ├── jest.config.ts ├── package.json ├── splash ├── app.tsx ├── bundle │ ├── .well-known │ │ └── acme-challenge │ │ │ ├── 7z1xjuRhh4VD8Kbn8dcds-I-TaggOL2m-m8mMQ4KFXc │ │ │ └── JYbvNgrdXxoNqqzOq7PL4OdCStEdaFHkt3fdr27mT_E │ ├── 17e66cea79c0a77138e0.png │ ├── 487337271d3e589bb2ca.jpg │ ├── 4d65afbeee5c93f19eb78c16a6f541f7.svg │ ├── 5a8d8efcb625ceadace20cdc3e211f8c.svg │ ├── 6742f36679004f281e61a3c7c4bfecc8.svg │ ├── 6ef6d3baf95397d816ffb4e6fe14daf9.svg │ ├── 9e6a7306d2f701aff33a.gif │ ├── _redirects │ ├── a64cac4786b3a3f26ee2ae6dbecca056.svg │ ├── ad930f133b588618a67b.jpg │ ├── b138d6d7c0c8f27ac4f8.jpg │ ├── c82b0acf13f3f94459071d557ecee477.png │ ├── d151a06aa6206494cbdb.gif │ ├── d6f0de647e53361b017989b0a1105021.svg │ ├── dd7777d0e8f795b523dc376f99063ff5.svg │ ├── demo │ │ ├── index.html │ │ ├── main.js │ │ └── main.js.LICENSE.txt │ ├── f4904cece825a96909a0c440c50feb20.svg │ ├── favicon.svg │ ├── index.html │ ├── main.8dc9fad69eb45e05a276.js │ ├── main.8dc9fad69eb45e05a276.js.LICENSE.txt │ └── netlify.toml ├── custom.d.ts ├── img │ ├── Asset.svg │ ├── exportdelete.gif │ ├── favicon.svg │ ├── favicon2.svg │ ├── favicon3.svg │ ├── favicon5.svg │ ├── github.svg │ ├── install.png │ ├── linkedin.svg │ ├── logotext.svg │ ├── npm.svg │ ├── singleroute.gif │ ├── team1.jpg │ ├── team2.jpg │ ├── team3.jpg │ └── twitter.svg ├── index.html ├── index.tsx ├── style.scss ├── tocopy │ ├── .well-known │ │ └── acme-challenge │ │ │ ├── 7z1xjuRhh4VD8Kbn8dcds-I-TaggOL2m-m8mMQ4KFXc │ │ │ └── JYbvNgrdXxoNqqzOq7PL4OdCStEdaFHkt3fdr27mT_E │ ├── _redirects │ └── netlify.toml ├── tsconfig.json ├── webpack.dev.config.ts └── webpack.prod.config.ts ├── src ├── client │ ├── app.tsx │ ├── components │ │ ├── darkModeSwitch.tsx │ │ ├── modal.tsx │ │ ├── navigation.tsx │ │ ├── resultsComponents │ │ │ ├── graphs.tsx │ │ │ ├── tables.tsx │ │ │ ├── verticalTabLabels.tsx │ │ │ └── verticalTabs.tsx │ │ └── testConfigComponents │ │ │ ├── RangeSliders.tsx │ │ │ ├── SingleSlider.tsx │ │ │ ├── TargetInputSingle.tsx │ │ │ ├── TargetInputs.tsx │ │ │ ├── buttonStopSpinner.tsx │ │ │ ├── buttonsstartstop.tsx │ │ │ ├── highrpswarning.tsx │ │ │ └── testProgress.tsx │ ├── img │ │ ├── Asset.svg │ │ ├── favicon.svg │ │ └── noresults.png │ ├── index.html │ ├── index.tsx │ ├── interfaces.ts │ ├── pages │ │ ├── results.tsx │ │ └── testconfigpage.tsx │ ├── state │ │ ├── actions │ │ │ └── actions.ts │ │ ├── hooks.ts │ │ ├── reducers │ │ │ ├── configReducer.ts │ │ │ └── initialState.ts │ │ └── store.ts │ └── tsconfig.json ├── custom.d.ts ├── middleware │ ├── index.ts │ └── tsconfig.json ├── server │ ├── app.ts │ ├── helpers │ │ ├── allRPSfinished.ts │ │ ├── emitPercentage.ts │ │ ├── processData.ts │ │ ├── processLastMiddleware.ts │ │ ├── sendRequests.ts │ │ ├── sendRequestsAtRPS.ts │ │ └── singleRPSfinished.ts │ ├── index.ts │ ├── interfaces.ts │ ├── server.ts │ ├── testrouter.ts │ └── tsconfig.json └── tsconfig.json ├── tsconfig.json ├── webpack.dev.config.ts ├── webpack.prod.config.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], 3 | "plugins": [ 4 | [ 5 | "@babel/plugin-transform-runtime", 6 | { 7 | "regenerator": true 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2015, 5 | "sourceType": "module" 6 | }, 7 | "plugins": ["@typescript-eslint", "react-hooks"], 8 | "extends": ["plugin:react/recommended", "plugin:@typescript-eslint/recommended"], 9 | "ignorePatterns": ["dist/**/*.js"], 10 | "rules": { 11 | "react-hooks/rules-of-hooks": "error", 12 | "react-hooks/exhaustive-deps": "warn", 13 | "react/prop-types": "off" 14 | }, 15 | "settings": { 16 | "react": { 17 | "pragma": "React", 18 | "version": "detect" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | 105 | # MacOs specific folder files 106 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jagtester 2 | 3 | [![npm version](https://badge.fury.io/js/jagtester.svg)](https://badge.fury.io/js/jagtester) 4 | 5 | There is a big community of developers using Express JS with Node.js to power their web applications. Our team of developers love the simplicity of Express and have used it numerous times in our applications. But, as developers who like to optimize the performance of our application, we wanted to test the performance of the server under heavy load, so we started looking around for solutions. Sure, there are a lot of products that can help developers test the performance of the server, but we wanted to go much deeper. What if we could show how each middleware within the server performs before sending out the response? What if we wanted to compare how the performance scales based on the requests per second? We couldn’t find the product we needed, so we did what every great developer team does, we created our own solution. 6 | Enter [Jagtester](https://jagtester.com/). 7 | 8 | ## Installing 9 | 10 | For npm: 11 | 12 | ```bash 13 | npm install jagtester 14 | ``` 15 | 16 | or yarn: 17 | 18 | ```bash 19 | yarn add jagtester 20 | ``` 21 | 22 | ## Usage 23 | 24 | It is very easy to use jagtester, just install it and include it as a middleware in your Express server, and you are ready to go. 25 | 26 | ```JavaScript 27 | const jagtester = require('jagtester'); 28 | app.use(jagtester(app)); 29 | ``` 30 | 31 | **NOTE: you need to pass your Express app to jagtester middleware, so it can enable middleware level reporting** 32 | 33 | **NOTE: Make sure to put the Jagtester above all other global middlewares (for ex. express.static()), because the test will not be able to start if other middlewares are sending a response before the request can hit the jagtester middleware.** 34 | 35 | Now just run the Jagtester with 36 | 37 | ```bash 38 | npx jagtester 39 | ``` 40 | 41 | and it will start up jagtester on a local server port 15000 (or next available port). 42 | 43 | With our intuitive web interface, you will ba able to configure the test and run it. Make sure to also start your own server you want to use Jagtester on. 44 | 45 | ## Features 46 | 47 | - Allows users to fully customize testing based on the needs of their application. Here are the options you can configure: 48 | - Requests per second (RPS) interval. 49 | - Starting and ending RPS. 50 | - Time to stay at each RPS range. 51 | - Ability to simulate traffic to one or more targets, with a specified percentage of the load going to each target. 52 | - Stop feature, which will allow the user to stop the current test if it is taking too long and get the results that have been generated so far. 53 | - Displays graphs to show a breakdown of all routes along with the error percentage. 54 | - Graphs to display details for each route, broken down to the middleware level to analyze the performance of each of the routes. 55 | - Export functionality to generate result reports, either from all tests or only single test results, with an ability to delete the collected results. 56 | - And for the lover of dark mode, we got you covered with an option to switch between light and dark modes. 57 | 58 | ## We are open to any issues! 59 | -------------------------------------------------------------------------------- /__tests-server__/mock-server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import jagtester from '../src/middleware/index'; 3 | // import getMiddleware from '../lib/index'; 4 | 5 | const app = express(); 6 | // const port = 5001; 7 | 8 | app.use(jagtester(app)); 9 | 10 | app.get('/', (req, res) => { 11 | res.sendStatus(200); 12 | }); 13 | 14 | // const mockServer = app.listen(port, () => console.log(`Running on on port ${port}`)); 15 | 16 | // export default mockServer; 17 | export default app; 18 | -------------------------------------------------------------------------------- /__tests-server__/sampleTestVariables.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import { Server } from 'socket.io'; 3 | import AbortController from 'abort-controller'; 4 | import { 5 | ioSocketCommands, 6 | TimeArrRoutes, 7 | TestConfigData, 8 | GlobalVariables, 9 | PulledDataFromTest, 10 | } from '../src/server/interfaces'; 11 | 12 | let timeOutArray, 13 | abortController, 14 | globalTestConfig: TestConfigData, 15 | timeArrRoutes: TimeArrRoutes, 16 | pulledDataFromTest: PulledDataFromTest, 17 | globalVariables: GlobalVariables, 18 | io: Server; 19 | 20 | const initializeVariables: () => void = () => { 21 | io = new Server(); 22 | io.emit = jest.fn(); 23 | timeOutArray = [setTimeout(() => 0, 1000)]; 24 | clearTimeout(timeOutArray[0]); 25 | abortController = new AbortController(); 26 | globalTestConfig = { 27 | rpsInterval: 100, 28 | startRPS: 100, 29 | endRPS: 100, 30 | testLength: 1, 31 | inputsData: [ 32 | { 33 | method: 'GET', 34 | targetURL: 'http://localhost:3000', 35 | percentage: 100, 36 | jagTesterEnabled: true, 37 | }, 38 | ], 39 | }; 40 | 41 | timeArrRoutes = { 42 | // this key is used as the route name 43 | '/': { 44 | //this key is used as the rps number 45 | '100': { 46 | receivedTotalTime: 800, // 8 milliseconds for total 47 | errorCount: 0, 48 | successfulResCount: 100, 49 | }, 50 | }, 51 | }; 52 | 53 | // 2 average milliseconds for each, and last one should be 0 because the jagtester doesnt calculate the last one 54 | pulledDataFromTest = { 55 | '100': { 56 | //used as route 57 | '/': { 58 | '1': { 59 | reqId: '1', 60 | reqRoute: '/', 61 | middlewares: [ 62 | { fnName: 'fn1', elapsedTime: 1 }, 63 | { fnName: 'fn2', elapsedTime: 1 }, 64 | { fnName: 'fn3', elapsedTime: 1 }, 65 | { fnName: 'fn4', elapsedTime: 0 }, 66 | ], 67 | }, 68 | '2': { 69 | reqId: '2', 70 | reqRoute: '/', 71 | middlewares: [ 72 | { fnName: 'fn1', elapsedTime: 3 }, 73 | { fnName: 'fn2', elapsedTime: 3 }, 74 | { fnName: 'fn3', elapsedTime: 3 }, 75 | { fnName: 'fn4', elapsedTime: 0 }, 76 | ], 77 | }, 78 | }, 79 | }, 80 | }; 81 | 82 | const isTestRunningListener: (val: boolean) => void = (val: boolean) => { 83 | io.emit(ioSocketCommands.testRunningStateChange, val); 84 | }; 85 | globalVariables = { 86 | currentInterval: 100, 87 | errorCount: 0, 88 | successfulResCount: 100, 89 | abortController, 90 | timeArrRoutes, 91 | timeOutArray, 92 | pulledDataFromTest, 93 | isTestRunningInternal: true, 94 | isTestRunningListener, 95 | isTestRunning: true, 96 | agent: new http.Agent({ keepAlive: true }), 97 | }; 98 | }; 99 | // const initializeEmitPercentage = () => { 100 | 101 | // } 102 | export { 103 | globalTestConfig, 104 | timeOutArray, 105 | abortController, 106 | globalVariables, 107 | io, 108 | initializeVariables, 109 | }; 110 | -------------------------------------------------------------------------------- /__tests__/allRPSfinished.ts: -------------------------------------------------------------------------------- 1 | import { ioSocketCommands, middlewareSingle, Jagtestercommands } from '../src/server/interfaces'; 2 | import fetch from 'node-fetch'; 3 | jest.mock('node-fetch', () => 4 | jest.fn(() => { 5 | return Promise.resolve(); 6 | }) 7 | ); 8 | import allRPSfinished from '../src/server/helpers/allRPSfinished'; 9 | import { 10 | globalTestConfig, 11 | abortController, 12 | globalVariables, 13 | io, 14 | initializeVariables, 15 | } from '../__tests-server__/sampleTestVariables'; 16 | 17 | describe('Testing allRPSfinished functionality', () => { 18 | beforeEach(() => { 19 | initializeVariables(); 20 | allRPSfinished(globalTestConfig, io, globalVariables); 21 | }); 22 | it('should assign a new instance of abortcontroller', () => { 23 | expect(globalVariables.abortController).not.toBe(abortController); 24 | }); 25 | it('should not have an aborted signal', () => { 26 | expect(!globalVariables.abortController.signal.aborted); 27 | }); 28 | it('should switch off is test running boolean', () => { 29 | expect(globalVariables.isTestRunning).toEqual(false); 30 | }); 31 | it('should clear timeout array', () => { 32 | expect(globalVariables.timeOutArray.length).toEqual(0); 33 | }); 34 | it('should divide the received total time (sum of all responses) by the response count', () => { 35 | expect(globalVariables.timeArrRoutes['/']['100'].receivedTotalTime).toEqual(8); 36 | }); 37 | it('process middlewares, average them, combine timearrroutes.', () => { 38 | for (const middleware of globalVariables.pulledDataFromTest['100']['/'] 39 | .middlewares as middlewareSingle[]) { 40 | expect(middleware.elapsedTime).toEqual(2); 41 | } 42 | }); 43 | it('should call emit with new test data on IO when done testing', () => { 44 | expect(io.emit).toBeCalledTimes(1); 45 | expect(io.emit).toBeCalledWith(ioSocketCommands.allRPSfinished, [ 46 | expect.objectContaining({ 47 | testTime: expect.anything(), 48 | testData: globalVariables.pulledDataFromTest, 49 | }), 50 | ]); 51 | }); 52 | it('should call fetch with correct header jagtester command', () => { 53 | expect(fetch).toBeCalledWith(globalTestConfig.inputsData[0].targetURL, { 54 | headers: { 55 | jagtestercommand: Jagtestercommands.endTest.toString(), 56 | }, 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /__tests__/emitPercentage.ts: -------------------------------------------------------------------------------- 1 | import emitPercentage from '../src/server/helpers/emitPercentage'; 2 | import { ioSocketCommands } from '../src/server/interfaces'; 3 | 4 | import { 5 | globalVariables, 6 | io, 7 | initializeVariables, 8 | globalTestConfig, 9 | } from '../__tests-server__/sampleTestVariables'; 10 | 11 | describe('Testing emitPercentage functionality', () => { 12 | beforeEach(() => { 13 | initializeVariables(); 14 | emitPercentage(globalVariables, globalTestConfig.startRPS, globalTestConfig.testLength, io); 15 | }); 16 | it('should emit 1 when the testing is finished', () => { 17 | expect(io.emit).toBeCalledTimes(1); 18 | expect(io.emit).toBeCalledWith(ioSocketCommands.currentRPSProgress, 1); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/mockServer.ts: -------------------------------------------------------------------------------- 1 | import mockApp from '../__tests-server__/mock-server'; 2 | import request from 'supertest'; 3 | import { Jagtestercommands } from '../src/server/interfaces'; 4 | // import fetch from 'node-fetch'; 5 | jest.mock('node-fetch', () => 6 | jest.fn(() => { 7 | return Promise.resolve({ 8 | json: () => Promise.resolve({ jagtester: true }), 9 | }); 10 | }) 11 | ); 12 | describe('Mock Server', () => { 13 | it('Should start test server', async () => { 14 | await request(mockApp).get('/').expect(200); 15 | }); 16 | it('Should respond with {jagtester: true} when receives the update layer headers', async () => { 17 | await request(mockApp) 18 | .get('/') 19 | .set({ jagtestercommand: Jagtestercommands.updateLayer.toString() }) 20 | .expect({ jagtester: true }) 21 | .expect(200); 22 | }); 23 | it('Should respond with {} when receives the end test headers without starting test', async () => { 24 | await request(mockApp).get('/'); 25 | await request(mockApp) 26 | .get('/') 27 | .set({ jagtestercommand: Jagtestercommands.endTest.toString() }) 28 | .expect({}) 29 | .expect(200); 30 | }); 31 | it('should return collected data object after updating the layer, sending a request then calling endtest', async () => { 32 | await request(mockApp) 33 | .get('/') 34 | .set({ jagtestercommand: Jagtestercommands.updateLayer.toString() }); 35 | 36 | await request(mockApp).get('/').set({ 37 | jagtestercommand: Jagtestercommands.running.toString(), 38 | jagtesterreqid: 1, 39 | }); 40 | await request(mockApp) 41 | .get('/') 42 | .set({ jagtestercommand: Jagtestercommands.endTest.toString() }) 43 | .set('Accept', 'application/json') 44 | .expect(200) 45 | .then((res) => { 46 | expect(res.body).toEqual( 47 | expect.objectContaining({ 48 | '/': { 49 | '1': { 50 | reqId: '1', 51 | reqRoute: '/', 52 | middlewares: expect.arrayContaining([ 53 | expect.objectContaining({ 54 | fnName: expect.any(String), 55 | elapsedTime: expect.any(Number), 56 | }), 57 | ]), 58 | }, 59 | }, 60 | }) 61 | ); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /__tests__/server.ts: -------------------------------------------------------------------------------- 1 | import { app } from '../src/server/app'; 2 | import request from 'supertest'; 3 | import { globalVariables } from '../src/server/testrouter'; 4 | // import fetch from 'node-fetch'; 5 | jest.mock('node-fetch', () => 6 | jest.fn(() => { 7 | return Promise.resolve({ 8 | json: () => Promise.resolve({ jagtester: true }), 9 | }); 10 | }) 11 | ); 12 | import { globalTestConfig, initializeVariables } from '../__tests-server__/sampleTestVariables'; 13 | 14 | describe('Jag server (mocked fetch)', () => { 15 | beforeAll(() => { 16 | initializeVariables(); 17 | }); 18 | describe('GET', () => { 19 | it('route / should respond with html content', async () => { 20 | await request(app).get('/').expect('Content-Type', /html/).expect(200); 21 | }); 22 | it('route /api/stopTest should respond with 200 and have signal be aborted', async () => { 23 | await request(app) 24 | .get('/api/stopTest') 25 | .expect(200) 26 | .then(() => { 27 | expect(globalVariables.abortController.signal.aborted); 28 | }); 29 | }); 30 | }); 31 | 32 | describe('POST /api/checkjagtester', () => { 33 | it('Should return jagtester true', async () => { 34 | await request(app) 35 | .post('/api/checkjagtester') 36 | .send({ 37 | inputURL: globalTestConfig.inputsData[0].targetURL, 38 | method: globalTestConfig.inputsData[0].method, 39 | }) 40 | .expect('Content-Type', /json/) 41 | .expect({ jagtester: true }) 42 | .expect(200); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/singleRPSfinished.ts: -------------------------------------------------------------------------------- 1 | import singleRPSfinished from '../src/server/helpers/singleRPSfinished'; 2 | import { ioSocketCommands, Jagtestercommands } from '../src/server/interfaces'; 3 | import fetch from 'node-fetch'; 4 | import { 5 | globalVariables, 6 | io, 7 | initializeVariables, 8 | globalTestConfig, 9 | } from '../__tests-server__/sampleTestVariables'; 10 | 11 | jest.mock('node-fetch', () => 12 | jest.fn(() => { 13 | return Promise.resolve({ 14 | json: () => 15 | Promise.resolve( 16 | globalVariables.pulledDataFromTest[globalVariables.currentInterval] 17 | ), 18 | }); 19 | }) 20 | ); 21 | 22 | let originalPulledData; 23 | describe('Testing singleRPSfinished functionality', () => { 24 | beforeEach(() => { 25 | initializeVariables(); 26 | originalPulledData = globalVariables.pulledDataFromTest; 27 | globalVariables.pulledDataFromTest = {}; 28 | singleRPSfinished(globalTestConfig.rpsInterval, io, globalTestConfig, globalVariables); 29 | }); 30 | 31 | it('should call emit singleRPS finished with rpsGroup', () => { 32 | expect(io.emit).toBeCalledWith( 33 | ioSocketCommands.singleRPSfinished, 34 | globalTestConfig.rpsInterval 35 | ); 36 | }); 37 | 38 | it('should call fetch with correct header jagtester command and return correct response data', async () => { 39 | expect(fetch).toBeCalledWith(globalTestConfig.inputsData[0].targetURL, { 40 | headers: { 41 | jagtestercommand: Jagtestercommands.endTest.toString(), 42 | }, 43 | }); 44 | expect(globalVariables.pulledDataFromTest[globalVariables.currentInterval]).toEqual( 45 | originalPulledData[globalVariables.currentInterval] 46 | ); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | # Jagtester 2 | 3 | [![npm version](https://badge.fury.io/js/jagtester.svg)](https://badge.fury.io/js/jagtester) 4 | 5 | There is a big community of developers using Express JS with Node.js to power their web applications. Our team of developers love the simplicity of Express and have used it numerous times in our applications. But, as developers who like to optimize the performance of our application, we wanted to test the performance of the server under heavy load, so we started looking around for solutions. Sure, there are a lot of products that can help developers test the performance of the server, but we wanted to go much deeper. What if we could show how each middleware within the server performs before sending out the response? What if we wanted to compare how the performance scales based on the requests per second? We couldn’t find the product we needed, so we did what every great developer team does, we created our own solution. 6 | Enter [Jagtester](https://jagtester.com/). 7 | 8 | ## Installing 9 | 10 | For npm: 11 | 12 | ```bash 13 | npm install jagtester 14 | ``` 15 | 16 | or yarn: 17 | 18 | ```bash 19 | yarn add jagtester 20 | ``` 21 | 22 | ## Usage 23 | 24 | It is very easy to use jagtester, just install it and include it as a middleware in your Express server, and you are ready to go. 25 | 26 | ```JavaScript 27 | const jagtester = require('jagtester'); 28 | app.use(jagtester(app)); 29 | ``` 30 | 31 | **NOTE: you need to pass your Express app to jagtester middleware, so it can enable middleware level reporting** 32 | 33 | **NOTE: Make sure to put the Jagtester above all other global middlewares (for ex. express.static()), because the test will not be able to start if other middlewares are sending a response before the request can hit the jagtester middleware.** 34 | 35 | Now just run the Jagtester with 36 | 37 | ```bash 38 | npx jagtester 39 | ``` 40 | 41 | and it will start up jagtester on a local server port 15000 (or next available port). 42 | 43 | With our intuitive web interface, you will ba able to configure the test and run it. Make sure to also start your own server you want to use Jagtester on. 44 | 45 | ## Features 46 | 47 | - Allows users to fully customize testing based on the needs of their application. Here are the options you can configure: 48 | - Requests per second (RPS) interval. 49 | - Starting and ending RPS. 50 | - Time to stay at each RPS range. 51 | - Ability to simulate traffic to one or more targets, with a specified percentage of the load going to each target. 52 | - Stop feature, which will allow the user to stop the current test if it is taking too long and get the results that have been generated so far. 53 | - Displays graphs to show a breakdown of all routes along with the error percentage. 54 | - Graphs to display details for each route, broken down to the middleware level to analyze the performance of each of the routes. 55 | - Export functionality to generate result reports, either from all tests or only single test results, with an ability to delete the collected results. 56 | - And for the lover of dark mode, we got you covered with an option to switch between light and dark modes. 57 | 58 | ## We are open to any issues! 59 | -------------------------------------------------------------------------------- /dist/client/c82b0acf13f3f94459071d557ecee477.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/dist/client/c82b0acf13f3f94459071d557ecee477.png -------------------------------------------------------------------------------- /dist/client/index.html: -------------------------------------------------------------------------------- 1 | Jagtester
-------------------------------------------------------------------------------- /dist/client/main.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2018 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /*! 14 | * Chart.js v3.3.2 15 | * https://www.chartjs.org 16 | * (c) 2021 Chart.js Contributors 17 | * Released under the MIT License 18 | */ 19 | 20 | /** 21 | * A better abstraction over CSS. 22 | * 23 | * @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present 24 | * @website https://github.com/cssinjs/jss 25 | * @license MIT 26 | */ 27 | 28 | /** @license React v0.20.2 29 | * scheduler.production.min.js 30 | * 31 | * Copyright (c) Facebook, Inc. and its affiliates. 32 | * 33 | * This source code is licensed under the MIT license found in the 34 | * LICENSE file in the root directory of this source tree. 35 | */ 36 | 37 | /** @license React v16.13.1 38 | * react-is.production.min.js 39 | * 40 | * Copyright (c) Facebook, Inc. and its affiliates. 41 | * 42 | * This source code is licensed under the MIT license found in the 43 | * LICENSE file in the root directory of this source tree. 44 | */ 45 | 46 | /** @license React v17.0.2 47 | * react-dom.production.min.js 48 | * 49 | * Copyright (c) Facebook, Inc. and its affiliates. 50 | * 51 | * This source code is licensed under the MIT license found in the 52 | * LICENSE file in the root directory of this source tree. 53 | */ 54 | 55 | /** @license React v17.0.2 56 | * react.production.min.js 57 | * 58 | * Copyright (c) Facebook, Inc. and its affiliates. 59 | * 60 | * This source code is licensed under the MIT license found in the 61 | * LICENSE file in the root directory of this source tree. 62 | */ 63 | -------------------------------------------------------------------------------- /dist/middleware/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, Application } from 'express'; 2 | declare type FunctionType = (app: Application) => (req: Request, res: Response, next: NextFunction) => unknown; 3 | declare const getMiddleware: FunctionType; 4 | export default getMiddleware; 5 | -------------------------------------------------------------------------------- /dist/middleware/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var response_time_1 = __importDefault(require("response-time")); 7 | var Jagtestercommands; 8 | (function (Jagtestercommands) { 9 | Jagtestercommands[Jagtestercommands["updateLayer"] = 0] = "updateLayer"; 10 | Jagtestercommands[Jagtestercommands["running"] = 1] = "running"; 11 | Jagtestercommands[Jagtestercommands["endTest"] = 2] = "endTest"; 12 | })(Jagtestercommands || (Jagtestercommands = {})); 13 | var getMiddleware = function (app) { 14 | var collectedData = {}; 15 | var routeData = {}; 16 | var isPrototypeChanged = false; 17 | var resetLayerPrototype = function () { 18 | app._router.stack[0].__proto__.handle_request = originalLayerHandleRequest; 19 | isPrototypeChanged = false; 20 | collectedData = {}; 21 | routeData = {}; 22 | }; 23 | var updateLayerPrototype = function () { 24 | app._router.stack[0].__proto__.handle_request = newLayerHandleRequest; 25 | isPrototypeChanged = true; 26 | collectedData = {}; 27 | routeData = {}; 28 | }; 29 | var originalLayerHandleRequest = function handle(req, res, next) { 30 | var fn = this.handle; 31 | if (fn.length > 3) { 32 | // not a standard request handler 33 | return next(); 34 | } 35 | try { 36 | fn(req, res, next); 37 | } 38 | catch (err) { 39 | next(err); 40 | } 41 | }; 42 | var newLayerHandleRequest = function handle(req, res, next) { 43 | var fn = this.handle; 44 | if (fn.length > 3) { 45 | // not a standard request handler 46 | return next(); 47 | } 48 | try { 49 | var fnName = this.name, reqId_1 = req.headers.jagtesterreqid 50 | ? req.headers.jagtesterreqid.toString() 51 | : undefined, reqRoute_1 = req.originalUrl; 52 | // create a data object in the collected data if it doesnt already exist 53 | if (!routeData[reqRoute_1]) { 54 | var newCollectedData = {}; 55 | newCollectedData[reqId_1] = { 56 | reqId: reqId_1, 57 | reqRoute: reqRoute_1, 58 | middlewares: [], 59 | }; 60 | routeData[reqRoute_1] = newCollectedData; 61 | } 62 | else { 63 | // create a data object in the collected data if it doesnt already exist 64 | if (reqId_1 && !routeData[reqRoute_1][reqId_1]) { 65 | routeData[reqRoute_1][reqId_1] = { 66 | reqId: reqId_1, 67 | reqRoute: reqRoute_1, 68 | middlewares: [], 69 | }; 70 | } 71 | } 72 | // create a data object in the collected data if it doesnt already exist 73 | if (reqId_1 && !collectedData[reqId_1]) { 74 | collectedData[reqId_1] = { 75 | reqId: reqId_1, 76 | reqRoute: reqRoute_1, 77 | middlewares: [], 78 | }; 79 | } 80 | if (reqId_1) { 81 | // add layer information to the collectedData 82 | routeData[reqRoute_1][reqId_1].middlewares.push({ 83 | fnName: fnName, 84 | elapsedTime: 0, 85 | }); 86 | collectedData[reqId_1].middlewares.push({ 87 | fnName: fnName, 88 | elapsedTime: 0, 89 | }); 90 | } 91 | // call the middleware and time it in the next function 92 | var beforeFunctionCall_1 = Date.now(); 93 | fn(req, res, function () { 94 | if (reqId_1 && routeData[reqRoute_1] && routeData[reqRoute_1][reqId_1]) { 95 | var lastElIndex = routeData[reqRoute_1][reqId_1].middlewares.length - 1; 96 | routeData[reqRoute_1][reqId_1].middlewares[lastElIndex].elapsedTime = 97 | Date.now() - beforeFunctionCall_1; 98 | } 99 | next(); 100 | }); 101 | } 102 | catch (err) { 103 | next(err); 104 | } 105 | }; 106 | // this is the actual middleware that will take jagtestercommands 107 | return function (req, res, next) { 108 | // getting the command 109 | var jagtestercommand = +req.headers.jagtestercommand; 110 | switch (jagtestercommand) { 111 | //changing the prototype of the layer handle request while running 112 | case Jagtestercommands.running: 113 | if (!isPrototypeChanged) { 114 | updateLayerPrototype(); 115 | } 116 | // res.header({ jagtesterRoute: req.url }); 117 | break; 118 | //changing the prototype of the layer handle request 119 | case Jagtestercommands.updateLayer: 120 | updateLayerPrototype(); 121 | // res.header({ jagtesterRoute: req.url }); 122 | return res.json({ jagtester: true }); 123 | //reset the prototype and send back json data 124 | case Jagtestercommands.endTest: 125 | res.json(routeData); 126 | resetLayerPrototype(); 127 | return; 128 | default: 129 | // changing layer prototype back to original 130 | if (isPrototypeChanged) { 131 | resetLayerPrototype(); 132 | } 133 | break; 134 | } 135 | return response_time_1.default({ suffix: false })(req, res, next); 136 | }; 137 | }; 138 | exports.default = getMiddleware; 139 | module.exports = getMiddleware; 140 | -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jagtester", 3 | "version": "1.0.4", 4 | "description": "Load tester for Express servers with detailed middleware metrics.", 5 | "main": "middleware/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node ./server/index.js" 9 | }, 10 | "bin": { 11 | "jagtester": "./server/index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/oslabs-beta/jagtester.git" 16 | }, 17 | "keywords": [ 18 | "express", 19 | "load", 20 | "load tester", 21 | "jag tester", 22 | "stress tester", 23 | "express fast", 24 | "express test" 25 | ], 26 | "author": "Grigor Minasyan, Jason de Vera, Abigail Dorso", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/oslabs-beta/jagtester/issues" 30 | }, 31 | "homepage": "https://github.com/oslabs-beta/jagtester#readme", 32 | "dependencies": { 33 | "abort-controller": "^3.0.0", 34 | "express": "4.16.0", 35 | "node-fetch": "^2.6.1", 36 | "open": "^8.2.0", 37 | "path": "^0.12.7", 38 | "response-time": "^2.3.2", 39 | "socket.io": "^4.1.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /dist/server/app.d.ts: -------------------------------------------------------------------------------- 1 | declare const app: import("express-serve-static-core").Express; 2 | export { app }; 3 | -------------------------------------------------------------------------------- /dist/server/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.app = void 0; 7 | const express_1 = __importDefault(require("express")); 8 | const path_1 = __importDefault(require("path")); 9 | const testrouter_1 = __importDefault(require("./testrouter")); 10 | const app = express_1.default(); 11 | exports.app = app; 12 | app.use(express_1.default.json()); 13 | app.use(express_1.default.urlencoded({ extended: false })); 14 | app.use('/', express_1.default.static(path_1.default.join(__dirname, '../client'))); 15 | app.use('/api', testrouter_1.default); 16 | app.get(['/', '/results'], (req, res) => { 17 | res.sendFile(path_1.default.join(__dirname, '../client/index.html')); 18 | }); 19 | app.use('/*', (req, res) => { 20 | res.redirect('/'); 21 | }); 22 | -------------------------------------------------------------------------------- /dist/server/helperFunctions.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { CollectedData, CollectedDataSingle, PulledDataFromTest, TimeArrRoutes, TrackedVariables, GlobalVariables } from './interfaces'; 3 | import AbortController from 'abort-controller'; 4 | import http from 'http'; 5 | import { Server } from 'socket.io'; 6 | declare const processData: (data: CollectedData) => CollectedDataSingle; 7 | declare const processLastMiddleware: (pulledDataFromTest: PulledDataFromTest, rps: string, route: string) => void; 8 | declare const emitPercentage: (successfulResCount: number, errorCount: number, rpsGroup: number, secondsToTest: number, io: Server) => void; 9 | declare const sendRequests: (targetURL: string, rpsGroup: number, rpsActual: number, secondsToTest: number, agent: http.Agent, abortController: AbortController, timeArrRoutes: TimeArrRoutes, trackedVariables: TrackedVariables, globalVariables: GlobalVariables, io: Server, timeOutArray: NodeJS.Timeout[], singleRPSfinished: (rpsGroup: number) => void, allRPSfinished: () => void, emitPercentage: (successfulResCount: number, errorCount: number, rpsGroup: number, secondsToTest: number, io: Server) => void) => void; 10 | export { processData, processLastMiddleware, emitPercentage, sendRequests }; 11 | -------------------------------------------------------------------------------- /dist/server/helperFunctions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.sendRequests = exports.emitPercentage = exports.processLastMiddleware = exports.processData = void 0; 7 | const interfaces_1 = require("./interfaces"); 8 | const node_fetch_1 = __importDefault(require("node-fetch")); 9 | const processData = (data) => { 10 | const collectedDataArr = []; 11 | for (const key in data) { 12 | collectedDataArr.push(data[key]); 13 | } 14 | // add middlewares elapsed times 15 | const collectedDataSingle = collectedDataArr.reduce((acc, cur) => { 16 | for (let i = 0; i < acc.middlewares.length; i++) { 17 | if (i < cur.middlewares.length) { 18 | acc.middlewares[i].elapsedTime += cur.middlewares[i].elapsedTime; 19 | } 20 | } 21 | return acc; 22 | }); 23 | // divide by the count of requests 24 | collectedDataSingle.middlewares.forEach((middleware) => { 25 | middleware.elapsedTime = 26 | Math.round((100 * middleware.elapsedTime) / collectedDataArr.length) / 100; 27 | }); 28 | return collectedDataSingle; 29 | }; 30 | exports.processData = processData; 31 | const processLastMiddleware = (pulledDataFromTest, rps, route) => { 32 | const indexOfLast = pulledDataFromTest[rps][route].middlewares.length - 1; 33 | const tempMiddleware = { 34 | fnName: 'temp', 35 | elapsedTime: 0, 36 | }; 37 | pulledDataFromTest[rps][route].middlewares[indexOfLast].elapsedTime = 38 | Math.round(100 * 39 | (pulledDataFromTest[rps][route].receivedTime - 40 | pulledDataFromTest[rps][route].middlewares.reduce((acc, cur) => { 41 | acc.elapsedTime += cur.elapsedTime; 42 | return acc; 43 | }, tempMiddleware).elapsedTime)) / 100; 44 | }; 45 | exports.processLastMiddleware = processLastMiddleware; 46 | const emitPercentage = (successfulResCount, errorCount, rpsGroup, secondsToTest, io) => { 47 | const percent = (successfulResCount + errorCount) / (rpsGroup * secondsToTest); 48 | if (Math.floor(10000 * percent) % 1000 === 0) { 49 | io.emit(interfaces_1.ioSocketCommands.currentRPSProgress, percent); 50 | } 51 | }; 52 | exports.emitPercentage = emitPercentage; 53 | const sendRequests = (targetURL, rpsGroup, rpsActual, secondsToTest, agent, abortController, timeArrRoutes, trackedVariables, globalVariables, io, timeOutArray, singleRPSfinished, allRPSfinished, emitPercentage) => { 54 | const sendFetch = (reqId) => { 55 | node_fetch_1.default(targetURL, { 56 | agent, 57 | signal: abortController.signal, 58 | headers: { 59 | jagtestercommand: interfaces_1.Jagtestercommands.running.toString(), 60 | jagtesterreqid: reqId.toString(), 61 | }, 62 | }) 63 | .then((res) => { 64 | const resRoute = new URL(targetURL).pathname; 65 | timeArrRoutes[resRoute][rpsGroup].successfulResCount++; 66 | globalVariables.successfulResCount++; 67 | emitPercentage(globalVariables.successfulResCount, globalVariables.errorCount, rpsGroup, secondsToTest, io); 68 | if (globalVariables.successfulResCount + globalVariables.errorCount >= 69 | rpsGroup * secondsToTest) { 70 | // eventEmitter.emit(ioSocketCommands.singleRPSfinished, rpsGroup); 71 | singleRPSfinished(rpsGroup); 72 | } 73 | if (res.headers.has('x-response-time')) { 74 | const xResponseTime = res.headers.get('x-response-time'); 75 | timeArrRoutes[resRoute][rpsGroup].receivedTotalTime += xResponseTime 76 | ? +xResponseTime 77 | : 0; 78 | } 79 | }) 80 | .catch((error) => { 81 | if (error.name === 'AbortError') { 82 | if (trackedVariables.isTestRunning) { 83 | trackedVariables.isTestRunning = false; 84 | // eventEmitter.emit(ioSocketCommands.allRPSfinished); 85 | allRPSfinished(); 86 | } 87 | } 88 | else { 89 | const resRoute = new URL(targetURL).pathname; 90 | timeArrRoutes[resRoute][rpsGroup].errorCount++; 91 | globalVariables.errorCount++; 92 | emitPercentage(globalVariables.successfulResCount, globalVariables.errorCount, rpsGroup, secondsToTest, io); 93 | if (globalVariables.successfulResCount + globalVariables.errorCount >= 94 | rpsGroup * secondsToTest) { 95 | // eventEmitter.emit(ioSocketCommands.singleRPSfinished, rpsGroup); 96 | singleRPSfinished(rpsGroup); 97 | } 98 | } 99 | }); 100 | }; 101 | // outer for loop to run for every second and set timeouts for after that second 102 | for (let j = 0; j < secondsToTest; j++) { 103 | for (let i = 0; i < rpsActual; i++) { 104 | const timeout = setTimeout(sendFetch.bind(this, i + j * rpsActual), Math.floor(Math.random() * 1000 + 1000 * j)); 105 | timeOutArray.push(timeout); 106 | } 107 | } 108 | return; 109 | }; 110 | exports.sendRequests = sendRequests; 111 | -------------------------------------------------------------------------------- /dist/server/helpers/allRPSfinished.d.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | import { TestConfigData, GlobalVariables } from '../interfaces'; 3 | declare type AllRPSfinished = (globalTestConfig: TestConfigData, io: Server, globalVariables: GlobalVariables) => void; 4 | declare const allRPSfinished: AllRPSfinished; 5 | export default allRPSfinished; 6 | export type { AllRPSfinished }; 7 | -------------------------------------------------------------------------------- /dist/server/helpers/allRPSfinished.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const node_fetch_1 = __importDefault(require("node-fetch")); 7 | const abort_controller_1 = __importDefault(require("abort-controller")); 8 | const interfaces_1 = require("../interfaces"); 9 | const processData_1 = __importDefault(require("./processData")); 10 | const processLastMiddleware_1 = __importDefault(require("./processLastMiddleware")); 11 | const allRPSfinished = (globalTestConfig, io, globalVariables) => { 12 | node_fetch_1.default(globalTestConfig.inputsData[0].targetURL, { 13 | headers: { 14 | jagtestercommand: interfaces_1.Jagtestercommands.endTest.toString(), 15 | }, 16 | }).catch((err) => { 17 | io.emit(interfaces_1.ioSocketCommands.errorInfo, err.toString()); 18 | }); 19 | globalVariables.abortController = new abort_controller_1.default(); 20 | globalVariables.isTestRunning = false; 21 | // clear timeouts 22 | for (const timeout of globalVariables.timeOutArray) { 23 | clearTimeout(timeout); 24 | } 25 | globalVariables.timeOutArray.splice(0, globalVariables.timeOutArray.length); 26 | // getting the average response time, since we had the total response times added together 27 | for (const route in globalVariables.timeArrRoutes) { 28 | for (const rpsGroup in globalVariables.timeArrRoutes[route]) { 29 | globalVariables.timeArrRoutes[route][rpsGroup].receivedTotalTime = 30 | Math.round((1000 * globalVariables.timeArrRoutes[route][rpsGroup].receivedTotalTime) / 31 | globalVariables.timeArrRoutes[route][rpsGroup].successfulResCount) / 1000; 32 | } 33 | } 34 | // processing middlewares, averaging them, then combining timearrroutes 35 | for (const rps in globalVariables.pulledDataFromTest) { 36 | for (const route in globalVariables.pulledDataFromTest[rps]) { 37 | globalVariables.pulledDataFromTest[rps][route] = processData_1.default(globalVariables.pulledDataFromTest[rps][route]); 38 | globalVariables.pulledDataFromTest[rps][route].receivedTime = 39 | globalVariables.timeArrRoutes[route][rps].receivedTotalTime; 40 | globalVariables.pulledDataFromTest[rps][route].errorCount = 41 | globalVariables.timeArrRoutes[route][rps].errorCount; 42 | globalVariables.pulledDataFromTest[rps][route].successfulResCount = 43 | globalVariables.timeArrRoutes[route][rps].successfulResCount; 44 | //fixing the elapsed time for the last middleware 45 | processLastMiddleware_1.default(globalVariables.pulledDataFromTest, rps, route); 46 | } 47 | } 48 | if (Object.keys(globalVariables.pulledDataFromTest).length > 0) { 49 | const newPulledData = { 50 | testTime: Date.now(), 51 | testData: globalVariables.pulledDataFromTest, 52 | }; 53 | io.emit(interfaces_1.ioSocketCommands.allRPSfinished, [newPulledData]); 54 | } 55 | }; 56 | exports.default = allRPSfinished; 57 | -------------------------------------------------------------------------------- /dist/server/helpers/emitPercentage.d.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | import { GlobalVariables } from '../interfaces'; 3 | declare type EmitPercentage = (globalVariables: GlobalVariables, rpsGroup: number, secondsToTest: number, io: Server) => void; 4 | declare const emitPercentage: EmitPercentage; 5 | export default emitPercentage; 6 | export type { EmitPercentage }; 7 | -------------------------------------------------------------------------------- /dist/server/helpers/emitPercentage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const interfaces_1 = require("../interfaces"); 4 | const emitPercentage = (globalVariables, rpsGroup, secondsToTest, io) => { 5 | const percent = (globalVariables.successfulResCount + globalVariables.errorCount) / 6 | (rpsGroup * secondsToTest); 7 | if (Math.floor(10000 * percent) % 1000 === 0) { 8 | io.emit(interfaces_1.ioSocketCommands.currentRPSProgress, percent); 9 | } 10 | }; 11 | exports.default = emitPercentage; 12 | -------------------------------------------------------------------------------- /dist/server/helpers/processData.d.ts: -------------------------------------------------------------------------------- 1 | import { CollectedData, CollectedDataSingle } from '../interfaces'; 2 | declare type ProcessData = (data: CollectedData) => CollectedDataSingle; 3 | declare const processData: ProcessData; 4 | export default processData; 5 | export type { ProcessData }; 6 | -------------------------------------------------------------------------------- /dist/server/helpers/processData.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const processData = (data) => { 4 | const collectedDataArr = []; 5 | for (const key in data) { 6 | collectedDataArr.push(data[key]); 7 | } 8 | // add middlewares elapsed times 9 | const collectedDataSingle = collectedDataArr.reduce((acc, cur) => { 10 | for (let i = 0; i < acc.middlewares.length && i < cur.middlewares.length; i++) { 11 | acc.middlewares[i].elapsedTime += cur.middlewares[i].elapsedTime; 12 | } 13 | return acc; 14 | }); 15 | // divide by the count of requests 16 | collectedDataSingle.middlewares.forEach((middleware) => { 17 | middleware.elapsedTime = 18 | Math.round((100 * middleware.elapsedTime) / collectedDataArr.length) / 100; 19 | }); 20 | return collectedDataSingle; 21 | }; 22 | exports.default = processData; 23 | -------------------------------------------------------------------------------- /dist/server/helpers/processLastMiddleware.d.ts: -------------------------------------------------------------------------------- 1 | import { PulledDataFromTest } from '../interfaces'; 2 | declare type ProcessLastMiddleware = (pulledDataFromTest: PulledDataFromTest, rps: string, route: string) => void; 3 | declare const processLastMiddleware: ProcessLastMiddleware; 4 | export default processLastMiddleware; 5 | export type { ProcessLastMiddleware }; 6 | -------------------------------------------------------------------------------- /dist/server/helpers/processLastMiddleware.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const processLastMiddleware = (pulledDataFromTest, rps, route) => { 4 | const indexOfLast = pulledDataFromTest[rps][route].middlewares.length - 1; 5 | const tempMiddleware = { 6 | fnName: 'temp', 7 | elapsedTime: 0, 8 | }; 9 | pulledDataFromTest[rps][route].middlewares[indexOfLast].elapsedTime = 10 | Math.round(100 * 11 | (pulledDataFromTest[rps][route].receivedTime - 12 | pulledDataFromTest[rps][route].middlewares.reduce((acc, cur) => { 13 | acc.elapsedTime += cur.elapsedTime; 14 | return acc; 15 | }, tempMiddleware).elapsedTime)) / 100; 16 | }; 17 | exports.default = processLastMiddleware; 18 | -------------------------------------------------------------------------------- /dist/server/helpers/sendRequests.d.ts: -------------------------------------------------------------------------------- 1 | import { GlobalVariables, TestConfigData } from '../interfaces'; 2 | import { Server } from 'socket.io'; 3 | declare type SendRequests = (targetURL: string, rpsGroup: number, rpsActual: number, secondsToTest: number, globalVariables: GlobalVariables, io: Server, globalTestConfig: TestConfigData) => void; 4 | declare const sendRequests: SendRequests; 5 | export default sendRequests; 6 | export type { SendRequests }; 7 | -------------------------------------------------------------------------------- /dist/server/helpers/sendRequests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const interfaces_1 = require("../interfaces"); 7 | const node_fetch_1 = __importDefault(require("node-fetch")); 8 | const singleRPSfinished_1 = __importDefault(require("./singleRPSfinished")); 9 | const allRPSfinished_1 = __importDefault(require("./allRPSfinished")); 10 | const emitPercentage_1 = __importDefault(require("./emitPercentage")); 11 | const sendRequests = (targetURL, rpsGroup, rpsActual, secondsToTest, globalVariables, io, globalTestConfig) => { 12 | const sendFetch = (reqId) => { 13 | node_fetch_1.default(targetURL, { 14 | agent: globalVariables.agent, 15 | signal: globalVariables.abortController.signal, 16 | headers: { 17 | jagtestercommand: interfaces_1.Jagtestercommands.running.toString(), 18 | jagtesterreqid: reqId.toString(), 19 | }, 20 | }) 21 | .then((res) => { 22 | const resRoute = new URL(targetURL).pathname; 23 | globalVariables.timeArrRoutes[resRoute][rpsGroup].successfulResCount++; 24 | globalVariables.successfulResCount++; 25 | emitPercentage_1.default(globalVariables, rpsGroup, secondsToTest, io); 26 | if (globalVariables.successfulResCount + globalVariables.errorCount >= 27 | rpsGroup * secondsToTest) { 28 | singleRPSfinished_1.default(rpsGroup, io, globalTestConfig, globalVariables); 29 | } 30 | if (res.headers.has('x-response-time')) { 31 | const xResponseTime = res.headers.get('x-response-time'); 32 | globalVariables.timeArrRoutes[resRoute][rpsGroup].receivedTotalTime += 33 | xResponseTime ? +xResponseTime : 0; 34 | } 35 | }) 36 | .catch((error) => { 37 | if (error.name === 'AbortError') { 38 | if (globalVariables.isTestRunning) { 39 | globalVariables.isTestRunning = false; 40 | // eventEmitter.emit(ioSocketCommands.allRPSfinished); 41 | allRPSfinished_1.default(globalTestConfig, io, globalVariables); 42 | } 43 | } 44 | else { 45 | const resRoute = new URL(targetURL).pathname; 46 | globalVariables.timeArrRoutes[resRoute][rpsGroup].errorCount++; 47 | globalVariables.errorCount++; 48 | emitPercentage_1.default(globalVariables, rpsGroup, secondsToTest, io); 49 | if (globalVariables.successfulResCount + globalVariables.errorCount >= 50 | rpsGroup * secondsToTest) { 51 | singleRPSfinished_1.default(rpsGroup, io, globalTestConfig, globalVariables); 52 | } 53 | } 54 | }); 55 | }; 56 | // outer for loop to run for every second and set timeouts for after that second 57 | for (let j = 0; j < secondsToTest; j++) { 58 | for (let i = 0; i < rpsActual; i++) { 59 | const timeout = setTimeout(sendFetch.bind(this, i + j * rpsActual), Math.floor(Math.random() * 1000 + 1000 * j)); 60 | globalVariables.timeOutArray.push(timeout); 61 | } 62 | } 63 | return; 64 | }; 65 | exports.default = sendRequests; 66 | -------------------------------------------------------------------------------- /dist/server/helpers/sendRequestsAtRPS.d.ts: -------------------------------------------------------------------------------- 1 | import { GlobalVariables, TestConfigData } from '../interfaces'; 2 | import { Server } from 'socket.io'; 3 | declare type SendRequestsAtRPS = (globalVariables: GlobalVariables, globalTestConfig: TestConfigData, io: Server) => void; 4 | declare const sendRequestsAtRPS: SendRequestsAtRPS; 5 | export default sendRequestsAtRPS; 6 | export type { SendRequestsAtRPS }; 7 | -------------------------------------------------------------------------------- /dist/server/helpers/sendRequestsAtRPS.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const interfaces_1 = require("../interfaces"); 7 | const node_fetch_1 = __importDefault(require("node-fetch")); 8 | const allRPSfinished_1 = __importDefault(require("./allRPSfinished")); 9 | const sendRequests_1 = __importDefault(require("./sendRequests")); 10 | const sendRequestsAtRPS = (globalVariables, globalTestConfig, io) => { 11 | // check if finished testing 12 | const curRPS = globalTestConfig.startRPS + globalVariables.currentInterval * globalTestConfig.rpsInterval; 13 | if (curRPS > globalTestConfig.endRPS) { 14 | allRPSfinished_1.default(globalTestConfig, io, globalVariables); 15 | return; 16 | } 17 | // update layer first then start testing 18 | for (const target of globalTestConfig.inputsData) { 19 | node_fetch_1.default(target.targetURL, { 20 | agent: globalVariables.agent, 21 | headers: { 22 | jagtestercommand: interfaces_1.Jagtestercommands.updateLayer.toString(), 23 | }, 24 | }) 25 | .then(() => { 26 | // saving the resroute into the collection object 27 | const resRoute = new URL(target.targetURL).pathname; 28 | if (globalVariables.timeArrRoutes[resRoute] === undefined) { 29 | globalVariables.timeArrRoutes[resRoute] = {}; 30 | } 31 | if (globalVariables.timeArrRoutes[resRoute][curRPS.toString()] === undefined) { 32 | globalVariables.timeArrRoutes[resRoute][curRPS.toString()] = { 33 | receivedTotalTime: 0, 34 | errorCount: 0, 35 | successfulResCount: 0, 36 | }; 37 | } 38 | globalVariables.errorCount = 0; 39 | globalVariables.successfulResCount = 0; 40 | sendRequests_1.default(target.targetURL, curRPS, Math.round((curRPS * target.percentage) / 100), globalTestConfig.testLength, globalVariables, io, globalTestConfig); 41 | }) 42 | .catch(() => { 43 | allRPSfinished_1.default(globalTestConfig, io, globalVariables); 44 | }); 45 | } 46 | }; 47 | exports.default = sendRequestsAtRPS; 48 | -------------------------------------------------------------------------------- /dist/server/helpers/singleRPSfinished.d.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | import { GlobalVariables, TestConfigData } from '../interfaces'; 3 | declare type SingleRPSfinished = (rpsGroup: number, io: Server, globalTestConfig: TestConfigData, globalVariables: GlobalVariables) => void; 4 | declare const singleRPSfinished: SingleRPSfinished; 5 | export default singleRPSfinished; 6 | export type { SingleRPSfinished }; 7 | -------------------------------------------------------------------------------- /dist/server/helpers/singleRPSfinished.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const node_fetch_1 = __importDefault(require("node-fetch")); 7 | const interfaces_1 = require("../interfaces"); 8 | const allRPSfinished_1 = __importDefault(require("./allRPSfinished")); 9 | const sendRequestsAtRPS_1 = __importDefault(require("./sendRequestsAtRPS")); 10 | const singleRPSfinished = (rpsGroup, io, globalTestConfig, globalVariables) => { 11 | io.emit(interfaces_1.ioSocketCommands.singleRPSfinished, rpsGroup); 12 | node_fetch_1.default(globalTestConfig.inputsData[0].targetURL, { 13 | headers: { 14 | jagtestercommand: interfaces_1.Jagtestercommands.endTest.toString(), 15 | }, 16 | }) 17 | .then((fetchRes) => fetchRes.json()) 18 | .then((data) => { 19 | const curRPS = globalTestConfig.startRPS + 20 | globalVariables.currentInterval * globalTestConfig.rpsInterval; 21 | globalVariables.pulledDataFromTest[curRPS.toString()] = data; 22 | globalVariables.currentInterval++; 23 | sendRequestsAtRPS_1.default(globalVariables, globalTestConfig, io); 24 | }) 25 | .catch(() => { 26 | allRPSfinished_1.default(globalTestConfig, io, globalVariables); 27 | }); 28 | }; 29 | exports.default = singleRPSfinished; 30 | -------------------------------------------------------------------------------- /dist/server/index.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /// 3 | declare const server: import("http").Server; 4 | export default server; 5 | -------------------------------------------------------------------------------- /dist/server/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | var __importDefault = (this && this.__importDefault) || function (mod) { 4 | return (mod && mod.__esModule) ? mod : { "default": mod }; 5 | }; 6 | Object.defineProperty(exports, "__esModule", { value: true }); 7 | // TODO use cluster to imrpove our server performance 8 | const server_1 = require("./server"); 9 | const open_1 = __importDefault(require("open")); 10 | let port = 15000; 11 | server_1.http.on('error', function (e) { 12 | if (e.code === 'EADDRINUSE') { 13 | port++; 14 | server_1.http.listen(port); 15 | } 16 | }); 17 | const server = server_1.http.on('listening', function () { 18 | console.log(`Jagtester running on http://localhost:${port}`); 19 | open_1.default(`http://localhost:${port}`).catch((err) => console.log(err)); 20 | }); 21 | server_1.http.listen(port); 22 | exports.default = server; 23 | -------------------------------------------------------------------------------- /dist/server/interfaces.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import http from 'http'; 3 | export interface TimeArrInterface { 4 | receivedTotalTime: number; 5 | recordedTotalTime: number; 6 | } 7 | export interface middlewareSingle { 8 | fnName: string; 9 | elapsedTime: number; 10 | } 11 | export interface CollectedData { 12 | [key: string]: { 13 | reqId: string; 14 | reqRoute: string; 15 | middlewares: middlewareSingle[]; 16 | }; 17 | } 18 | export interface CollectedDataSingle { 19 | receivedTime?: number; 20 | recordedTime?: number; 21 | errorCount?: number; 22 | requestCount?: number; 23 | successfulResCount?: number; 24 | RPS?: number; 25 | reqId?: string; 26 | reqRoute: string; 27 | middlewares: middlewareSingle[]; 28 | } 29 | export declare enum Jagtestercommands { 30 | updateLayer = 0, 31 | running = 1, 32 | endTest = 2 33 | } 34 | export declare enum ioSocketCommands { 35 | testRunningStateChange = "testRunningStateChange", 36 | singleRPSfinished = "singleRPSfinished", 37 | allRPSfinished = "allRPSfinished", 38 | errorInfo = "errorInfo", 39 | currentRPSProgress = "currentRPSProgress" 40 | } 41 | export interface TestConfigData { 42 | rpsInterval: number; 43 | startRPS: number; 44 | endRPS: number; 45 | testLength: number; 46 | inputsData: { 47 | method: string; 48 | targetURL: string; 49 | percentage: number; 50 | jagTesterEnabled: boolean; 51 | }[]; 52 | } 53 | export interface PulledDataFromTest { 54 | [key: string]: { 55 | [key: string]: CollectedDataSingle | CollectedData; 56 | }; 57 | } 58 | export declare enum HTTPMethods { 59 | GET = "GET", 60 | POST = "POST", 61 | PUT = "PUT", 62 | DELETE = "DELETE", 63 | PATCH = "PATCH", 64 | HEAD = "HEAD", 65 | CONNECT = "CONNECT", 66 | TRACE = "TRACE" 67 | } 68 | export interface TimeArrRoutes { 69 | [key: string]: { 70 | [key: string]: { 71 | receivedTotalTime: number; 72 | errorCount: number; 73 | successfulResCount: number; 74 | }; 75 | }; 76 | } 77 | export interface TrackedVariables { 78 | isTestRunningInternal: boolean; 79 | isTestRunningListener: (val: boolean) => void; 80 | isTestRunning: boolean; 81 | } 82 | export interface GlobalVariables { 83 | currentInterval: number; 84 | errorCount: number; 85 | successfulResCount: number; 86 | abortController: AbortController; 87 | timeArrRoutes: TimeArrRoutes; 88 | timeOutArray: NodeJS.Timeout[]; 89 | pulledDataFromTest: PulledDataFromTest; 90 | isTestRunningInternal: boolean; 91 | isTestRunningListener: (val: boolean) => void; 92 | isTestRunning: boolean; 93 | agent: http.Agent; 94 | } 95 | -------------------------------------------------------------------------------- /dist/server/interfaces.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.HTTPMethods = exports.ioSocketCommands = exports.Jagtestercommands = void 0; 4 | var Jagtestercommands; 5 | (function (Jagtestercommands) { 6 | Jagtestercommands[Jagtestercommands["updateLayer"] = 0] = "updateLayer"; 7 | Jagtestercommands[Jagtestercommands["running"] = 1] = "running"; 8 | Jagtestercommands[Jagtestercommands["endTest"] = 2] = "endTest"; 9 | })(Jagtestercommands = exports.Jagtestercommands || (exports.Jagtestercommands = {})); 10 | var ioSocketCommands; 11 | (function (ioSocketCommands) { 12 | ioSocketCommands["testRunningStateChange"] = "testRunningStateChange"; 13 | ioSocketCommands["singleRPSfinished"] = "singleRPSfinished"; 14 | ioSocketCommands["allRPSfinished"] = "allRPSfinished"; 15 | ioSocketCommands["errorInfo"] = "errorInfo"; 16 | ioSocketCommands["currentRPSProgress"] = "currentRPSProgress"; 17 | })(ioSocketCommands = exports.ioSocketCommands || (exports.ioSocketCommands = {})); 18 | var HTTPMethods; 19 | (function (HTTPMethods) { 20 | HTTPMethods["GET"] = "GET"; 21 | HTTPMethods["POST"] = "POST"; 22 | HTTPMethods["PUT"] = "PUT"; 23 | HTTPMethods["DELETE"] = "DELETE"; 24 | HTTPMethods["PATCH"] = "PATCH"; 25 | HTTPMethods["HEAD"] = "HEAD"; 26 | HTTPMethods["CONNECT"] = "CONNECT"; 27 | HTTPMethods["TRACE"] = "TRACE"; 28 | })(HTTPMethods = exports.HTTPMethods || (exports.HTTPMethods = {})); 29 | -------------------------------------------------------------------------------- /dist/server/sampleTestVariables.d.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | import { TestConfigData, GlobalVariables } from './interfaces'; 3 | declare let timeOutArray: any, abortController: any, globalTestConfig: TestConfigData, globalVariables: GlobalVariables, io: Server; 4 | declare const initializeVariables: () => void; 5 | export { globalTestConfig, timeOutArray, abortController, globalVariables, io, initializeVariables, }; 6 | -------------------------------------------------------------------------------- /dist/server/sampleTestVariables.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.initializeVariables = exports.io = exports.globalVariables = exports.abortController = exports.timeOutArray = exports.globalTestConfig = void 0; 7 | const http_1 = __importDefault(require("http")); 8 | const socket_io_1 = require("socket.io"); 9 | const abort_controller_1 = __importDefault(require("abort-controller")); 10 | const interfaces_1 = require("./interfaces"); 11 | let timeOutArray, abortController, globalTestConfig, timeArrRoutes, pulledDataFromTest, globalVariables, io; 12 | exports.timeOutArray = timeOutArray; 13 | exports.abortController = abortController; 14 | exports.globalTestConfig = globalTestConfig; 15 | exports.globalVariables = globalVariables; 16 | exports.io = io; 17 | const initializeVariables = () => { 18 | exports.io = io = new socket_io_1.Server(); 19 | io.emit = jest.fn(); 20 | exports.timeOutArray = timeOutArray = [setTimeout(() => 0, 1000)]; 21 | clearTimeout(timeOutArray[0]); 22 | exports.abortController = abortController = new abort_controller_1.default(); 23 | exports.globalTestConfig = globalTestConfig = { 24 | rpsInterval: 100, 25 | startRPS: 100, 26 | endRPS: 100, 27 | testLength: 1, 28 | inputsData: [ 29 | { 30 | method: 'GET', 31 | targetURL: 'http://localhost:3000', 32 | percentage: 100, 33 | jagTesterEnabled: true, 34 | }, 35 | ], 36 | }; 37 | timeArrRoutes = { 38 | // this key is used as the route name 39 | '/': { 40 | //this key is used as the rps number 41 | '100': { 42 | receivedTotalTime: 800, 43 | errorCount: 0, 44 | successfulResCount: 100, 45 | }, 46 | }, 47 | }; 48 | // 2 average milliseconds for each, and last one should be 0 because the jagtester doesnt calculate the last one 49 | pulledDataFromTest = { 50 | '100': { 51 | //used as route 52 | '/': { 53 | '1': { 54 | reqId: '1', 55 | reqRoute: '/', 56 | middlewares: [ 57 | { fnName: 'fn1', elapsedTime: 1 }, 58 | { fnName: 'fn2', elapsedTime: 1 }, 59 | { fnName: 'fn3', elapsedTime: 1 }, 60 | { fnName: 'fn4', elapsedTime: 0 }, 61 | ], 62 | }, 63 | '2': { 64 | reqId: '2', 65 | reqRoute: '/', 66 | middlewares: [ 67 | { fnName: 'fn1', elapsedTime: 3 }, 68 | { fnName: 'fn2', elapsedTime: 3 }, 69 | { fnName: 'fn3', elapsedTime: 3 }, 70 | { fnName: 'fn4', elapsedTime: 0 }, 71 | ], 72 | }, 73 | }, 74 | }, 75 | }; 76 | const isTestRunningListener = (val) => { 77 | io.emit(interfaces_1.ioSocketCommands.testRunningStateChange, val); 78 | }; 79 | exports.globalVariables = globalVariables = { 80 | currentInterval: 100, 81 | errorCount: 0, 82 | successfulResCount: 100, 83 | abortController, 84 | timeArrRoutes, 85 | timeOutArray, 86 | pulledDataFromTest, 87 | isTestRunningInternal: true, 88 | isTestRunningListener, 89 | isTestRunning: true, 90 | agent: new http_1.default.Agent({ keepAlive: true }), 91 | }; 92 | }; 93 | exports.initializeVariables = initializeVariables; 94 | -------------------------------------------------------------------------------- /dist/server/server.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /// 3 | import { Server } from 'socket.io'; 4 | declare const http: import("http").Server; 5 | declare const io: Server; 6 | export { http, io }; 7 | -------------------------------------------------------------------------------- /dist/server/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | exports.io = exports.http = void 0; 5 | // TODO use cluster to imrpove our server performance 6 | const app_1 = require("./app"); 7 | const http_1 = require("http"); 8 | const socket_io_1 = require("socket.io"); 9 | const http = http_1.createServer(app_1.app); 10 | exports.http = http; 11 | const io = new socket_io_1.Server(http); 12 | exports.io = io; 13 | -------------------------------------------------------------------------------- /dist/server/testrouter.d.ts: -------------------------------------------------------------------------------- 1 | import { GlobalVariables } from './interfaces'; 2 | declare const router: import("express-serve-static-core").Router; 3 | declare const globalVariables: GlobalVariables; 4 | export default router; 5 | export { globalVariables }; 6 | -------------------------------------------------------------------------------- /dist/server/testrouter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.globalVariables = void 0; 7 | const express_1 = __importDefault(require("express")); 8 | const node_fetch_1 = __importDefault(require("node-fetch")); 9 | const http_1 = __importDefault(require("http")); 10 | const server_1 = require("./server"); 11 | const interfaces_1 = require("./interfaces"); 12 | const sendRequestsAtRPS_1 = __importDefault(require("./helpers/sendRequestsAtRPS")); 13 | const abort_controller_1 = __importDefault(require("abort-controller")); 14 | const router = express_1.default.Router(); 15 | const globalVariables = { 16 | currentInterval: 0, 17 | errorCount: 0, 18 | successfulResCount: 0, 19 | abortController: new abort_controller_1.default(), 20 | timeArrRoutes: {}, 21 | timeOutArray: [], 22 | pulledDataFromTest: {}, 23 | isTestRunningInternal: false, 24 | agent: new http_1.default.Agent({ keepAlive: true }), 25 | isTestRunningListener: (val) => { 26 | server_1.io.emit(interfaces_1.ioSocketCommands.testRunningStateChange, val); 27 | }, 28 | set isTestRunning(val) { 29 | this.isTestRunningInternal = val; 30 | this.isTestRunningListener(val); 31 | }, 32 | get isTestRunning() { 33 | return this.isTestRunningInternal; 34 | }, 35 | }; 36 | exports.globalVariables = globalVariables; 37 | let globalTestConfig; 38 | router.post('/startmultiple', (req, res) => { 39 | if (!globalVariables.isTestRunning) { 40 | globalVariables.isTestRunning = true; 41 | globalVariables.timeArrRoutes = {}; 42 | globalVariables.pulledDataFromTest = {}; 43 | globalVariables.currentInterval = 0; 44 | globalTestConfig = { 45 | rpsInterval: req.body.rpsInterval, 46 | startRPS: req.body.startRPS, 47 | endRPS: req.body.endRPS, 48 | testLength: req.body.testLength, 49 | inputsData: req.body.inputsData, 50 | }; 51 | sendRequestsAtRPS_1.default(globalVariables, globalTestConfig, server_1.io); 52 | } 53 | res.sendStatus(200); 54 | }); 55 | router.post('/checkjagtester', (req, res) => { 56 | node_fetch_1.default(req.body.inputURL, { 57 | method: req.body.method, 58 | agent: globalVariables.agent, 59 | headers: { 60 | jagtestercommand: interfaces_1.Jagtestercommands.updateLayer.toString(), 61 | }, 62 | }) 63 | .then((fetchRes) => fetchRes.json()) 64 | .then((data) => res.json(data)) 65 | .catch(() => res.json({ jagtester: false })); 66 | }); 67 | router.get('/stopTest', (req, res) => { 68 | globalVariables.abortController.abort(); 69 | res.sendStatus(200); 70 | }); 71 | exports.default = router; 72 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | // Sync object 4 | const config: Config.InitialOptions = { 5 | verbose: true, 6 | preset: 'ts-jest', 7 | testEnvironment: 'node', 8 | }; 9 | export default config; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jagtester-parent", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/server/index.js", 6 | "scripts": { 7 | "moveimages": "mv ./splash/bundle/demo/*.png ./splash/bundle/", 8 | "build-front": "JAG=real webpack --config webpack.prod.config.ts && cp README.md dist/README.md", 9 | "build-back": "tsc --build src && cp README.md dist/README.md", 10 | "build": "tsc --build src && JAG=real webpack --config webpack.prod.config.ts && cp README.md dist/README.md", 11 | "dev-front": "webpack serve --config webpack.dev.config.ts", 12 | "dev-back": "tsc --build -w src & nodemon ./dist/server/index.js", 13 | "dev": "tsc --build -w src & nodemon ./dist/server/index.js & webpack serve --config webpack.dev.config.ts", 14 | "prod": "node ./dist/server/index.js", 15 | "splash-build": "webpack --config ./splash/webpack.prod.config.ts && cp -a ./splash/tocopy/ ./splash/bundle/ && JAG=demo webpack --config webpack.prod.config.ts && yarn moveimages", 16 | "splash-dev": "webpack serve --config ./splash/webpack.dev.config.ts", 17 | "test": "jest", 18 | "test-w": "jest --watch --detectOpenHandles --verbose" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/oslabs-beta/jagtester.git" 23 | }, 24 | "keywords": [], 25 | "author": "", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/oslabs-beta/jagtester/issues" 29 | }, 30 | "homepage": "https://github.com/oslabs-beta/jagtester#readme", 31 | "dependencies": { 32 | "@material-ui/core": "^4.11.4", 33 | "@material-ui/icons": "^4.11.2", 34 | "@reduxjs/toolkit": "^1.5.1", 35 | "@types/node-fetch": "^2.5.10", 36 | "abort-controller": "^3.0.0", 37 | "chart.js": "^3.3.2", 38 | "express": "^4.17.1", 39 | "file-loader": "^6.2.0", 40 | "node-fetch": "^2.6.1", 41 | "open": "^8.2.0", 42 | "path": "^0.12.7", 43 | "react": "^17.0.2", 44 | "react-bootstrap": "^1.6.0", 45 | "react-chartjs-2": "^3.0.3", 46 | "react-dark-mode-toggle": "^0.2.0", 47 | "react-dom": "^17.0.2", 48 | "react-redux": "^7.2.4", 49 | "react-router-bootstrap": "^0.25.0", 50 | "react-router-dom": "^5.2.0", 51 | "redux-persist": "^6.0.0", 52 | "response-time": "^2.3.2", 53 | "socket.io": "^4.1.2", 54 | "socket.io-client": "^4.1.2" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.14.3", 58 | "@babel/plugin-transform-runtime": "^7.14.3", 59 | "@babel/preset-env": "^7.14.2", 60 | "@babel/preset-react": "^7.13.13", 61 | "@babel/preset-typescript": "^7.13.0", 62 | "@babel/runtime": "^7.14.0", 63 | "@material-ui/lab": "^4.0.0-alpha.58", 64 | "@svgr/webpack": "^5.5.0", 65 | "@types/chart.js": "^2.9.32", 66 | "@types/express": "^4.17.12", 67 | "@types/fork-ts-checker-webpack-plugin": "^0.4.5", 68 | "@types/jest": "^26.0.23", 69 | "@types/lodash": "^4.14.170", 70 | "@types/node": "^15.6.1", 71 | "@types/react": "^17.0.8", 72 | "@types/react-dom": "^17.0.5", 73 | "@types/react-redux": "^7.1.16", 74 | "@types/react-router-bootstrap": "^0.24.5", 75 | "@types/react-router-dom": "^5.1.7", 76 | "@types/response-time": "^2.3.4", 77 | "@types/socket.io": "^3.0.2", 78 | "@types/supertest": "^2.0.11", 79 | "@types/webpack": "^5.28.0", 80 | "@types/webpack-dev-server": "^3.11.4", 81 | "@typescript-eslint/eslint-plugin": "^4.25.0", 82 | "@typescript-eslint/parser": "^4.25.0", 83 | "animate.css": "^4.1.1", 84 | "babel-loader": "^8.2.2", 85 | "clean-webpack-plugin": "^4.0.0-alpha.0", 86 | "css-loader": "^5.2.6", 87 | "eslint": "^7.27.0", 88 | "eslint-plugin-react": "^7.23.2", 89 | "eslint-plugin-react-hooks": "^4.2.0", 90 | "eslint-webpack-plugin": "^2.5.4", 91 | "fork-ts-checker-webpack-plugin": "^6.2.10", 92 | "html-webpack-plugin": "^5.3.1", 93 | "image-minimizer-webpack-plugin": "^2.2.0", 94 | "imagemin-gifsicle": "^7.0.0", 95 | "imagemin-jpegtran": "^7.0.0", 96 | "imagemin-optipng": "^8.0.0", 97 | "imagemin-svgo": "^9.0.0", 98 | "jest": "^27.0.5", 99 | "jpegtran": "^2.0.0", 100 | "nodemon": "^2.0.7", 101 | "react-reveal": "^1.2.2", 102 | "sass": "^1.35.1", 103 | "sass-loader": "^12.1.0", 104 | "style-loader": "^2.0.0", 105 | "supertest": "^6.1.3", 106 | "ts-jest": "^27.0.3", 107 | "ts-node": "^10.0.0", 108 | "typescript": "^4.2.4", 109 | "webpack": "^5.37.1", 110 | "webpack-cli": "^4.7.0", 111 | "webpack-dev-server": "^3.11.2" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /splash/bundle/.well-known/acme-challenge/7z1xjuRhh4VD8Kbn8dcds-I-TaggOL2m-m8mMQ4KFXc: -------------------------------------------------------------------------------- 1 | 7z1xjuRhh4VD8Kbn8dcds-I-TaggOL2m-m8mMQ4KFXc.nMCZwPLoHO9tblyT8xvWvouJU0q--nR2zvjNFQN5bFQ -------------------------------------------------------------------------------- /splash/bundle/.well-known/acme-challenge/JYbvNgrdXxoNqqzOq7PL4OdCStEdaFHkt3fdr27mT_E: -------------------------------------------------------------------------------- 1 | JYbvNgrdXxoNqqzOq7PL4OdCStEdaFHkt3fdr27mT_E.nMCZwPLoHO9tblyT8xvWvouJU0q--nR2zvjNFQN5bFQ -------------------------------------------------------------------------------- /splash/bundle/17e66cea79c0a77138e0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/bundle/17e66cea79c0a77138e0.png -------------------------------------------------------------------------------- /splash/bundle/487337271d3e589bb2ca.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/bundle/487337271d3e589bb2ca.jpg -------------------------------------------------------------------------------- /splash/bundle/6742f36679004f281e61a3c7c4bfecc8.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/bundle/6ef6d3baf95397d816ffb4e6fe14daf9.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/bundle/9e6a7306d2f701aff33a.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/bundle/9e6a7306d2f701aff33a.gif -------------------------------------------------------------------------------- /splash/bundle/_redirects: -------------------------------------------------------------------------------- 1 | /results /demo -------------------------------------------------------------------------------- /splash/bundle/a64cac4786b3a3f26ee2ae6dbecca056.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/bundle/ad930f133b588618a67b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/bundle/ad930f133b588618a67b.jpg -------------------------------------------------------------------------------- /splash/bundle/b138d6d7c0c8f27ac4f8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/bundle/b138d6d7c0c8f27ac4f8.jpg -------------------------------------------------------------------------------- /splash/bundle/c82b0acf13f3f94459071d557ecee477.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/bundle/c82b0acf13f3f94459071d557ecee477.png -------------------------------------------------------------------------------- /splash/bundle/d151a06aa6206494cbdb.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/bundle/d151a06aa6206494cbdb.gif -------------------------------------------------------------------------------- /splash/bundle/d6f0de647e53361b017989b0a1105021.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/bundle/dd7777d0e8f795b523dc376f99063ff5.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/bundle/demo/index.html: -------------------------------------------------------------------------------- 1 | Jagtester
-------------------------------------------------------------------------------- /splash/bundle/demo/main.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2018 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /*! 14 | * Chart.js v3.3.2 15 | * https://www.chartjs.org 16 | * (c) 2021 Chart.js Contributors 17 | * Released under the MIT License 18 | */ 19 | 20 | /** 21 | * A better abstraction over CSS. 22 | * 23 | * @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present 24 | * @website https://github.com/cssinjs/jss 25 | * @license MIT 26 | */ 27 | 28 | /** @license React v0.20.2 29 | * scheduler.production.min.js 30 | * 31 | * Copyright (c) Facebook, Inc. and its affiliates. 32 | * 33 | * This source code is licensed under the MIT license found in the 34 | * LICENSE file in the root directory of this source tree. 35 | */ 36 | 37 | /** @license React v16.13.1 38 | * react-is.production.min.js 39 | * 40 | * Copyright (c) Facebook, Inc. and its affiliates. 41 | * 42 | * This source code is licensed under the MIT license found in the 43 | * LICENSE file in the root directory of this source tree. 44 | */ 45 | 46 | /** @license React v17.0.2 47 | * react-dom.production.min.js 48 | * 49 | * Copyright (c) Facebook, Inc. and its affiliates. 50 | * 51 | * This source code is licensed under the MIT license found in the 52 | * LICENSE file in the root directory of this source tree. 53 | */ 54 | 55 | /** @license React v17.0.2 56 | * react.production.min.js 57 | * 58 | * Copyright (c) Facebook, Inc. and its affiliates. 59 | * 60 | * This source code is licensed under the MIT license found in the 61 | * LICENSE file in the root directory of this source tree. 62 | */ 63 | -------------------------------------------------------------------------------- /splash/bundle/f4904cece825a96909a0c440c50feb20.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/bundle/index.html: -------------------------------------------------------------------------------- 1 | Jagtester
-------------------------------------------------------------------------------- /splash/bundle/main.8dc9fad69eb45e05a276.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2018 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /** 14 | * A better abstraction over CSS. 15 | * 16 | * @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present 17 | * @website https://github.com/cssinjs/jss 18 | * @license MIT 19 | */ 20 | 21 | /** @license React v0.20.2 22 | * scheduler.production.min.js 23 | * 24 | * Copyright (c) Facebook, Inc. and its affiliates. 25 | * 26 | * This source code is licensed under the MIT license found in the 27 | * LICENSE file in the root directory of this source tree. 28 | */ 29 | 30 | /** @license React v16.13.1 31 | * react-is.production.min.js 32 | * 33 | * Copyright (c) Facebook, Inc. and its affiliates. 34 | * 35 | * This source code is licensed under the MIT license found in the 36 | * LICENSE file in the root directory of this source tree. 37 | */ 38 | 39 | /** @license React v17.0.2 40 | * react-dom.production.min.js 41 | * 42 | * Copyright (c) Facebook, Inc. and its affiliates. 43 | * 44 | * This source code is licensed under the MIT license found in the 45 | * LICENSE file in the root directory of this source tree. 46 | */ 47 | 48 | /** @license React v17.0.2 49 | * react.production.min.js 50 | * 51 | * Copyright (c) Facebook, Inc. and its affiliates. 52 | * 53 | * This source code is licensed under the MIT license found in the 54 | * LICENSE file in the root directory of this source tree. 55 | */ 56 | -------------------------------------------------------------------------------- /splash/bundle/netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/results" 3 | to = "/demo" 4 | 5 | [[redirects]] 6 | from = "/results" 7 | to = "/demo" -------------------------------------------------------------------------------- /splash/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: string; 3 | export default content; 4 | } 5 | declare module '*.png' { 6 | const content: string; 7 | export default content; 8 | } 9 | declare module '*.gif' { 10 | const content: string; 11 | export default content; 12 | } 13 | declare module '*.jpg' { 14 | const content: string; 15 | export default content; 16 | } 17 | 18 | declare module 'react-reveal/Fade'; 19 | 20 | declare module 'react-scrollspy-nav'; 21 | -------------------------------------------------------------------------------- /splash/img/exportdelete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/img/exportdelete.gif -------------------------------------------------------------------------------- /splash/img/favicon2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/img/favicon3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/img/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/img/install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/img/install.png -------------------------------------------------------------------------------- /splash/img/linkedin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/img/logotext.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/img/npm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/img/singleroute.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/img/singleroute.gif -------------------------------------------------------------------------------- /splash/img/team1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/img/team1.jpg -------------------------------------------------------------------------------- /splash/img/team2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/img/team2.jpg -------------------------------------------------------------------------------- /splash/img/team3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/splash/img/team3.jpg -------------------------------------------------------------------------------- /splash/img/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splash/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Jagtester 8 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 32 | 33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /splash/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | import './style.scss'; 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /splash/style.scss: -------------------------------------------------------------------------------- 1 | $px: '(0.8px + 0.04335vw)'; 2 | 3 | #root { 4 | // background-color: rgb(61, 66, 124); 5 | // min-height: 100vh; 6 | font-family: Helvetica; 7 | color: white; 8 | } 9 | body { 10 | position: relative; 11 | } 12 | footer { 13 | position: relative; 14 | bottom: 0; 15 | left: 0; 16 | right: 0; 17 | color: white; 18 | } 19 | 20 | .image-cropper { 21 | width: 400px; 22 | height: 400px; 23 | position: relative; 24 | overflow: hidden; 25 | border-radius: 50%; 26 | width: 100%; 27 | height: auto; 28 | } 29 | 30 | .social-team { 31 | margin: 0 10px 0 10px; 32 | } 33 | 34 | $grid-breakpoints: ( 35 | xs: 0, 36 | sm: 576px, 37 | md: 768px, 38 | lg: 992px, 39 | xl: 1200px, 40 | xxl: 1400px, 41 | ); 42 | 43 | h1 { 44 | font-size: calc(#{$px} * 30); 45 | } 46 | h2 { 47 | font-size: calc(#{$px} * 28); 48 | } 49 | h3 { 50 | font-size: calc(#{$px} * 24); 51 | } 52 | h4 { 53 | font-size: calc(#{$px} * 20); 54 | } 55 | h5 { 56 | font-size: calc(#{$px} * 13); 57 | } 58 | h6 { 59 | font-size: calc(#{$px} * 10); 60 | } 61 | p { 62 | font-size: calc(#{$px} * 15); 63 | } 64 | 65 | .navbar-collapse.collapsing, 66 | .navbar-collapse.show { 67 | background-color: rgba(45, 45, 45, 0.75); 68 | border-color: white; 69 | } 70 | -------------------------------------------------------------------------------- /splash/tocopy/.well-known/acme-challenge/7z1xjuRhh4VD8Kbn8dcds-I-TaggOL2m-m8mMQ4KFXc: -------------------------------------------------------------------------------- 1 | 7z1xjuRhh4VD8Kbn8dcds-I-TaggOL2m-m8mMQ4KFXc.nMCZwPLoHO9tblyT8xvWvouJU0q--nR2zvjNFQN5bFQ -------------------------------------------------------------------------------- /splash/tocopy/.well-known/acme-challenge/JYbvNgrdXxoNqqzOq7PL4OdCStEdaFHkt3fdr27mT_E: -------------------------------------------------------------------------------- 1 | JYbvNgrdXxoNqqzOq7PL4OdCStEdaFHkt3fdr27mT_E.nMCZwPLoHO9tblyT8xvWvouJU0q--nR2zvjNFQN5bFQ -------------------------------------------------------------------------------- /splash/tocopy/_redirects: -------------------------------------------------------------------------------- 1 | /results /demo -------------------------------------------------------------------------------- /splash/tocopy/netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/results" 3 | to = "/demo" 4 | 5 | [[redirects]] 6 | from = "/results/" 7 | to = "/demo" -------------------------------------------------------------------------------- /splash/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "lib": ["dom", "dom.iterable", "esnext", "webworker", "es6", "scripthost"], 5 | "allowJs": true, 6 | "allowSyntheticDefaultImports": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react", 16 | "paths": { 17 | "*": ["../../node_modules/*"] 18 | } 19 | }, 20 | "include": ["./custom.d.ts", "./*"] 21 | } 22 | -------------------------------------------------------------------------------- /splash/webpack.dev.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 5 | import ESLintPlugin from 'eslint-webpack-plugin'; 6 | 7 | import { Configuration as WebpackConfiguration } from 'webpack'; 8 | import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server'; 9 | 10 | interface Configuration extends WebpackConfiguration { 11 | devServer?: WebpackDevServerConfiguration; 12 | } 13 | 14 | const config: Configuration = { 15 | mode: 'development', 16 | output: { 17 | publicPath: '/', 18 | }, 19 | entry: path.join(__dirname, 'index.tsx'), 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.(ts|js)x?$/i, 24 | exclude: /node_modules/, 25 | use: { 26 | loader: 'babel-loader', 27 | options: { 28 | presets: [ 29 | '@babel/preset-env', 30 | '@babel/preset-react', 31 | '@babel/preset-typescript', 32 | ], 33 | }, 34 | }, 35 | }, 36 | { 37 | test: /\.(png|jp(e*)g|gif)$/, 38 | use: [ 39 | { 40 | loader: 'file-loader', 41 | }, 42 | ], 43 | }, 44 | { 45 | test: /\.svg$/, 46 | use: ['babel-loader', '@svgr/webpack', 'file-loader'], 47 | }, 48 | { 49 | test: /\.s[ac]ss$/i, 50 | use: ['style-loader', 'css-loader', 'sass-loader'], 51 | }, 52 | ], 53 | }, 54 | resolve: { 55 | extensions: ['.tsx', '.ts', '.js'], 56 | modules: [path.resolve(__dirname, '../node_modules'), path.resolve(__dirname, './splash')], 57 | }, 58 | plugins: [ 59 | new HtmlWebpackPlugin({ 60 | template: path.join(__dirname, 'index.html'), 61 | favicon: path.join(__dirname, 'img/favicon.svg'), 62 | }), 63 | new webpack.HotModuleReplacementPlugin(), 64 | new ForkTsCheckerWebpackPlugin({ 65 | async: false, 66 | typescript: { 67 | configFile: path.join(__dirname, 'tsconfig.json'), 68 | }, 69 | }), 70 | new ESLintPlugin({ 71 | extensions: ['js', 'jsx', 'ts', 'tsx'], 72 | }), 73 | ], 74 | devtool: 'inline-source-map', 75 | devServer: { 76 | contentBase: path.join(__dirname, 'build'), 77 | historyApiFallback: true, 78 | port: 8080, 79 | open: false, 80 | hot: true, 81 | }, 82 | }; 83 | 84 | export default config; 85 | -------------------------------------------------------------------------------- /splash/webpack.prod.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 5 | import ESLintPlugin from 'eslint-webpack-plugin'; 6 | import { CleanWebpackPlugin } from 'clean-webpack-plugin'; 7 | import ImageMinimizerPlugin from 'image-minimizer-webpack-plugin'; 8 | 9 | const config: webpack.Configuration = { 10 | mode: 'production', 11 | entry: path.join(__dirname, 'index.tsx'), 12 | output: { 13 | path: path.resolve(__dirname, 'bundle'), 14 | filename: '[name].[contenthash].js', 15 | publicPath: '', 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(ts|js)x?$/i, 21 | exclude: /node_modules/, 22 | use: { 23 | loader: 'babel-loader', 24 | options: { 25 | presets: [ 26 | '@babel/preset-env', 27 | '@babel/preset-react', 28 | '@babel/preset-typescript', 29 | ], 30 | }, 31 | }, 32 | }, 33 | { 34 | test: /\.(jpe?g|png|gif)$/i, 35 | type: 'asset', 36 | }, 37 | { 38 | test: /\.svg$/, 39 | use: ['babel-loader', '@svgr/webpack', 'file-loader'], 40 | }, 41 | { 42 | test: /\.s[ac]ss$/i, 43 | use: ['style-loader', 'css-loader', 'sass-loader'], 44 | }, 45 | ], 46 | }, 47 | resolve: { 48 | extensions: ['.tsx', '.ts', '.js'], 49 | modules: [path.resolve(__dirname, '../node_modules'), path.resolve(__dirname, './splash')], 50 | }, 51 | plugins: [ 52 | new HtmlWebpackPlugin({ 53 | template: path.join(__dirname, 'index.html'), 54 | favicon: path.join(__dirname, 'img/favicon.svg'), 55 | }), 56 | new ForkTsCheckerWebpackPlugin({ 57 | async: false, 58 | typescript: { 59 | configFile: path.join(__dirname, 'tsconfig.json'), 60 | }, 61 | }), 62 | new ESLintPlugin({ 63 | extensions: ['js', 'jsx', 'ts', 'tsx'], 64 | }), 65 | new CleanWebpackPlugin(), 66 | 67 | new ImageMinimizerPlugin({ 68 | minimizerOptions: { 69 | // Lossless optimization with custom option 70 | // Feel free to experiment with options for better result for you 71 | plugins: [ 72 | ['gifsicle', { interlaced: true, optimizationLevel: 3, color: 16 }], 73 | ['jpegtran', { progressive: true }], 74 | ['optipng', { optimizationLevel: 5 }], 75 | ], 76 | }, 77 | }), 78 | ], 79 | }; 80 | 81 | export default config; 82 | -------------------------------------------------------------------------------- /src/client/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import TestPage from './pages/testconfigpage'; 3 | import ResultsPage from './pages/results'; 4 | import Navigation from './components/navigation'; 5 | import { BrowserRouter, Switch, Route } from 'react-router-dom'; 6 | import Container from 'react-bootstrap/Container'; 7 | import { AllPulledDataFromTest } from './interfaces'; 8 | import socketIOClient from 'socket.io-client'; 9 | import { useAppDispatch, useAppSelector } from './state/hooks'; 10 | import Actions from './state/actions/actions'; 11 | import Modal from './components/modal'; 12 | import { ioSocketCommands } from '../client/interfaces'; 13 | 14 | //MUI imports 15 | import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; 16 | import CssBaseline from '@material-ui/core/CssBaseline'; 17 | 18 | const App: () => JSX.Element = () => { 19 | const dispatch = useAppDispatch(); 20 | 21 | const prefersDarkMode = useAppSelector((state) => state.darkMode); 22 | 23 | const theme = React.useMemo( 24 | () => 25 | createMuiTheme({ 26 | palette: { 27 | type: prefersDarkMode ? 'dark' : 'light', 28 | }, 29 | }), 30 | [prefersDarkMode] 31 | ); 32 | 33 | useEffect(() => { 34 | if (process.env.JAG !== 'demo') { 35 | const socket = socketIOClient(); 36 | // start----------------------------------- socket io funcitonality 37 | socket.on(ioSocketCommands.singleRPSfinished, (rps: number) => { 38 | dispatch(Actions.SetCurRunningRPS(rps)); 39 | dispatch(Actions.SetCurRPSpercent(0)); 40 | }); 41 | socket.on(ioSocketCommands.testRunningStateChange, (isTestRunning: boolean) => { 42 | dispatch(Actions.SetIsTestRunning(isTestRunning)); 43 | }); 44 | socket.on( 45 | ioSocketCommands.allRPSfinished, 46 | (allPulledDataFromTest: AllPulledDataFromTest[]) => { 47 | dispatch(Actions.SetReceivedData(allPulledDataFromTest)); 48 | } 49 | ); 50 | socket.on(ioSocketCommands.errorInfo, (errName: string) => { 51 | dispatch(Actions.SetShowModal(true)); 52 | dispatch(Actions.SetModalError(errName)); 53 | }); 54 | socket.on(ioSocketCommands.currentRPSProgress, (percent: number) => { 55 | dispatch(Actions.SetCurRPSpercent(percent)); 56 | }); 57 | 58 | return function cleanup() { 59 | socket.off(ioSocketCommands.singleRPSfinished); 60 | socket.off(ioSocketCommands.testRunningStateChange); 61 | socket.off(ioSocketCommands.allRPSfinished); 62 | socket.off(ioSocketCommands.errorInfo); 63 | socket.off(ioSocketCommands.currentRPSProgress); 64 | }; 65 | } 66 | }, [dispatch]); 67 | 68 | // end ----------------------------------- socket io funcitonality 69 | return ( 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ); 89 | }; 90 | 91 | export default App; 92 | -------------------------------------------------------------------------------- /src/client/components/darkModeSwitch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles, Theme, createStyles } from '@material-ui/core/styles'; 3 | import Switch, { SwitchClassKey, SwitchProps } from '@material-ui/core/Switch'; 4 | 5 | import { useAppDispatch, useAppSelector } from '../state/hooks'; 6 | import Actions from '../state/actions/actions'; 7 | 8 | interface Styles extends Partial> { 9 | focusVisible?: string; 10 | } 11 | 12 | interface Props extends SwitchProps { 13 | classes: Styles; 14 | } 15 | 16 | const IOSSwitch = withStyles((theme: Theme) => 17 | createStyles({ 18 | root: { 19 | width: 42, 20 | height: 26, 21 | padding: 0, 22 | margin: theme.spacing(3), 23 | }, 24 | switchBase: { 25 | padding: 1, 26 | '&$checked': { 27 | transform: 'translateX(16px)', 28 | color: theme.palette.common.white, 29 | '& + $track': { 30 | backgroundColor: '#636896', 31 | opacity: 1, 32 | border: 'none', 33 | }, 34 | }, 35 | '&$focusVisible $thumb': { 36 | color: '#636896', 37 | border: '6px solid #fff', 38 | }, 39 | }, 40 | thumb: { 41 | width: 24, 42 | height: 24, 43 | border: `2px solid ${theme.palette.grey[400]}`, 44 | }, 45 | track: { 46 | borderRadius: 26 / 2, 47 | border: `1px solid ${theme.palette.grey[400]}`, 48 | backgroundColor: theme.palette.grey[50], 49 | opacity: 1, 50 | transition: theme.transitions.create(['background-color', 'border']), 51 | }, 52 | checked: {}, 53 | focusVisible: {}, 54 | }) 55 | )(({ classes }: Props) => { 56 | const dispatch = useAppDispatch(); 57 | const darkMode = useAppSelector((state) => state.darkMode); 58 | return ( 59 | dispatch(Actions.SetDarkMode(e.target.checked))} 71 | /> 72 | ); 73 | }); 74 | 75 | export default IOSSwitch; 76 | -------------------------------------------------------------------------------- /src/client/components/modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from 'react-bootstrap/Button'; 3 | import Modal from 'react-bootstrap/Modal'; 4 | 5 | import { useAppDispatch, useAppSelector } from '../state/hooks'; 6 | import Actions from '../state/actions/actions'; 7 | 8 | const ModalCustom: () => JSX.Element = () => { 9 | const showModal = useAppSelector((state) => state.showModal); 10 | const error = useAppSelector((state) => state.modalError); 11 | const dispatch = useAppDispatch(); 12 | 13 | const handleClose = () => dispatch(Actions.SetShowModal(false)); 14 | return ( 15 | <> 16 | 17 | 18 | Error 19 | 20 | {error} 21 | 22 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default ModalCustom; 32 | -------------------------------------------------------------------------------- /src/client/components/navigation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from 'react-bootstrap/Navbar'; 3 | import Nav from 'react-bootstrap/Nav'; 4 | import Container from 'react-bootstrap/Container'; 5 | import TestProgrss from '../components/testConfigComponents/testProgress'; 6 | import { LinkContainer } from 'react-router-bootstrap'; 7 | import Asset from '../img/Asset.svg'; 8 | 9 | import DarkSwitch from './darkModeSwitch'; 10 | const Navigation: () => JSX.Element = () => { 11 | return ( 12 | 19 | 24 | 32 | 43 | 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default Navigation; 53 | -------------------------------------------------------------------------------- /src/client/components/resultsComponents/graphs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Bar } from 'react-chartjs-2'; 3 | import { PulledDataFromTest, ChartDataSet } from '../../interfaces'; 4 | 5 | import { useAppSelector } from '../../state/hooks'; 6 | 7 | const shadedColor = ( 8 | index: number, 9 | totalCount: number, 10 | darkMode: boolean, 11 | color1 = [255, 175, 145], 12 | color2 = [30, 32, 60] 13 | ) => { 14 | const finalColor = []; 15 | for (let i = 0; i < 3; i++) { 16 | let color = color1[i] + (index * (color2[i] - color1[i])) / totalCount; 17 | if (darkMode) color = 255 - (255 - color) * 0.8; 18 | finalColor.push(Math.floor(color)); 19 | } 20 | return `rgba(${finalColor[0]}, ${finalColor[1]}, ${finalColor[2]}, 1)`; 21 | // return '#' + Math.floor(Math.random() * 16777215).toString(16) 22 | }; 23 | 24 | const StackedBar: (props: { 25 | testData: PulledDataFromTest; 26 | singleRoute: boolean; 27 | routeName?: string; 28 | }) => JSX.Element = ({ testData, singleRoute, routeName }) => { 29 | const darkMode = useAppSelector((state) => state.darkMode); 30 | const chartOptions = { 31 | plugins: { 32 | title: { 33 | display: true, 34 | text: singleRoute ? `Route - ${routeName}` : 'All routes', 35 | color: darkMode ? 'white' : 'black', 36 | font: { 37 | size: '30rem', 38 | }, 39 | }, 40 | }, 41 | responsive: true, 42 | color: darkMode ? 'white' : 'black', 43 | scales: { 44 | x: { 45 | title: { 46 | text: 'RPS', 47 | display: true, 48 | color: darkMode ? 'white' : 'black', 49 | }, 50 | stacked: singleRoute, 51 | ticks: { 52 | color: darkMode ? 'white' : 'black', 53 | }, 54 | grid: { 55 | color: darkMode ? '#606060' : '#dddddd', 56 | }, 57 | }, 58 | y: { 59 | title: { 60 | text: 'milliseconds', 61 | display: true, 62 | color: darkMode ? 'white' : 'black', 63 | }, 64 | stacked: singleRoute, 65 | beginAtZero: true, 66 | ticks: { 67 | color: darkMode ? 'white' : 'black', 68 | }, 69 | grid: { 70 | color: darkMode ? '#606060' : '#dddddd', 71 | }, 72 | }, 73 | yError: { 74 | display: !singleRoute, 75 | position: 'right', 76 | title: { 77 | text: 'Error %', 78 | display: true, 79 | color: darkMode ? 'white' : 'black', 80 | }, 81 | stacked: false, 82 | beginAtZero: true, 83 | ticks: { 84 | color: darkMode ? 'white' : 'black', 85 | }, 86 | grid: { 87 | display: false, 88 | }, 89 | }, 90 | }, 91 | }; 92 | 93 | const dataSetArray: ChartDataSet[] = []; 94 | const rpsArr: string[] = []; 95 | 96 | //this object will have property of each route, with an array of recieved times 97 | const resultObj: { 98 | [key: string]: { 99 | elapsedTimes: number[]; 100 | errorCounts: number[]; 101 | }; 102 | } = {}; 103 | 104 | const resultArr: { 105 | fnName: string; 106 | elapsedTimes: number[]; 107 | }[] = []; 108 | 109 | if (singleRoute) { 110 | //pushing rps to an array 111 | Object.keys(testData).forEach((rps) => { 112 | rpsArr.push(rps); 113 | }); 114 | 115 | // pushing function names of the first rps group at routenmae 116 | testData[rpsArr[0]][routeName as string].middlewares.forEach((middlewareObj) => { 117 | resultArr.push({ fnName: middlewareObj.fnName, elapsedTimes: [] }); 118 | }); 119 | 120 | // pushing all the elapsed times for each route from the all rps groups 121 | resultArr.forEach((middlewareObj, i) => { 122 | Object.keys(testData).forEach((rps) => { 123 | middlewareObj.elapsedTimes.push( 124 | testData[rps][routeName as string].middlewares[i].elapsedTime 125 | ); 126 | }); 127 | }); 128 | 129 | for (let i = 0; i < resultArr.length; i++) { 130 | dataSetArray.push({ 131 | type: 'bar', 132 | label: resultArr[i].fnName, 133 | data: resultArr[i].elapsedTimes, 134 | backgroundColor: [shadedColor(i, resultArr.length, darkMode)], 135 | borderWidth: 0, 136 | }); 137 | } 138 | } else { 139 | Object.keys(testData).forEach((rps) => { 140 | rpsArr.push(rps); 141 | // then for each key, pull the route as a label 142 | //add to object as a property 143 | //then grab recieved time and put in an array for value in onj 144 | Object.keys(testData[rps]).forEach((route) => { 145 | if (!resultObj[route]) { 146 | resultObj[route] = { 147 | elapsedTimes: [], 148 | errorCounts: [], 149 | }; 150 | } 151 | resultObj[route].elapsedTimes.push(testData[rps][route].receivedTime as number); 152 | resultObj[route].errorCounts.push( 153 | Math.round( 154 | (100 * (testData[rps][route].errorCount as number)) / 155 | ((testData[rps][route].successfulResCount as number) + 156 | (testData[rps][route].errorCount as number)) 157 | ) 158 | ); 159 | }); 160 | }); 161 | //create a background color array? 162 | //have them select color scheme? or colors per route? 163 | 164 | //loop through the resultObj to create the dataset array on objs per route/ property 165 | Object.keys(resultObj).forEach((route, i) => { 166 | const lineColor = shadedColor(i, Object.keys(resultObj).length, darkMode); 167 | dataSetArray.push({ 168 | type: 'bar', 169 | label: route, 170 | data: resultObj[route].elapsedTimes, 171 | backgroundColor: [lineColor], 172 | borderWidth: 0, 173 | order: 2, 174 | }); 175 | const lineColorRed = shadedColor( 176 | i, 177 | Object.keys(resultObj).length, 178 | darkMode, 179 | [100, 25, 25], 180 | [220, 50, 50] 181 | ); 182 | dataSetArray.push({ 183 | type: 'line', 184 | label: `Error percent for ${route}`, 185 | yAxisID: 'yError', 186 | data: resultObj[route].errorCounts, 187 | backgroundColor: [lineColorRed], 188 | borderColor: lineColorRed, 189 | borderWidth: 4, 190 | fill: false, 191 | order: 1, 192 | }); 193 | }); 194 | } 195 | 196 | // setChartData({ labels: rpsArr, datasets: dataSetArray }); 197 | const chartData: { labels: string[]; datasets: ChartDataSet[] } = { 198 | labels: rpsArr, 199 | datasets: dataSetArray, 200 | }; 201 | 202 | return ; 203 | }; 204 | 205 | export default StackedBar; 206 | -------------------------------------------------------------------------------- /src/client/components/resultsComponents/tables.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import Table from '@material-ui/core/Table'; 4 | import TableBody from '@material-ui/core/TableBody'; 5 | import TableCell from '@material-ui/core/TableCell'; 6 | import TableContainer from '@material-ui/core/TableContainer'; 7 | import TableHead from '@material-ui/core/TableHead'; 8 | import TableRow from '@material-ui/core/TableRow'; 9 | import Paper from '@material-ui/core/Paper'; 10 | 11 | import { PulledDataFromTest } from '../../interfaces'; 12 | 13 | const useStyles = makeStyles({ 14 | table: { 15 | minWidth: 650, 16 | }, 17 | }); 18 | 19 | const DenseTable: (props: { routeData: PulledDataFromTest; routeName?: string }) => JSX.Element = ({ 20 | routeData, 21 | routeName, 22 | }) => { 23 | const classes = useStyles(); 24 | 25 | const rpsArr: string[] = []; 26 | 27 | const resultArr: { 28 | fnName: string; 29 | elapsedTimes: number[]; 30 | }[] = []; 31 | 32 | //pushing rps to an array 33 | Object.keys(routeData).forEach((rps) => { 34 | rpsArr.push(rps); 35 | }); 36 | 37 | // pushing function names of the first rps group at routenmae 38 | routeData[rpsArr[0]][routeName as string].middlewares.forEach((middlewareObj) => { 39 | resultArr.push({ fnName: middlewareObj.fnName, elapsedTimes: [] }); 40 | }); 41 | 42 | // pushing all the elapsed times for each route from the all rps groups 43 | resultArr.forEach((middlewareObj, i) => { 44 | Object.keys(routeData).forEach((rps) => { 45 | middlewareObj.elapsedTimes.push( 46 | routeData[rps][routeName as string].middlewares[i].elapsedTime 47 | ); 48 | }); 49 | }); 50 | const rows: string[][] = []; 51 | const rows2: string[][] = [ 52 | ['Total Response Time'], 53 | ['Successful Response count'], 54 | ['Error count'], 55 | ['Error %'], 56 | ]; 57 | for (const rps of rpsArr) { 58 | rows2[0].push(routeData[rps][routeName as string].receivedTime + ' ms'); 59 | rows2[1].push( 60 | (routeData[rps][routeName as string].successfulResCount as number).toString() 61 | ); 62 | rows2[2].push((routeData[rps][routeName as string].errorCount as number).toString()); 63 | const errorPercent: number = 64 | 100 * 65 | ((routeData[rps][routeName as string].errorCount as number) / 66 | ((routeData[rps][routeName as string].successfulResCount as number) + 67 | (routeData[rps][routeName as string].errorCount as number))); 68 | rows2[3].push(errorPercent.toFixed(2) + '%'); 69 | } 70 | 71 | const rowsHeaders: string[] = ([] = ['Middleware of ' + routeName, ...rpsArr]); 72 | const rowsHeaders2: string[] = ([] = ['', ...rpsArr]); 73 | for (const middlewareData of resultArr) { 74 | rows.push([middlewareData.fnName, ...middlewareData.elapsedTimes.map((e) => e.toString())]); 75 | } 76 | return ( 77 | 78 | 79 | 80 | 81 | {rowsHeaders.map((rps, i) => ( 82 | 83 | {i === 0 ? rps : `RPS - ${rps} `} 84 | 85 | ))} 86 | 87 | 88 | 89 | {rows.map((row, i) => ( 90 | 91 | {row.map((ele, j) => { 92 | return j === 0 ? ( 93 | 94 | {ele} 95 | 96 | ) : ( 97 | {`${ele} ms`} 101 | ); 102 | })} 103 | 104 | ))} 105 | 106 | 107 | 108 | {rowsHeaders2.map((rps, i) => ( 109 | 114 | {i === 0 ? rps : ``} 115 | 116 | ))} 117 | 118 | 119 | 120 | {rows2.map((row, i) => ( 121 | 122 | {row.map((ele, j) => { 123 | return j === 0 ? ( 124 | 125 | {ele} 126 | 127 | ) : ( 128 | {`${ele}`} 132 | ); 133 | })} 134 | 135 | ))} 136 | 137 |
138 |
139 | ); 140 | }; 141 | 142 | export default DenseTable; 143 | -------------------------------------------------------------------------------- /src/client/components/resultsComponents/verticalTabLabels.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Row from 'react-bootstrap/Row'; 4 | import Col from 'react-bootstrap/Col'; 5 | 6 | import DeleteIcon from '@material-ui/icons/Delete'; 7 | import GetAppIcon from '@material-ui/icons/GetApp'; 8 | import { makeStyles } from '@material-ui/core/styles'; 9 | import { useAppDispatch, useAppSelector } from '../../state/hooks'; 10 | import Actions from '../../state/actions/actions'; 11 | 12 | const useStyles = makeStyles(() => ({ 13 | deleteIcon: { 14 | fontSize: '2rem', 15 | '&:hover': { 16 | color: '#c20045', 17 | }, 18 | }, 19 | })); 20 | 21 | const TabLabels: (props: { index: number; time: number }) => JSX.Element = ({ index, time }) => { 22 | const classes = useStyles(); 23 | const dispatch = useAppDispatch(); 24 | const receivedData = useAppSelector((state) => state.receivedData); 25 | 26 | const handleDelete = () => { 27 | dispatch(Actions.DeleteSingleData(index)); 28 | dispatch(Actions.SetResultsTabValue(index)); 29 | }; 30 | 31 | const handleExport = () => { 32 | const element = document.createElement('a'); 33 | element.setAttribute( 34 | 'href', 35 | 'data:application/json;charset=utf-8,' + 36 | encodeURIComponent(JSON.stringify(receivedData[index], null, 4)) 37 | ); 38 | element.setAttribute( 39 | 'download', 40 | `jagtester-export-single-${new Date( 41 | receivedData[index].testTime 42 | ).toLocaleString()}.json` 43 | ); 44 | element.style.display = 'none'; 45 | document.body.appendChild(element); 46 | element.click(); 47 | document.body.removeChild(element); 48 | }; 49 | 50 | return ( 51 | 52 | {new Date(time).toLocaleString()} 53 | 54 | 55 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | export default TabLabels; 66 | -------------------------------------------------------------------------------- /src/client/components/resultsComponents/verticalTabs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles, Theme } from '@material-ui/core/styles'; 3 | import Tabs from '@material-ui/core/Tabs'; 4 | import Box from '@material-ui/core/Box'; 5 | import StackedBar from './graphs'; 6 | import DenseTable from './tables'; 7 | import Container from 'react-bootstrap/Container'; 8 | import Row from 'react-bootstrap/Row'; 9 | import Col from 'react-bootstrap/Col'; 10 | import { useAppSelector, useAppDispatch } from '../../state/hooks'; 11 | import Actions from '../../state/actions/actions'; 12 | import noresults from '../../img/noresults.png'; 13 | 14 | import TabLabels from './verticalTabLabels'; 15 | import Card from '@material-ui/core/Card'; 16 | import CardContent from '@material-ui/core/CardContent'; 17 | import Tab from '@material-ui/core/Tab'; 18 | 19 | interface TabPanelProps { 20 | children?: React.ReactNode; 21 | index: number; 22 | value: number; 23 | } 24 | 25 | function TabPanel(props: TabPanelProps) { 26 | const { children, value, index, ...other } = props; 27 | 28 | return ( 29 | 38 | ); 39 | } 40 | 41 | const useStyles = makeStyles((theme: Theme) => ({ 42 | root: { 43 | flexGrow: 1, 44 | backgroundColor: theme.palette.background.paper, 45 | display: 'flex', 46 | width: '100%', 47 | }, 48 | tabs: { 49 | borderRight: `1px solid ${theme.palette.divider}`, 50 | }, 51 | })); 52 | 53 | const a11yProps = (index: number) => { 54 | return { 55 | id: `vertical-tab-${index}`, 56 | 'aria-controls': `vertical-tabpanel-${index}`, 57 | }; 58 | }; 59 | 60 | const VerticalTabs: () => JSX.Element = () => { 61 | const classes = useStyles(); 62 | const dispatch = useAppDispatch(); 63 | const resultsTabValue = useAppSelector((state) => state.resultsTabValue); 64 | 65 | const handleChange = (event: React.ChangeEvent, newValue: number) => { 66 | dispatch(Actions.SetResultsTabValue(newValue)); 67 | }; 68 | 69 | const receivedData = useAppSelector((state) => state.receivedData); 70 | 71 | //pushing tab data 72 | const tabsArr: JSX.Element[] = []; 73 | const tabPanelsArr: JSX.Element[] = []; 74 | for (let i = 0; i < receivedData.length; i++) { 75 | const singleTest = receivedData[i]; 76 | // tabsArr.push(); 77 | tabsArr.push( 78 | } 80 | key={i} 81 | {...a11yProps(i)} 82 | /> 83 | ); 84 | const routeNames: JSX.Element[] = []; 85 | 86 | Object.keys(singleTest.testData[Object.keys(singleTest.testData)[0]]).forEach( 87 | (routeName, j) => { 88 | routeNames.push( 89 | 90 | 91 | 92 | 97 | 98 | 99 | 100 | 101 | ); 102 | } 103 | ); 104 | tabPanelsArr.push( 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | {routeNames} 114 | 115 | ); 116 | } 117 | return ( 118 | 119 | {receivedData.length !== 0 && ( 120 | 121 | 122 | 130 | {tabsArr} 131 | 132 | 133 | {tabPanelsArr} 134 | 135 | )} 136 | {receivedData.length === 0 && ( 137 | 138 | 139 |

140 | Oops! No results are available! 141 |
142 | Go back and start a test on your server. 143 |

{' '} 144 | 145 | 146 | 154 | 155 | 156 | 157 |
158 | )} 159 |
160 | ); 161 | }; 162 | 163 | export default VerticalTabs; 164 | -------------------------------------------------------------------------------- /src/client/components/testConfigComponents/RangeSliders.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Row from 'react-bootstrap/Row'; 3 | import Col from 'react-bootstrap/Col'; 4 | import Container from 'react-bootstrap/Container'; 5 | 6 | import SingleSlider from './SingleSlider'; 7 | 8 | import { useAppSelector, useAppDispatch } from '../../state/hooks'; 9 | import Actions from '../../state/actions/actions'; 10 | import Typography from '@material-ui/core/Typography'; 11 | 12 | import HighRPSWarning from './highrpswarning'; 13 | 14 | const RangeSliders: () => JSX.Element = () => { 15 | const MAXRPS = 1000, 16 | MAXRPSEND = 20000, 17 | MAXSECONDS = 20; 18 | const valueRPS = useAppSelector((state) => state.valueRPS); 19 | const valueStart = useAppSelector((state) => state.valueStart); 20 | const valueEnd = useAppSelector((state) => state.valueEnd); 21 | const valueSeconds = useAppSelector((state) => state.valueSeconds); 22 | const isTestRunning = useAppSelector((state) => state.isTestRunning); 23 | const curTestTotalPercent = useAppSelector((state) => state.curTestTotalPercent); 24 | const curTestStartTime = useAppSelector((state) => state.curTestStartTime); 25 | const dispatch = useAppDispatch(); 26 | 27 | const handleChangeRPS = (event: unknown, newValue: number | number[]) => { 28 | dispatch(Actions.SetValueRPS(newValue as number)); 29 | dispatch(Actions.SetValueEnd(Math.min(MAXRPSEND, valueStart + 15 * valueRPS))); 30 | dispatch(Actions.SetCurRunningRPS(0)); 31 | }; 32 | const handleChangeSeconds = (event: unknown, newValue: number | number[]) => { 33 | dispatch(Actions.SetValueSeconds(newValue as number)); 34 | }; 35 | const handleChangeStartEnd = (event: unknown, newValue: number | number[]) => { 36 | dispatch(Actions.SetValueStart((newValue as number[])[0])); 37 | dispatch(Actions.SetValueEnd((newValue as number[])[1])); 38 | dispatch(Actions.SetCurRunningRPS(0)); 39 | }; 40 | 41 | const approximateTestTime = (valueSeconds * (valueEnd + valueRPS - valueStart)) / valueRPS; 42 | const curElapsedTime = (Date.now() - curTestStartTime) / 1000; 43 | const estimatedTime = Math.min( 44 | (curElapsedTime * (100 - curTestTotalPercent)) / curTestTotalPercent, 45 | approximateTestTime * 100 46 | ); 47 | 48 | return ( 49 |
50 | 51 | 52 | 53 | 66 | 79 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | {isTestRunning ? ( 101 | 102 | Estimated time remaining: {Math.round(estimatedTime)} seconds 103 | 104 | ) : ( 105 | 106 | Approximate test time: {Math.round(approximateTestTime)} seconds 107 | 108 | )} 109 | 110 | 111 | 112 | 113 |
114 | ); 115 | }; 116 | 117 | export default RangeSliders; 118 | -------------------------------------------------------------------------------- /src/client/components/testConfigComponents/SingleSlider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Typography from '@material-ui/core/Typography'; 3 | import Slider from '@material-ui/core/Slider'; 4 | 5 | import Row from 'react-bootstrap/Row'; 6 | import Col from 'react-bootstrap/Col'; 7 | 8 | import { useAppSelector } from '../../state/hooks'; 9 | 10 | const SingleSlider: (props: { 11 | text: string; 12 | id: string; 13 | key: string; 14 | value: number | number[]; 15 | onChange: (event: unknown, newValue: number | number[]) => void; 16 | min: number; 17 | max: number; 18 | step: number; 19 | marks?: { 20 | interval: number; 21 | min: number; 22 | max: number; 23 | }; 24 | disabled?: boolean; 25 | extraLabel?: string; 26 | extraLabelDanger?: string | undefined; 27 | }) => JSX.Element = (props) => { 28 | const darkMode = useAppSelector((state) => state.darkMode); 29 | // generates marks for the sliders 30 | const marks = (interval: number, min: number, max: number) => { 31 | const marksArr: { 32 | value: number; 33 | label: string; 34 | }[] = []; 35 | 36 | // making sure it doesnt push the same value, which will cause react same key error 37 | if (min !== interval) { 38 | marksArr.push({ value: min, label: min.toString() }); 39 | } 40 | for (let i = interval; i <= max; i += interval) { 41 | marksArr.push({ value: i, label: i.toString() }); 42 | } 43 | return marksArr; 44 | }; 45 | 46 | function valuetext(value: number) { 47 | return `${value}`; 48 | } 49 | 50 | return ( 51 | <> 52 | 53 | 54 | {props.text} 55 | {props.extraLabel} 56 | 57 | 58 | 77 | 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default SingleSlider; 84 | -------------------------------------------------------------------------------- /src/client/components/testConfigComponents/TargetInputs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Container from 'react-bootstrap/Container'; 3 | 4 | import TargetInputSingle, { TargetInputDisabled } from './TargetInputSingle'; 5 | 6 | import { useAppSelector } from '../../state/hooks'; 7 | //---------------------------- will render single targets based on the state passed down from the parent 8 | const TartgetInputs: () => JSX.Element = () => { 9 | const inputsData = useAppSelector((state) => state.inputsData); 10 | 11 | //---------------------------- inputting all inputs into an array 12 | const inputsArr: JSX.Element[] = []; 13 | for (let i = 0; i < inputsData.length; i++) { 14 | inputsArr.push(); 15 | } 16 | // pushing a disabled input 17 | inputsArr.push(); 18 | 19 | return {inputsArr}; 20 | }; 21 | 22 | export default TartgetInputs; 23 | -------------------------------------------------------------------------------- /src/client/components/testConfigComponents/buttonStopSpinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Backdrop from '@material-ui/core/Backdrop'; 3 | import CircularProgress from '@material-ui/core/CircularProgress'; 4 | import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; 5 | 6 | import { useAppSelector, useAppDispatch } from '../../state/hooks'; 7 | import Actions from '../../state/actions/actions'; 8 | const useStyles = makeStyles((theme: Theme) => 9 | createStyles({ 10 | backdrop: { 11 | zIndex: theme.zIndex.drawer + 1, 12 | color: '#fff', 13 | }, 14 | }) 15 | ); 16 | 17 | const SimpleBackdrop: () => JSX.Element = () => { 18 | const dispatch = useAppDispatch(); 19 | const isSpinning = useAppSelector((state) => state.stoppingSpinner); 20 | const classes = useStyles(); 21 | 22 | return ( 23 |
24 | dispatch(Actions.SetStoppingSpinner(false))} 28 | > 29 | 30 | 31 |
32 | ); 33 | }; 34 | 35 | export default SimpleBackdrop; 36 | -------------------------------------------------------------------------------- /src/client/components/testConfigComponents/buttonsstartstop.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Container from 'react-bootstrap/Container'; 3 | import Row from 'react-bootstrap/Row'; 4 | import Col from 'react-bootstrap/Col'; 5 | import Button from 'react-bootstrap/Button'; 6 | 7 | import { useAppSelector, useAppDispatch } from '../../state/hooks'; 8 | import Actions from '../../state/actions/actions'; 9 | 10 | import { HTTPMethods, TestConfigData } from '../../interfaces'; 11 | import StopButtonSpinner from './buttonStopSpinner'; 12 | 13 | const Buttons: () => JSX.Element = () => { 14 | const isTestRunning = useAppSelector((state) => state.isTestRunning); 15 | const valueRPS = useAppSelector((state) => state.valueRPS); 16 | const valueStart = useAppSelector((state) => state.valueStart); 17 | const valueEnd = useAppSelector((state) => state.valueEnd); 18 | const valueSeconds = useAppSelector((state) => state.valueSeconds); 19 | const inputsData = useAppSelector((state) => state.inputsData); 20 | const dispatch = useAppDispatch(); 21 | 22 | const jagEndabledInputs = inputsData.some((target) => !target.jagTesterEnabled); 23 | 24 | const handleStopTest = () => { 25 | dispatch(Actions.SetStoppingSpinner(true)); 26 | fetch('/api/stopTest') 27 | .then(() => { 28 | dispatch(Actions.SetStoppingSpinner(false)); 29 | }) 30 | .catch((err) => { 31 | dispatch(Actions.SetShowModal(true)); 32 | dispatch(Actions.SetModalError(err.toString())); 33 | }); 34 | }; 35 | 36 | const handleStartTest = () => { 37 | dispatch(Actions.SetCurRunningRPS(0)); 38 | dispatch(Actions.SetCurTestStartTime(Date.now())); 39 | const testConfigObj: TestConfigData = { 40 | rpsInterval: valueRPS, 41 | startRPS: valueStart, 42 | endRPS: valueEnd, 43 | testLength: valueSeconds, 44 | inputsData, 45 | }; 46 | fetch('/api/startmultiple', { 47 | method: HTTPMethods.POST, 48 | headers: new Headers({ 'Content-Type': 'application/json' }), 49 | body: JSON.stringify(testConfigObj), 50 | }).catch((err) => { 51 | dispatch(Actions.SetShowModal(true)); 52 | dispatch(Actions.SetModalError(err.toString())); 53 | }); 54 | }; 55 | 56 | return ( 57 | 58 | 59 | 60 | 67 | 68 | 69 | 77 | 78 | 79 | 86 | 87 | 88 | 89 | 90 | ); 91 | }; 92 | 93 | export default Buttons; 94 | -------------------------------------------------------------------------------- /src/client/components/testConfigComponents/highrpswarning.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Snackbar from '@material-ui/core/Snackbar'; 3 | import MuiAlert, { AlertProps } from '@material-ui/lab/Alert'; 4 | import { makeStyles, Theme } from '@material-ui/core/styles'; 5 | 6 | import { useAppSelector, useAppDispatch } from '../../state/hooks'; 7 | import Actions from '../../state/actions/actions'; 8 | 9 | function Alert(props: AlertProps) { 10 | return ; 11 | } 12 | 13 | const useStyles = makeStyles((theme: Theme) => ({ 14 | root: { 15 | width: '100%', 16 | '& > * + *': { 17 | marginTop: theme.spacing(2), 18 | }, 19 | }, 20 | })); 21 | 22 | const CustomizedSnackbars: () => JSX.Element = () => { 23 | const highRPSwarning = useAppSelector((state) => state.highRPSwarning); 24 | const dispatch = useAppDispatch(); 25 | const classes = useStyles(); 26 | 27 | const handleClose = (event?: React.SyntheticEvent, reason?: string) => { 28 | if (reason === 'clickaway') { 29 | return; 30 | } 31 | 32 | dispatch(Actions.SetHighRPSwarning(false)); 33 | }; 34 | 35 | return ( 36 |
37 | 38 | 39 | Jagtester may experience slowdowns when testing higher RPS. Results will still 40 | be accurate, but the test might take longer than expected. In case the Jagtester 41 | server freezes, stop the server and reset with the button below. 42 | 43 | 44 |
45 | ); 46 | }; 47 | 48 | export default CustomizedSnackbars; 49 | -------------------------------------------------------------------------------- /src/client/components/testConfigComponents/testProgress.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Row from 'react-bootstrap/Row'; 3 | import Col from 'react-bootstrap/Col'; 4 | import Container from 'react-bootstrap/Container'; 5 | 6 | import { useAppSelector } from '../../state/hooks'; 7 | 8 | import LinearProgress from '@material-ui/core/LinearProgress'; 9 | import { makeStyles, createStyles, withStyles } from '@material-ui/core/styles'; 10 | 11 | const BorderLinearProgress = withStyles(() => 12 | createStyles({ 13 | root: { 14 | height: 10, 15 | width: '100%', 16 | }, 17 | colorPrimary: { 18 | backgroundColor: '#3D405B', 19 | }, 20 | bar: { 21 | backgroundColor: '#3D405B', 22 | }, 23 | }) 24 | )(LinearProgress); 25 | 26 | const useStyles = makeStyles({ 27 | root: { 28 | flexGrow: 1, 29 | }, 30 | }); 31 | const TestProgrss: () => JSX.Element = () => { 32 | const classes = useStyles(); 33 | const isTestRunning = useAppSelector((state) => state.isTestRunning); 34 | const curTestTotalPercent = useAppSelector((state) => state.curTestTotalPercent); 35 | 36 | return ( 37 | 38 | 39 | 40 |
41 | 46 |
47 | 48 |
49 |
50 | ); 51 | }; 52 | 53 | export default TestProgrss; 54 | -------------------------------------------------------------------------------- /src/client/img/noresults.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/jagtester/6b18bb13f6b0d851a83648b007993c70b1411e87/src/client/img/noresults.png -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Jagtester 8 | 14 | 15 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { Provider } from 'react-redux'; 6 | import store from './state/store'; 7 | 8 | import { PersistGate } from 'redux-persist/integration/react'; 9 | import { persistStore } from 'redux-persist'; 10 | const persistor = persistStore(store); 11 | 12 | ReactDOM.render( 13 | 14 | {process.env.JAG === 'demo' ? ( 15 | 16 | 17 | 18 | ) : ( 19 | 20 | 21 | 22 | 23 | 24 | )} 25 | , 26 | document.getElementById('root') 27 | ); 28 | -------------------------------------------------------------------------------- /src/client/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface TimeArrInterface { 2 | receivedTotalTime: number; 3 | recordedTotalTime: number; 4 | } 5 | 6 | export interface CollectedData { 7 | [key: string]: { 8 | reqId: string; 9 | reqRoute: string; 10 | middlewares: { 11 | fnName: string; 12 | elapsedTime: number; 13 | }[]; 14 | }; 15 | } 16 | 17 | export interface CollectedDataSingle { 18 | receivedTime?: number; 19 | recordedTime?: number; 20 | errorCount?: number; 21 | requestCount?: number; 22 | successfulResCount?: number; 23 | RPS?: number; 24 | reqId?: string; 25 | reqRoute: string; 26 | middlewares: { 27 | fnName: string; 28 | elapsedTime: number; 29 | }[]; 30 | } 31 | 32 | export enum Jagtestercommands { 33 | updateLayer, 34 | running, 35 | endTest, 36 | } 37 | 38 | export enum ioSocketCommands { 39 | testRunningStateChange = 'testRunningStateChange', 40 | singleRPSfinished = 'singleRPSfinished', 41 | allRPSfinished = 'allRPSfinished', 42 | errorInfo = 'errorInfo', 43 | currentRPSProgress = 'currentRPSProgress', 44 | } 45 | 46 | export interface TestConfigData { 47 | rpsInterval: number; 48 | startRPS: number; 49 | endRPS: number; 50 | testLength: number; 51 | inputsData: { 52 | method: string; 53 | targetURL: string; 54 | percentage: number; 55 | jagTesterEnabled: boolean; 56 | }[]; 57 | } 58 | 59 | export interface PulledDataFromTest { 60 | [key: string]: { 61 | [key: string]: CollectedDataSingle; 62 | }; 63 | } 64 | 65 | export interface AllPulledDataFromTest { 66 | testTime: number; 67 | testData: PulledDataFromTest; 68 | } 69 | 70 | export enum HTTPMethods { 71 | GET = 'GET', 72 | POST = 'POST', 73 | PUT = 'PUT', 74 | DELETE = 'DELETE', 75 | PATCH = 'PATCH', 76 | HEAD = 'HEAD', 77 | CONNECT = 'CONNECT', 78 | TRACE = 'TRACE', 79 | } 80 | 81 | export interface ChartDataSet { 82 | type: string; 83 | label: string; 84 | data: number[]; 85 | backgroundColor: string[]; 86 | borderWidth: number; 87 | borderColor?: string; 88 | fill?: boolean; 89 | yAxisID?: string; 90 | order?: number; 91 | } 92 | -------------------------------------------------------------------------------- /src/client/pages/results.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Container from 'react-bootstrap/Container'; 3 | import Row from 'react-bootstrap/Row'; 4 | import Col from 'react-bootstrap/Col'; 5 | import Button from 'react-bootstrap/Button'; 6 | import ButtonGroup from 'react-bootstrap/ButtonGroup'; 7 | import VerticalTabs from '../components/resultsComponents/verticalTabs'; 8 | import { useAppSelector, useAppDispatch } from '../state/hooks'; 9 | import Actions from '../state/actions/actions'; 10 | 11 | const ResultsPage: () => JSX.Element = () => { 12 | const receivedData = useAppSelector((state) => state.receivedData); 13 | const dispatch = useAppDispatch(); 14 | 15 | //Export JSON function 16 | const download = () => { 17 | const element = document.createElement('a'); 18 | element.setAttribute( 19 | 'href', 20 | 'data:application/json;charset=utf-8,' + 21 | encodeURIComponent(JSON.stringify(receivedData, null, 4)) 22 | ); 23 | element.setAttribute('download', `jagtester-exportall-${new Date().toLocaleString()}.json`); 24 | element.style.display = 'none'; 25 | document.body.appendChild(element); 26 | element.click(); 27 | document.body.removeChild(element); 28 | }; 29 | 30 | const deleteAllData = () => { 31 | dispatch(Actions.DeleteReceivedData()); 32 | dispatch(Actions.SetResultsTabValue(0)); 33 | }; 34 | 35 | return ( 36 | 37 | {receivedData.length > 0 && ( 38 | 39 | 40 | 41 | 44 | 47 | 48 | 49 | 50 | )} 51 | 52 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default ResultsPage; 59 | -------------------------------------------------------------------------------- /src/client/pages/testconfigpage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Row from 'react-bootstrap/Row'; 3 | import Col from 'react-bootstrap/Col'; 4 | 5 | import Buttons from '../components/testConfigComponents/buttonsstartstop'; 6 | import TargetInputs from '../components/testConfigComponents/TargetInputs'; 7 | import RangeSliders from '../components/testConfigComponents/RangeSliders'; 8 | 9 | //MUI imports 10 | import Card from '@material-ui/core/Card'; 11 | import CardContent from '@material-ui/core/CardContent'; 12 | import Typography from '@material-ui/core/Typography'; 13 | 14 | const TestPage: () => JSX.Element = () => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {process.env.JAG === 'demo' && ( 30 | 31 | Warning! Currently running in demo mode and you cannot start any tests. You can look around the app and check out our features. Take a look at the results page and see what kinds of 32 | data we are reporting. To use it, simply download from npm and use it locally. 33 | 34 | )} 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default TestPage; 43 | -------------------------------------------------------------------------------- /src/client/state/actions/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit'; 2 | 3 | import { HTTPMethods, AllPulledDataFromTest } from '../../interfaces'; 4 | 5 | enum ActionTypes { 6 | setValueRPS = 'setValueRPS', 7 | setValueStart = 'setValueStart', 8 | setValueEnd = 'setValueEnd', 9 | setValueSeconds = 'setValueSeconds', 10 | setIsTestRunning = 'setIsTestRunning', 11 | setCurRunningRPS = 'setCurRunningRPS', 12 | 13 | changeTargetMethod = 'changeTargetMethod', 14 | changeTargetURL = 'changeTargetURL', 15 | changeTargetJagEnabled = 'changeTargetJagEnabled', 16 | changeTargetPercent = 'changeTargetPercent', 17 | addTarget = 'addTarget', 18 | deleteTarget = 'deleteTarget', 19 | 20 | setReceivedData = 'setRedeceivedData', 21 | deleteReceivedData = 'deleteReceivedData', 22 | 23 | setShowModal = 'setShowModal', 24 | setModalError = 'setModalError', 25 | 26 | deleteSingleData = 'deleteSingleData', 27 | setResultsTabValue = 'setResultsTabValue', 28 | setCurRPSpercent = 'setCurRPSpercent', 29 | 30 | setCurTestStartTime = 'setCurTestStartTime', 31 | setDarkMode = 'setDarkMode', 32 | resetState = 'resetState', 33 | setHighRPSwarning = 'setHighRPSwarning', 34 | setStoppingSpinner = 'setStoppingSpinner', 35 | } 36 | 37 | const SetValueRPS = createAction(ActionTypes.setValueRPS); 38 | const SetValueStart = createAction(ActionTypes.setValueStart); 39 | const SetValueEnd = createAction(ActionTypes.setValueEnd); 40 | const SetValueSeconds = createAction(ActionTypes.setValueSeconds); 41 | const SetIsTestRunning = createAction(ActionTypes.setIsTestRunning); 42 | const SetCurRunningRPS = createAction(ActionTypes.setCurRunningRPS); 43 | const ChangeTargetMethod = createAction<{ index: number; method: HTTPMethods }>( 44 | ActionTypes.changeTargetMethod 45 | ); 46 | const ChangeTargetURL = createAction<{ index: number; newURL: string }>( 47 | ActionTypes.changeTargetURL 48 | ); 49 | const ChangeTargetJagEnabled = createAction<{ index: number; isEnabled: boolean }>( 50 | ActionTypes.changeTargetJagEnabled 51 | ); 52 | const ChangeTargetPercent = createAction<{ index: number; newValue: number }>( 53 | ActionTypes.changeTargetPercent 54 | ); 55 | const AddTarget = createAction(ActionTypes.addTarget); 56 | const DeleteTarget = createAction(ActionTypes.deleteTarget); 57 | 58 | const SetReceivedData = createAction(ActionTypes.setReceivedData); 59 | const DeleteReceivedData = createAction(ActionTypes.deleteReceivedData); 60 | 61 | const SetShowModal = createAction(ActionTypes.setShowModal); 62 | const SetModalError = createAction(ActionTypes.setModalError); 63 | const DeleteSingleData = createAction(ActionTypes.deleteSingleData); 64 | const SetResultsTabValue = createAction(ActionTypes.setResultsTabValue); 65 | const SetCurRPSpercent = createAction(ActionTypes.setCurRPSpercent); 66 | const SetCurTestStartTime = createAction(ActionTypes.setCurTestStartTime); 67 | const SetDarkMode = createAction(ActionTypes.setDarkMode); 68 | const ResetState = createAction(ActionTypes.resetState); 69 | const SetHighRPSwarning = createAction(ActionTypes.setHighRPSwarning); 70 | const SetStoppingSpinner = createAction(ActionTypes.setStoppingSpinner); 71 | 72 | const Actions = { 73 | SetValueRPS, 74 | SetValueStart, 75 | SetValueEnd, 76 | SetValueSeconds, 77 | SetIsTestRunning, 78 | SetCurRunningRPS, 79 | ChangeTargetMethod, 80 | ChangeTargetURL, 81 | ChangeTargetJagEnabled, 82 | ChangeTargetPercent, 83 | AddTarget, 84 | DeleteTarget, 85 | SetReceivedData, 86 | SetShowModal, 87 | SetModalError, 88 | DeleteSingleData, 89 | SetResultsTabValue, 90 | SetCurRPSpercent, 91 | SetCurTestStartTime, 92 | SetDarkMode, 93 | ResetState, 94 | DeleteReceivedData, 95 | SetHighRPSwarning, 96 | SetStoppingSpinner, 97 | }; 98 | 99 | export default Actions; 100 | -------------------------------------------------------------------------------- /src/client/state/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import type { RootState, AppDispatch } from './store'; 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch: () => ReturnType = () => 6 | useDispatch(); 7 | export const useAppSelector: TypedUseSelectorHook = useSelector; 8 | -------------------------------------------------------------------------------- /src/client/state/reducers/configReducer.ts: -------------------------------------------------------------------------------- 1 | import Actions from '../actions/actions'; 2 | import { createReducer } from '@reduxjs/toolkit'; 3 | 4 | import { HTTPMethods } from '../../interfaces'; 5 | 6 | import { initialState } from './initialState'; 7 | 8 | const calculateTotalTestPercent = ( 9 | valueRPS: number, 10 | valueStart: number, 11 | valueEnd: number, 12 | curRunningRPS: number, 13 | curRPSpercent: number 14 | ) => { 15 | const range = (valueEnd - valueStart) / valueRPS; 16 | 17 | return curRunningRPS === 0 18 | ? Math.round((100 * curRPSpercent) / (range + 1)) 19 | : Math.round( 20 | (100 * ((curRunningRPS - valueStart) / valueRPS + 1 + curRPSpercent)) / (range + 1) 21 | ); 22 | }; 23 | 24 | const configReducer = createReducer(initialState, (builder) => { 25 | builder 26 | .addCase(Actions.SetValueRPS, (state, action) => { 27 | state.valueRPS = action.payload; 28 | }) 29 | .addCase(Actions.SetValueStart, (state, action) => { 30 | state.valueStart = action.payload; 31 | }) 32 | .addCase(Actions.SetValueEnd, (state, action) => { 33 | if (state.valueEnd < 10000 && action.payload > 10000) { 34 | state.highRPSwarning = true; 35 | } 36 | state.valueEnd = action.payload; 37 | }) 38 | .addCase(Actions.SetValueSeconds, (state, action) => { 39 | state.valueSeconds = action.payload; 40 | }) 41 | .addCase(Actions.SetIsTestRunning, (state, action) => { 42 | state.isTestRunning = action.payload; 43 | }) 44 | .addCase(Actions.SetCurRunningRPS, (state, action) => { 45 | state.curRunningRPS = action.payload; 46 | state.curTestTotalPercent = calculateTotalTestPercent( 47 | state.valueRPS, 48 | state.valueStart, 49 | state.valueEnd, 50 | state.curRunningRPS, 51 | state.curRPSpercent 52 | ); 53 | }) 54 | .addCase(Actions.ChangeTargetMethod, (state, action) => { 55 | state.inputsData[action.payload.index].method = action.payload.method; 56 | }) 57 | .addCase(Actions.ChangeTargetURL, (state, action) => { 58 | state.inputsData[action.payload.index].targetURL = action.payload.newURL; 59 | }) 60 | .addCase(Actions.ChangeTargetJagEnabled, (state, action) => { 61 | state.inputsData[action.payload.index].jagTesterEnabled = action.payload.isEnabled; 62 | }) 63 | .addCase(Actions.ChangeTargetPercent, (state, action) => { 64 | let index = action.payload.index; 65 | const newValue = action.payload.newValue; 66 | let diffWithNext = state.inputsData[index].percentage - newValue; 67 | const diffWithNextCopy = diffWithNext; 68 | while (diffWithNext !== 0) { 69 | index = index < state.inputsData.length - 1 ? index + 1 : 0; 70 | 71 | if (state.inputsData[index].percentage + diffWithNext > 100) { 72 | diffWithNext = diffWithNext - (100 - state.inputsData[index].percentage); 73 | state.inputsData[index].percentage = 100; 74 | } else if (state.inputsData[index].percentage + diffWithNext < 0) { 75 | diffWithNext = state.inputsData[index].percentage + diffWithNext; 76 | state.inputsData[index].percentage = 0; 77 | } else { 78 | state.inputsData[index].percentage = 79 | state.inputsData[index].percentage + diffWithNext; 80 | break; 81 | } 82 | } 83 | state.inputsData[action.payload.index].percentage -= diffWithNextCopy; 84 | }) 85 | .addCase(Actions.AddTarget, (state) => { 86 | state.inputsData.push({ 87 | method: HTTPMethods.GET, 88 | targetURL: '', 89 | percentage: 0, 90 | jagTesterEnabled: false, 91 | }); 92 | const percentagePerInput = Math.floor(100 / state.inputsData.length); 93 | for (let i = 0; i < state.inputsData.length; i++) { 94 | state.inputsData[i].percentage = percentagePerInput; 95 | } 96 | if (state.inputsData.length > 0) 97 | state.inputsData[0].percentage += 98 | 100 - state.inputsData.length * percentagePerInput; 99 | }) 100 | .addCase(Actions.DeleteTarget, (state, action) => { 101 | state.inputsData.splice(action.payload, 1); 102 | }) 103 | .addCase(Actions.SetReceivedData, (state, action) => { 104 | state.receivedData.push(...action.payload); 105 | }) 106 | .addCase(Actions.DeleteReceivedData, (state) => { 107 | state.receivedData = []; 108 | }) 109 | .addCase(Actions.SetShowModal, (s, a) => { 110 | s.showModal = a.payload; 111 | }) 112 | .addCase(Actions.SetModalError, (state, action) => { 113 | state.modalError = action.payload; 114 | }) 115 | .addCase(Actions.DeleteSingleData, (state, action) => { 116 | state.receivedData.splice(action.payload, 1); 117 | }) 118 | .addCase(Actions.SetResultsTabValue, (state, action) => { 119 | state.resultsTabValue = Math.max( 120 | Math.min(action.payload, state.receivedData.length - 1), 121 | 0 122 | ); 123 | }) 124 | .addCase(Actions.SetCurRPSpercent, (state, action) => { 125 | state.curRPSpercent = action.payload; 126 | state.curTestTotalPercent = calculateTotalTestPercent( 127 | state.valueRPS, 128 | state.valueStart, 129 | state.valueEnd, 130 | state.curRunningRPS, 131 | state.curRPSpercent 132 | ); 133 | }) 134 | .addCase(Actions.SetCurTestStartTime, (state, action) => { 135 | state.curTestStartTime = action.payload; 136 | }) 137 | .addCase(Actions.SetDarkMode, (state, action) => { 138 | state.darkMode = action.payload; 139 | }) 140 | .addCase(Actions.ResetState, (state) => { 141 | state.valueRPS = initialState.valueRPS; 142 | state.valueStart = initialState.valueStart; 143 | state.valueEnd = initialState.valueEnd; 144 | state.valueSeconds = initialState.valueSeconds; 145 | state.isTestRunning = initialState.isTestRunning; 146 | state.curRunningRPS = initialState.curRunningRPS; 147 | state.showModal = initialState.showModal; 148 | state.modalError = initialState.modalError; 149 | state.resultsTabValue = initialState.resultsTabValue; 150 | state.curRPSpercent = initialState.curRPSpercent; 151 | state.curTestTotalPercent = initialState.curTestTotalPercent; 152 | state.curTestStartTime = initialState.curTestStartTime; 153 | state.highRPSwarning = initialState.highRPSwarning; 154 | state.stoppingSpinner = initialState.stoppingSpinner; 155 | }) 156 | .addCase(Actions.SetHighRPSwarning, (state, action) => { 157 | state.highRPSwarning = action.payload; 158 | }) 159 | .addCase(Actions.SetStoppingSpinner, (state, action) => { 160 | state.stoppingSpinner = action.payload; 161 | }); 162 | }); 163 | 164 | export default configReducer; 165 | -------------------------------------------------------------------------------- /src/client/state/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import configReducer from './reducers/configReducer'; 3 | 4 | import storage from 'redux-persist/lib/storage'; 5 | import { persistReducer } from 'redux-persist'; 6 | 7 | const persistConfig = { 8 | key: 'root', 9 | storage, 10 | }; 11 | 12 | const persistedReducer = persistReducer(persistConfig, configReducer); 13 | 14 | const store = configureStore({ 15 | reducer: persistedReducer, 16 | devTools: process.env.NODE_ENV !== 'production', 17 | }); 18 | 19 | const storeNoPersist = configureStore({ 20 | reducer: configReducer, 21 | devTools: process.env.NODE_ENV !== 'production', 22 | }); 23 | 24 | export type RootState = ReturnType; 25 | export type AppDispatch = typeof store.dispatch; 26 | export default process.env.JAG === 'demo' ? storeNoPersist : store; 27 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "lib": ["dom", "dom.iterable", "esnext", "webworker", "es6", "scripthost"], 5 | "allowJs": true, 6 | "outDir": "../../dist/client", 7 | "allowSyntheticDefaultImports": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | "paths": { 18 | "*": ["../../node_modules/*"] 19 | } 20 | }, 21 | "include": ["./**/*", "../custom.d.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: string; 3 | export default content; 4 | } 5 | declare module '*.png' { 6 | const content: string; 7 | export default content; 8 | } 9 | -------------------------------------------------------------------------------- /src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction, Application } from 'express'; 2 | import responseTime from 'response-time'; 3 | 4 | type FunctionType = ( 5 | app: Application 6 | ) => (req: Request, res: Response, next: NextFunction) => unknown; 7 | 8 | interface CollectedData { 9 | [key: string]: { 10 | reqId: string; 11 | reqRoute: string; 12 | middlewares: { 13 | fnName: string; 14 | elapsedTime: number; 15 | }[]; 16 | }; 17 | } 18 | 19 | interface RouteData { 20 | [key: string]: CollectedData; 21 | } 22 | 23 | enum Jagtestercommands { 24 | updateLayer, 25 | running, 26 | endTest, 27 | } 28 | 29 | const getMiddleware: FunctionType = (app: Application) => { 30 | let collectedData: CollectedData = {}; 31 | let routeData: RouteData = {}; 32 | let isPrototypeChanged = false; 33 | 34 | const resetLayerPrototype = () => { 35 | app._router.stack[0].__proto__.handle_request = originalLayerHandleRequest; 36 | isPrototypeChanged = false; 37 | collectedData = {}; 38 | routeData = {}; 39 | }; 40 | 41 | const updateLayerPrototype = () => { 42 | app._router.stack[0].__proto__.handle_request = newLayerHandleRequest; 43 | isPrototypeChanged = true; 44 | collectedData = {}; 45 | routeData = {}; 46 | }; 47 | 48 | const originalLayerHandleRequest = function handle( 49 | req: Request, 50 | res: Response, 51 | next: NextFunction 52 | ) { 53 | const fn = this.handle; 54 | 55 | if (fn.length > 3) { 56 | // not a standard request handler 57 | return next(); 58 | } 59 | 60 | try { 61 | fn(req, res, next); 62 | } catch (err) { 63 | next(err); 64 | } 65 | }; 66 | 67 | const newLayerHandleRequest = function handle(req: Request, res: Response, next: NextFunction) { 68 | const fn = this.handle; 69 | 70 | if (fn.length > 3) { 71 | // not a standard request handler 72 | return next(); 73 | } 74 | 75 | try { 76 | const fnName = this.name, 77 | reqId = req.headers.jagtesterreqid 78 | ? req.headers.jagtesterreqid.toString() 79 | : undefined, 80 | reqRoute = req.originalUrl; 81 | 82 | // create a data object in the collected data if it doesnt already exist 83 | if (!routeData[reqRoute]) { 84 | const newCollectedData: CollectedData = {}; 85 | newCollectedData[reqId] = { 86 | reqId, 87 | reqRoute, 88 | middlewares: [], 89 | }; 90 | routeData[reqRoute] = newCollectedData; 91 | } else { 92 | // create a data object in the collected data if it doesnt already exist 93 | if (reqId && !routeData[reqRoute][reqId]) { 94 | routeData[reqRoute][reqId] = { 95 | reqId, 96 | reqRoute, 97 | middlewares: [], 98 | }; 99 | } 100 | } 101 | 102 | // create a data object in the collected data if it doesnt already exist 103 | if (reqId && !collectedData[reqId]) { 104 | collectedData[reqId] = { 105 | reqId, 106 | reqRoute, 107 | middlewares: [], 108 | }; 109 | } 110 | 111 | if (reqId) { 112 | // add layer information to the collectedData 113 | routeData[reqRoute][reqId].middlewares.push({ 114 | fnName, 115 | elapsedTime: 0, 116 | }); 117 | 118 | collectedData[reqId].middlewares.push({ 119 | fnName, 120 | elapsedTime: 0, 121 | }); 122 | } 123 | 124 | // call the middleware and time it in the next function 125 | const beforeFunctionCall = Date.now(); 126 | fn(req, res, function () { 127 | if (reqId && routeData[reqRoute] && routeData[reqRoute][reqId]) { 128 | const lastElIndex = routeData[reqRoute][reqId].middlewares.length - 1; 129 | routeData[reqRoute][reqId].middlewares[lastElIndex].elapsedTime = 130 | Date.now() - beforeFunctionCall; 131 | } 132 | next(); 133 | }); 134 | } catch (err) { 135 | next(err); 136 | } 137 | }; 138 | 139 | // this is the actual middleware that will take jagtestercommands 140 | return (req: Request, res: Response, next: NextFunction) => { 141 | // getting the command 142 | const jagtestercommand = +req.headers.jagtestercommand; 143 | 144 | switch (jagtestercommand) { 145 | //changing the prototype of the layer handle request while running 146 | case Jagtestercommands.running: 147 | if (!isPrototypeChanged) { 148 | updateLayerPrototype(); 149 | } 150 | // res.header({ jagtesterRoute: req.url }); 151 | break; 152 | 153 | //changing the prototype of the layer handle request 154 | case Jagtestercommands.updateLayer: 155 | updateLayerPrototype(); 156 | // res.header({ jagtesterRoute: req.url }); 157 | return res.json({ jagtester: true }); 158 | 159 | //reset the prototype and send back json data 160 | case Jagtestercommands.endTest: 161 | res.json(routeData); 162 | resetLayerPrototype(); 163 | return; 164 | 165 | default: 166 | // changing layer prototype back to original 167 | if (isPrototypeChanged) { 168 | resetLayerPrototype(); 169 | } 170 | break; 171 | } 172 | return responseTime({ suffix: false })(req, res, next); 173 | }; 174 | }; 175 | export default getMiddleware; 176 | module.exports = getMiddleware; 177 | -------------------------------------------------------------------------------- /src/middleware/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "target": "ES5", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "sourceMap": false, 11 | "outDir": "../../dist/middleware", 12 | "baseUrl": ".", 13 | "paths": { 14 | "*": ["../../node_modules/*"] 15 | } 16 | }, 17 | "files": ["./index.ts"], 18 | "exclude": ["../../node_modules/*"] 19 | } 20 | -------------------------------------------------------------------------------- /src/server/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import testRouter from './testrouter'; 4 | 5 | const app = express(); 6 | 7 | app.use(express.json()); 8 | 9 | app.use(express.urlencoded({ extended: false })); 10 | 11 | app.use('/', express.static(path.join(__dirname, '../client'))); 12 | 13 | app.use('/api', testRouter); 14 | 15 | app.get(['/', '/results'], (req, res) => { 16 | res.sendFile(path.join(__dirname, '../client/index.html')); 17 | }); 18 | 19 | app.use('/*', (req, res) => { 20 | res.redirect('/'); 21 | }); 22 | 23 | export { app }; 24 | -------------------------------------------------------------------------------- /src/server/helpers/allRPSfinished.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { Server } from 'socket.io'; 3 | import AbortController from 'abort-controller'; 4 | import { 5 | ioSocketCommands, 6 | Jagtestercommands, 7 | CollectedData, 8 | TestConfigData, 9 | GlobalVariables, 10 | } from '../interfaces'; 11 | 12 | import processData from './processData'; 13 | import processLastMiddleware from './processLastMiddleware'; 14 | 15 | type AllRPSfinished = ( 16 | globalTestConfig: TestConfigData, 17 | io: Server, 18 | globalVariables: GlobalVariables 19 | ) => void; 20 | 21 | const allRPSfinished: AllRPSfinished = ( 22 | globalTestConfig: TestConfigData, 23 | io: Server, 24 | globalVariables: GlobalVariables 25 | ) => { 26 | fetch(globalTestConfig.inputsData[0].targetURL, { 27 | headers: { 28 | jagtestercommand: Jagtestercommands.endTest.toString(), 29 | }, 30 | }).catch((err) => { 31 | io.emit(ioSocketCommands.errorInfo, err.toString()); 32 | }); 33 | globalVariables.abortController = new AbortController(); 34 | globalVariables.isTestRunning = false; 35 | 36 | // clear timeouts 37 | for (const timeout of globalVariables.timeOutArray) { 38 | clearTimeout(timeout); 39 | } 40 | globalVariables.timeOutArray.splice(0, globalVariables.timeOutArray.length); 41 | 42 | // getting the average response time, since we had the total response times added together 43 | for (const route in globalVariables.timeArrRoutes) { 44 | for (const rpsGroup in globalVariables.timeArrRoutes[route]) { 45 | globalVariables.timeArrRoutes[route][rpsGroup].receivedTotalTime = 46 | Math.round( 47 | (1000 * globalVariables.timeArrRoutes[route][rpsGroup].receivedTotalTime) / 48 | globalVariables.timeArrRoutes[route][rpsGroup].successfulResCount 49 | ) / 1000; 50 | } 51 | } 52 | 53 | // processing middlewares, averaging them, then combining timearrroutes 54 | for (const rps in globalVariables.pulledDataFromTest) { 55 | for (const route in globalVariables.pulledDataFromTest[rps]) { 56 | globalVariables.pulledDataFromTest[rps][route] = processData( 57 | globalVariables.pulledDataFromTest[rps][route] as CollectedData 58 | ); 59 | globalVariables.pulledDataFromTest[rps][route].receivedTime = 60 | globalVariables.timeArrRoutes[route][rps].receivedTotalTime; 61 | globalVariables.pulledDataFromTest[rps][route].errorCount = 62 | globalVariables.timeArrRoutes[route][rps].errorCount; 63 | globalVariables.pulledDataFromTest[rps][route].successfulResCount = 64 | globalVariables.timeArrRoutes[route][rps].successfulResCount; 65 | 66 | //fixing the elapsed time for the last middleware 67 | processLastMiddleware(globalVariables.pulledDataFromTest, rps, route); 68 | } 69 | } 70 | 71 | if (Object.keys(globalVariables.pulledDataFromTest).length > 0) { 72 | const newPulledData = { 73 | testTime: Date.now(), 74 | testData: globalVariables.pulledDataFromTest, 75 | }; 76 | io.emit(ioSocketCommands.allRPSfinished, [newPulledData]); 77 | } 78 | }; 79 | 80 | export default allRPSfinished; 81 | export type { AllRPSfinished }; 82 | -------------------------------------------------------------------------------- /src/server/helpers/emitPercentage.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'socket.io'; 2 | import { GlobalVariables, ioSocketCommands } from '../interfaces'; 3 | 4 | type EmitPercentage = ( 5 | globalVariables: GlobalVariables, 6 | rpsGroup: number, 7 | secondsToTest: number, 8 | io: Server 9 | ) => void; 10 | 11 | const emitPercentage: EmitPercentage = (globalVariables, rpsGroup, secondsToTest, io) => { 12 | const percent = 13 | (globalVariables.successfulResCount + globalVariables.errorCount) / 14 | (rpsGroup * secondsToTest); 15 | if (Math.floor(10000 * percent) % 1000 === 0) { 16 | io.emit(ioSocketCommands.currentRPSProgress, percent); 17 | } 18 | }; 19 | 20 | export default emitPercentage; 21 | export type { EmitPercentage }; 22 | -------------------------------------------------------------------------------- /src/server/helpers/processData.ts: -------------------------------------------------------------------------------- 1 | import { CollectedData, CollectedDataSingle } from '../interfaces'; 2 | 3 | type ProcessData = (data: CollectedData) => CollectedDataSingle; 4 | const processData: ProcessData = (data: CollectedData) => { 5 | const collectedDataArr: CollectedDataSingle[] = []; 6 | for (const key in data) { 7 | collectedDataArr.push(data[key]); 8 | } 9 | 10 | // add middlewares elapsed times 11 | const collectedDataSingle: CollectedDataSingle = collectedDataArr.reduce((acc, cur) => { 12 | for (let i = 0; i < acc.middlewares.length && i < cur.middlewares.length; i++) { 13 | acc.middlewares[i].elapsedTime += cur.middlewares[i].elapsedTime; 14 | } 15 | return acc; 16 | }); 17 | 18 | // divide by the count of requests 19 | collectedDataSingle.middlewares.forEach((middleware) => { 20 | middleware.elapsedTime = 21 | Math.round((100 * middleware.elapsedTime) / collectedDataArr.length) / 100; 22 | }); 23 | 24 | return collectedDataSingle; 25 | }; 26 | 27 | export default processData; 28 | export type { ProcessData }; 29 | -------------------------------------------------------------------------------- /src/server/helpers/processLastMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { PulledDataFromTest, middlewareSingle } from '../interfaces'; 2 | 3 | type ProcessLastMiddleware = ( 4 | pulledDataFromTest: PulledDataFromTest, 5 | rps: string, 6 | route: string 7 | ) => void; 8 | 9 | const processLastMiddleware: ProcessLastMiddleware = (pulledDataFromTest, rps, route) => { 10 | const indexOfLast = 11 | (pulledDataFromTest[rps][route].middlewares as middlewareSingle[]).length - 1; 12 | const tempMiddleware: middlewareSingle = { 13 | fnName: 'temp', 14 | elapsedTime: 0, 15 | }; 16 | 17 | (pulledDataFromTest[rps][route].middlewares as middlewareSingle[])[indexOfLast].elapsedTime = 18 | Math.round( 19 | 100 * 20 | ((pulledDataFromTest[rps][route].receivedTime as number) - 21 | (pulledDataFromTest[rps][route].middlewares as middlewareSingle[]).reduce( 22 | (acc, cur) => { 23 | acc.elapsedTime += cur.elapsedTime; 24 | return acc; 25 | }, 26 | tempMiddleware 27 | ).elapsedTime) 28 | ) / 100; 29 | }; 30 | 31 | export default processLastMiddleware; 32 | export type { ProcessLastMiddleware }; 33 | -------------------------------------------------------------------------------- /src/server/helpers/sendRequests.ts: -------------------------------------------------------------------------------- 1 | import { Jagtestercommands, GlobalVariables, TestConfigData } from '../interfaces'; 2 | 3 | import fetch from 'node-fetch'; 4 | import { Server } from 'socket.io'; 5 | import singleRPSfinished from './singleRPSfinished'; 6 | import allRPSfinished from './allRPSfinished'; 7 | import emitPercentage from './emitPercentage'; 8 | 9 | type SendRequests = ( 10 | targetURL: string, 11 | rpsGroup: number, 12 | rpsActual: number, 13 | secondsToTest: number, 14 | globalVariables: GlobalVariables, 15 | io: Server, 16 | globalTestConfig: TestConfigData 17 | ) => void; 18 | 19 | const sendRequests: SendRequests = ( 20 | targetURL: string, 21 | rpsGroup: number, 22 | rpsActual: number, 23 | secondsToTest: number, 24 | globalVariables: GlobalVariables, 25 | io: Server, 26 | globalTestConfig: TestConfigData 27 | ) => { 28 | const sendFetch = (reqId: number) => { 29 | fetch(targetURL, { 30 | agent: globalVariables.agent, 31 | signal: globalVariables.abortController.signal, 32 | headers: { 33 | jagtestercommand: Jagtestercommands.running.toString(), 34 | jagtesterreqid: reqId.toString(), 35 | }, 36 | }) 37 | .then((res) => { 38 | const resRoute = new URL(targetURL).pathname; 39 | globalVariables.timeArrRoutes[resRoute][rpsGroup].successfulResCount++; 40 | globalVariables.successfulResCount++; 41 | emitPercentage(globalVariables, rpsGroup, secondsToTest, io); 42 | if ( 43 | globalVariables.successfulResCount + globalVariables.errorCount >= 44 | rpsGroup * secondsToTest 45 | ) { 46 | singleRPSfinished(rpsGroup, io, globalTestConfig, globalVariables); 47 | } 48 | if (res.headers.has('x-response-time')) { 49 | const xResponseTime = res.headers.get('x-response-time'); 50 | globalVariables.timeArrRoutes[resRoute][rpsGroup].receivedTotalTime += 51 | xResponseTime ? +xResponseTime : 0; 52 | } 53 | }) 54 | .catch((error) => { 55 | if (error.name === 'AbortError') { 56 | if (globalVariables.isTestRunning) { 57 | globalVariables.isTestRunning = false; 58 | // eventEmitter.emit(ioSocketCommands.allRPSfinished); 59 | allRPSfinished(globalTestConfig, io, globalVariables); 60 | } 61 | } else { 62 | const resRoute = new URL(targetURL).pathname; 63 | globalVariables.timeArrRoutes[resRoute][rpsGroup].errorCount++; 64 | globalVariables.errorCount++; 65 | emitPercentage(globalVariables, rpsGroup, secondsToTest, io); 66 | if ( 67 | globalVariables.successfulResCount + globalVariables.errorCount >= 68 | rpsGroup * secondsToTest 69 | ) { 70 | singleRPSfinished(rpsGroup, io, globalTestConfig, globalVariables); 71 | } 72 | } 73 | }); 74 | }; 75 | 76 | // outer for loop to run for every second and set timeouts for after that second 77 | for (let j = 0; j < secondsToTest; j++) { 78 | for (let i = 0; i < rpsActual; i++) { 79 | const timeout = setTimeout( 80 | sendFetch.bind(this, i + j * rpsActual), 81 | Math.floor(Math.random() * 1000 + 1000 * j) 82 | ); 83 | globalVariables.timeOutArray.push(timeout); 84 | } 85 | } 86 | return; 87 | }; 88 | 89 | export default sendRequests; 90 | export type { SendRequests }; 91 | -------------------------------------------------------------------------------- /src/server/helpers/sendRequestsAtRPS.ts: -------------------------------------------------------------------------------- 1 | import { Jagtestercommands, GlobalVariables, TestConfigData } from '../interfaces'; 2 | import { Server } from 'socket.io'; 3 | import fetch from 'node-fetch'; 4 | import allRPSfinished from './allRPSfinished'; 5 | import sendRequests from './sendRequests'; 6 | 7 | type SendRequestsAtRPS = ( 8 | globalVariables: GlobalVariables, 9 | globalTestConfig: TestConfigData, 10 | io: Server 11 | ) => void; 12 | 13 | const sendRequestsAtRPS: SendRequestsAtRPS = ( 14 | globalVariables: GlobalVariables, 15 | globalTestConfig: TestConfigData, 16 | io: Server 17 | ) => { 18 | // check if finished testing 19 | const curRPS = 20 | globalTestConfig.startRPS + globalVariables.currentInterval * globalTestConfig.rpsInterval; 21 | 22 | if (curRPS > globalTestConfig.endRPS) { 23 | allRPSfinished(globalTestConfig, io, globalVariables); 24 | return; 25 | } 26 | 27 | // update layer first then start testing 28 | for (const target of globalTestConfig.inputsData) { 29 | fetch(target.targetURL, { 30 | agent: globalVariables.agent, 31 | headers: { 32 | jagtestercommand: Jagtestercommands.updateLayer.toString(), 33 | }, 34 | }) 35 | .then(() => { 36 | // saving the resroute into the collection object 37 | const resRoute = new URL(target.targetURL).pathname; 38 | if (globalVariables.timeArrRoutes[resRoute] === undefined) { 39 | globalVariables.timeArrRoutes[resRoute] = {}; 40 | } 41 | if (globalVariables.timeArrRoutes[resRoute][curRPS.toString()] === undefined) { 42 | globalVariables.timeArrRoutes[resRoute][curRPS.toString()] = { 43 | receivedTotalTime: 0, 44 | errorCount: 0, 45 | successfulResCount: 0, 46 | }; 47 | } 48 | 49 | globalVariables.errorCount = 0; 50 | globalVariables.successfulResCount = 0; 51 | sendRequests( 52 | target.targetURL, 53 | curRPS, 54 | Math.round((curRPS * target.percentage) / 100), 55 | globalTestConfig.testLength, 56 | globalVariables, 57 | io, 58 | globalTestConfig 59 | ); 60 | }) 61 | .catch(() => { 62 | allRPSfinished(globalTestConfig, io, globalVariables); 63 | }); 64 | } 65 | }; 66 | 67 | export default sendRequestsAtRPS; 68 | export type { SendRequestsAtRPS }; 69 | -------------------------------------------------------------------------------- /src/server/helpers/singleRPSfinished.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { Server } from 'socket.io'; 3 | import { 4 | ioSocketCommands, 5 | Jagtestercommands, 6 | GlobalVariables, 7 | TestConfigData, 8 | } from '../interfaces'; 9 | import allRPSfinished from './allRPSfinished'; 10 | import sendRequestsAtRPS from './sendRequestsAtRPS'; 11 | 12 | type SingleRPSfinished = ( 13 | rpsGroup: number, 14 | io: Server, 15 | globalTestConfig: TestConfigData, 16 | globalVariables: GlobalVariables 17 | ) => void; 18 | 19 | const singleRPSfinished: SingleRPSfinished = ( 20 | rpsGroup: number, 21 | io: Server, 22 | globalTestConfig: TestConfigData, 23 | globalVariables: GlobalVariables 24 | ) => { 25 | io.emit(ioSocketCommands.singleRPSfinished, rpsGroup); 26 | fetch(globalTestConfig.inputsData[0].targetURL, { 27 | headers: { 28 | jagtestercommand: Jagtestercommands.endTest.toString(), 29 | }, 30 | }) 31 | .then((fetchRes) => fetchRes.json()) 32 | .then((data) => { 33 | const curRPS = 34 | globalTestConfig.startRPS + 35 | globalVariables.currentInterval * globalTestConfig.rpsInterval; 36 | globalVariables.pulledDataFromTest[curRPS.toString()] = data; 37 | globalVariables.currentInterval++; 38 | sendRequestsAtRPS(globalVariables, globalTestConfig, io); 39 | }) 40 | .catch(() => { 41 | allRPSfinished(globalTestConfig, io, globalVariables); 42 | }); 43 | }; 44 | 45 | export default singleRPSfinished; 46 | export type { SingleRPSfinished }; 47 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // TODO use cluster to imrpove our server performance 3 | import { http } from './server'; 4 | import open from 'open'; 5 | 6 | let port = 15000; 7 | 8 | http.on('error', function (e: NodeJS.ErrnoException) { 9 | if (e.code === 'EADDRINUSE') { 10 | port++; 11 | http.listen(port); 12 | } 13 | }); 14 | 15 | const server = http.on('listening', function () { 16 | console.log(`Jagtester running on http://localhost:${port}`); 17 | open(`http://localhost:${port}`).catch((err) => console.log(err)); 18 | }); 19 | 20 | http.listen(port); 21 | 22 | export default server; -------------------------------------------------------------------------------- /src/server/interfaces.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | export interface TimeArrInterface { 3 | receivedTotalTime: number; 4 | recordedTotalTime: number; 5 | } 6 | 7 | export interface middlewareSingle { 8 | fnName: string; 9 | elapsedTime: number; 10 | } 11 | 12 | export interface CollectedData { 13 | [key: string]: { 14 | reqId: string; 15 | reqRoute: string; 16 | middlewares: middlewareSingle[]; 17 | }; 18 | } 19 | 20 | export interface CollectedDataSingle { 21 | receivedTime?: number; 22 | recordedTime?: number; 23 | errorCount?: number; 24 | requestCount?: number; 25 | successfulResCount?: number; 26 | RPS?: number; 27 | reqId?: string; 28 | reqRoute: string; 29 | middlewares: middlewareSingle[]; 30 | } 31 | 32 | export enum Jagtestercommands { 33 | updateLayer, 34 | running, 35 | endTest, 36 | } 37 | 38 | export enum ioSocketCommands { 39 | testRunningStateChange = 'testRunningStateChange', 40 | singleRPSfinished = 'singleRPSfinished', 41 | allRPSfinished = 'allRPSfinished', 42 | errorInfo = 'errorInfo', 43 | currentRPSProgress = 'currentRPSProgress', 44 | } 45 | 46 | export interface TestConfigData { 47 | rpsInterval: number; 48 | startRPS: number; 49 | endRPS: number; 50 | testLength: number; 51 | inputsData: { 52 | method: string; 53 | targetURL: string; 54 | percentage: number; 55 | jagTesterEnabled: boolean; 56 | }[]; 57 | } 58 | 59 | export interface PulledDataFromTest { 60 | // used as rps 61 | [key: string]: { 62 | //used as route 63 | [key: string]: CollectedDataSingle | CollectedData; 64 | }; 65 | } 66 | 67 | export enum HTTPMethods { 68 | GET = 'GET', 69 | POST = 'POST', 70 | PUT = 'PUT', 71 | DELETE = 'DELETE', 72 | PATCH = 'PATCH', 73 | HEAD = 'HEAD', 74 | CONNECT = 'CONNECT', 75 | TRACE = 'TRACE', 76 | } 77 | 78 | export interface TimeArrRoutes { 79 | // this key is used as the route name 80 | [key: string]: { 81 | //this key is used as the rps number 82 | [key: string]: { 83 | receivedTotalTime: number; 84 | errorCount: number; 85 | successfulResCount: number; 86 | }; 87 | }; 88 | } 89 | 90 | export interface TrackedVariables { 91 | isTestRunningInternal: boolean; 92 | isTestRunningListener: (val: boolean) => void; 93 | isTestRunning: boolean; 94 | } 95 | 96 | export interface GlobalVariables { 97 | currentInterval: number; 98 | errorCount: number; 99 | successfulResCount: number; 100 | abortController: AbortController; 101 | timeArrRoutes: TimeArrRoutes; 102 | timeOutArray: NodeJS.Timeout[]; 103 | pulledDataFromTest: PulledDataFromTest; 104 | isTestRunningInternal: boolean; 105 | isTestRunningListener: (val: boolean) => void; 106 | isTestRunning: boolean; 107 | agent: http.Agent; 108 | } 109 | -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // TODO use cluster to imrpove our server performance 3 | import { app } from './app'; 4 | import { createServer } from 'http'; 5 | import { Server } from 'socket.io'; 6 | 7 | const http = createServer(app); 8 | const io = new Server(http); 9 | 10 | export { http, io }; 11 | -------------------------------------------------------------------------------- /src/server/testrouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import fetch from 'node-fetch'; 3 | import http from 'http'; 4 | import { io } from './server'; 5 | import { Jagtestercommands, TestConfigData, ioSocketCommands, GlobalVariables } from './interfaces'; 6 | 7 | import sendRequestsAtRPS from './helpers/sendRequestsAtRPS'; 8 | 9 | import AbortController from 'abort-controller'; 10 | 11 | const router = express.Router(); 12 | 13 | const globalVariables: GlobalVariables = { 14 | currentInterval: 0, 15 | errorCount: 0, 16 | successfulResCount: 0, 17 | abortController: new AbortController(), 18 | timeArrRoutes: {}, 19 | timeOutArray: [], 20 | pulledDataFromTest: {}, 21 | isTestRunningInternal: false, 22 | agent: new http.Agent({ keepAlive: true }), 23 | isTestRunningListener: (val: boolean) => { 24 | io.emit(ioSocketCommands.testRunningStateChange, val); 25 | }, 26 | set isTestRunning(val: boolean) { 27 | this.isTestRunningInternal = val; 28 | this.isTestRunningListener(val); 29 | }, 30 | get isTestRunning() { 31 | return this.isTestRunningInternal; 32 | }, 33 | }; 34 | 35 | let globalTestConfig: TestConfigData; 36 | 37 | router.post('/startmultiple', (req, res) => { 38 | if (!globalVariables.isTestRunning) { 39 | globalVariables.isTestRunning = true; 40 | globalVariables.timeArrRoutes = {}; 41 | globalVariables.pulledDataFromTest = {}; 42 | globalVariables.currentInterval = 0; 43 | globalTestConfig = { 44 | rpsInterval: req.body.rpsInterval, 45 | startRPS: req.body.startRPS, 46 | endRPS: req.body.endRPS, 47 | testLength: req.body.testLength, 48 | inputsData: req.body.inputsData, 49 | }; 50 | 51 | sendRequestsAtRPS(globalVariables, globalTestConfig, io); 52 | } 53 | res.sendStatus(200); 54 | }); 55 | router.post('/checkjagtester', (req, res) => { 56 | fetch(req.body.inputURL, { 57 | method: req.body.method, 58 | agent: globalVariables.agent, 59 | headers: { 60 | jagtestercommand: Jagtestercommands.updateLayer.toString(), 61 | }, 62 | }) 63 | .then((fetchRes) => fetchRes.json()) 64 | .then((data) => res.json(data)) 65 | .catch(() => res.json({ jagtester: false })); 66 | }); 67 | 68 | router.get('/stopTest', (req, res) => { 69 | globalVariables.abortController.abort(); 70 | res.sendStatus(200); 71 | }); 72 | 73 | export default router; 74 | export { globalVariables }; 75 | -------------------------------------------------------------------------------- /src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "target": "es6", 7 | "noImplicitAny": true, 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "declaration": true, 11 | "sourceMap": false, 12 | "outDir": "../../dist/server", 13 | "baseUrl": ".", 14 | "paths": { 15 | "*": ["../../node_modules/*"] 16 | } 17 | }, 18 | "include": ["./**/*"] 19 | } 20 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "outDir": "../dist" 5 | }, 6 | "files": [], 7 | "references": [{ "path": "./client" }, { "path": "./server" }, { "path": "./middleware" }] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "noEmit": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /webpack.dev.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 5 | import ESLintPlugin from 'eslint-webpack-plugin'; 6 | 7 | import { Configuration as WebpackConfiguration } from 'webpack'; 8 | import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server'; 9 | 10 | interface Configuration extends WebpackConfiguration { 11 | devServer?: WebpackDevServerConfiguration; 12 | } 13 | 14 | const config: Configuration = { 15 | mode: 'development', 16 | output: { 17 | publicPath: '/', 18 | }, 19 | entry: './src/client/index.tsx', 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.(ts|js)x?$/i, 24 | exclude: [/node_modules/, __dirname + './splash'], 25 | use: { 26 | loader: 'babel-loader', 27 | options: { 28 | presets: [ 29 | '@babel/preset-env', 30 | '@babel/preset-react', 31 | '@babel/preset-typescript', 32 | ], 33 | }, 34 | }, 35 | }, 36 | { 37 | test: /\.(png|jp(e*)g|gif)$/, 38 | exclude: __dirname + './splash', 39 | use: [ 40 | { 41 | loader: 'file-loader', 42 | }, 43 | ], 44 | }, 45 | { 46 | test: /\.svg$/, 47 | exclude: __dirname + './splash', 48 | use: ['babel-loader', '@svgr/webpack', 'file-loader'], 49 | }, 50 | ], 51 | }, 52 | resolve: { 53 | extensions: ['.tsx', '.ts', '.js'], 54 | }, 55 | plugins: [ 56 | new HtmlWebpackPlugin({ 57 | template: './src/client/index.html', 58 | favicon: './src/client/img/favicon.svg', 59 | }), 60 | new webpack.HotModuleReplacementPlugin(), 61 | new ForkTsCheckerWebpackPlugin({ 62 | async: false, 63 | typescript: { 64 | configFile: './src/client/tsconfig.json', 65 | }, 66 | }), 67 | new ESLintPlugin({ 68 | extensions: ['js', 'jsx', 'ts', 'tsx'], 69 | }), 70 | ], 71 | devtool: 'inline-source-map', 72 | devServer: { 73 | proxy: { 74 | '/api': 'http://localhost:15000', 75 | '/socket.io/': { 76 | target: 'http://localhost:15000', 77 | ws: true, 78 | }, 79 | }, 80 | contentBase: path.join(__dirname, 'build'), 81 | historyApiFallback: true, 82 | port: 8080, 83 | open: false, 84 | hot: true, 85 | }, 86 | }; 87 | 88 | export default config; 89 | -------------------------------------------------------------------------------- /webpack.prod.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 5 | import ESLintPlugin from 'eslint-webpack-plugin'; 6 | import { CleanWebpackPlugin } from 'clean-webpack-plugin'; 7 | 8 | const config: webpack.Configuration = { 9 | mode: 'production', 10 | entry: './src/client/index.tsx', 11 | output: { 12 | path: path.resolve( 13 | __dirname, 14 | process.env.JAG === 'demo' ? 'splash/bundle/demo' : 'dist/client' 15 | ), 16 | filename: '[name].js', 17 | publicPath: '', 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(ts|js)x?$/i, 23 | exclude: [/node_modules/, __dirname + './splash'], 24 | use: { 25 | loader: 'babel-loader', 26 | options: { 27 | presets: [ 28 | '@babel/preset-env', 29 | '@babel/preset-react', 30 | '@babel/preset-typescript', 31 | ], 32 | }, 33 | }, 34 | }, 35 | { 36 | test: /\.(png|jp(e*)g|gif)$/, 37 | exclude: __dirname + './splash', 38 | use: [ 39 | { 40 | loader: 'file-loader', 41 | }, 42 | ], 43 | }, 44 | { 45 | test: /\.svg$/, 46 | exclude: __dirname + './splash', 47 | use: ['babel-loader', '@svgr/webpack', 'file-loader'], 48 | }, 49 | ], 50 | }, 51 | resolve: { 52 | extensions: ['.tsx', '.ts', '.js'], 53 | }, 54 | plugins: [ 55 | new HtmlWebpackPlugin({ 56 | template: 'src/client/index.html', 57 | favicon: './src/client/img/favicon.svg', 58 | }), 59 | new ForkTsCheckerWebpackPlugin({ 60 | async: false, 61 | typescript: { 62 | configFile: './src/client/tsconfig.json', 63 | }, 64 | }), 65 | new ESLintPlugin({ 66 | extensions: ['js', 'jsx', 'ts', 'tsx'], 67 | }), 68 | new CleanWebpackPlugin(), 69 | new webpack.EnvironmentPlugin(['JAG']), 70 | ], 71 | }; 72 | 73 | export default config; 74 | --------------------------------------------------------------------------------