├── .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 | [](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 | [](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 |
23 | Close
24 |
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 |
33 |
34 | Test
35 |
36 |
37 | Results
38 |
39 |
40 | About Us
41 |
42 |
43 |
44 |
45 |
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 |
36 | {value === index && {children} }
37 |
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 |
65 | Start Testing
66 |
67 |
68 |
69 |
75 | Stop
76 |
77 |
78 |
79 | dispatch(Actions.ResetState())}
83 | >
84 | Reset to default
85 |
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 |
42 | Export All
43 |
44 |
45 | Delete All
46 |
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 |
--------------------------------------------------------------------------------