http-log-replay 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,16 +49,20 @@ This will launch the **Traffic Mirror Dashboard** at [http://localhost:4200](htt
49
49
  The Web UI is the best way to get started. It guides you through the entire workflow in three simple tabs.
50
50
 
51
51
  ### 1. 🔴 Record / Generate
52
- - **Manual Mode**: Start the proxy, point your application/client to it, and use your app normally. Requests are saved to a file.
52
+
53
+ - **Manual Mode**: Start the proxy, point your application/client to it, and select desired HTTP methods (e.g., GET, POST).
53
54
  - **Auto-Generate**: Upload an OpenAPI/Swagger JSON file to automatically generate realistic traffic patterns.
55
+ - **YAML Config Mode**: Load a `config.yaml` file to pre-configure Traffic Generation (target, source, exclusions, timeouts, methods).
54
56
 
55
57
  ### 2. ▶️ Replay
58
+
56
59
  - Configure your **Primary** (Stable) and **Secondary** (Test) environments.
57
60
  - Set **Concurrency** to speed up large suites.
58
61
  - Add **Ignore Fields** (e.g., `createdAt`, `traceId`) to filter out noise in the comparison.
59
62
  - Click **Replay & Compare**.
60
63
 
61
64
  ### 3. 📄 Report
65
+
62
66
  - Instantly view the results.
63
67
  - **Green**: Exact match.
64
68
  - **Red**: Mismatch (click to expand the JSON diff).
@@ -70,24 +74,36 @@ The Web UI is the best way to get started. It guides you through the entire work
70
74
  Perfect for **CI/CD pipelines** or headless environments.
71
75
 
72
76
  ### 1. Record Traffic
77
+
73
78
  Start a recording proxy on port `3000` forwarding to your real API at `localhost:8080`.
74
79
 
75
80
  ```bash
76
- npx http-log-replay record --target http://localhost:8080 --port 3000 --out traffic.jsonl
81
+ # Run from source
82
+ node index.js record --target http://localhost:8080 --port 3000 --out traffic.jsonl
77
83
  ```
78
84
 
79
85
  ### 2. Auto-Generate from Swagger
80
- Generate traffic without manual clicking.
86
+
87
+ Generate traffic without manual clicking. You can use command line flags or a YAML configuration file.
88
+
89
+ **Using Configuration File (Recommended):**
81
90
 
82
91
  ```bash
83
- npx http-log-replay generate --file ./openapi.json --target http://localhost:3000
92
+ node index.js generate --config config.example.yaml
93
+ ```
94
+
95
+ **Using Flags:**
96
+
97
+ ```bash
98
+ node index.js generate --file ./openapi.json --target http://localhost:3000
84
99
  ```
85
100
 
86
101
  ### 3. Replay & Verify
102
+
87
103
  Replay recorded traffic against two environments and generate an HTML report.
88
104
 
89
105
  ```bash
90
- npx http-log-replay replay \
106
+ node index.js replay \
91
107
  --log traffic.jsonl \
92
108
  --primary http://prod-api.com \
93
109
  --secondary http://staging-api.com \
@@ -102,18 +118,23 @@ npx http-log-replay replay \
102
118
  We provide optimized Docker images for both the UI and CLI.
103
119
 
104
120
  ### Prerequisites
121
+
105
122
  - Docker & Docker Compose installed.
106
123
 
107
124
  ### Option A: Complete Environment (Recommended)
125
+
108
126
  Use the included `docker-compose.yml` to run everything.
109
127
 
110
128
  **Start the GUI:**
129
+
111
130
  ```bash
112
131
  docker-compose up gui
113
132
  ```
133
+
114
134
  > Access at [http://localhost:4200](http://localhost:4200). Data is persisted to your host folder.
115
135
 
116
136
  **Run CLI Commands:**
137
+
117
138
  ```bash
118
139
  docker-compose run cli record --target http://host.docker.internal:8080 ...
119
140
  ```
@@ -121,11 +142,13 @@ docker-compose run cli record --target http://host.docker.internal:8080 ...
121
142
  ### Option B: Manual Docker Run
122
143
 
123
144
  **UI Image:**
145
+
124
146
  ```bash
125
147
  docker run -p 4200:4200 -v $(pwd):/app traffic-mirror-gui
126
148
  ```
127
149
 
128
150
  **CLI Image:**
151
+
129
152
  ```bash
130
153
  docker run -v $(pwd):/app traffic-mirror-cli --help
131
154
  ```
@@ -137,6 +160,7 @@ docker run -v $(pwd):/app traffic-mirror-cli --help
137
160
  We welcome contributions! Here is how to run the project locally for development.
138
161
 
139
162
  ### 1. Setup
163
+
140
164
  ```bash
141
165
  git clone https://github.com/your-username/http-log-replay.git
142
166
  cd http-log-replay
@@ -144,7 +168,9 @@ npm install
144
168
  ```
145
169
 
146
170
  ### 2. Build the UI
171
+
147
172
  The UI is built with Angular. You must build it before running the app.
173
+
148
174
  ```bash
149
175
  cd ui
150
176
  npm install
@@ -153,12 +179,14 @@ cd ..
153
179
  ```
154
180
 
155
181
  ### 3. Run Locally (Dev Mode)
182
+
156
183
  ```bash
157
184
  # Start the full app (Frontend + Backend)
158
185
  node index.js ui --port 4200
159
186
  ```
160
187
 
161
188
  ### 4. Testing & Code Quality
189
+
162
190
  We use **Jest** for testing and **ESLint** for code quality.
163
191
 
164
192
  ```bash
@@ -170,13 +198,35 @@ npm run lint
170
198
 
171
199
  # Format Code
172
200
  npm run format
201
+
202
+ # Production Start (PM2)
203
+ npm run start:pm2
173
204
  ```
174
205
 
206
+ ### 5. Contribution Guidelines
207
+
208
+ We use **Semantic Release** to automate version management and package publishing. To ensure this works correctly, please follow the **Conventional Commits** specification for your commit messages.
209
+
210
+ **Commit Message Format:**
211
+
212
+ ```
213
+ <type>(<scope>): <subject>
214
+ ```
215
+
216
+ **Common Types:**
217
+
218
+ - **`feat`**: A new feature (triggers a **MINOR** release).
219
+ - **`fix`**: A bug fix (triggers a **PATCH** release).
220
+ - **`docs`**: Documentation only changes (triggers a **PATCH** release).
221
+ - **`refactor`**: A code change that neither fixes a bug nor adds a feature (triggers a **PATCH** release).
222
+ - **`perf`**, **`test`**, **`ci`**, **`chore`**: Changes that do not trigger a release (unless configured otherwise).
223
+
175
224
  ---
176
225
 
177
226
  ## 📦 Maintenance
178
227
 
179
228
  ### Publishing to NPM
229
+
180
230
  1. **Bump Version**: Update `version` in `package.json`.
181
231
  2. **Build UI**: The `prepublishOnly` script will automatically build the Angular UI.
182
232
  3. **Publish**:
@@ -185,6 +235,7 @@ npm run format
185
235
  ```
186
236
 
187
237
  ### File Structure
238
+
188
239
  - `index.js`: CLI entry point.
189
240
  - `recorder.js`: Proxy logic.
190
241
  - `replayer.js`: Replay & Diff logic.
package/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  /* eslint-disable no-console */
3
3
  const { Command } = require('commander');
4
+ const fs = require('fs');
5
+ const yaml = require('js-yaml');
4
6
  const recorder = require('./recorder');
5
7
  const replayAndDiff = require('./replayer');
6
8
  const startServer = require('./server');
@@ -64,26 +66,34 @@ program
64
66
 
65
67
  program
66
68
  .command('generate')
67
- .description('Auto-generate traffic from Swagger file')
68
- .option('-t, --target <url>', 'Proxy URL', 'http://localhost:3000')
69
- .option('-f, --file <path>', 'Swagger file path', './full_documentation.json')
70
- .option('-x, --exclude <items>', 'Comma separated list of endpoints to exclude', (val) =>
69
+ .description('Auto-generate traffic from Swagger file using a configuration file')
70
+ .option('-c, --config <path>', 'Path to YAML configuration file', 'config.yaml')
71
+ .option('-m, --methods <items>', 'Comma separated list of HTTP methods', (val) =>
71
72
  val.split(',')
72
- ) // <--- NEW OPTION
73
- .option('-s, --source <url>', 'Source Server URL', 'http://localhost:1338')
73
+ )
74
74
  .action(async (options) => {
75
75
  try {
76
76
  console.log('🚀 Starting Traffic Generation...');
77
77
 
78
- // Pass options.exclude (or empty array) as 3rd arg
78
+ let config = {};
79
+ if (options.config && fs.existsSync(options.config)) {
80
+ console.log(`Loading configuration from ${options.config}...`);
81
+ const raw = fs.readFileSync(options.config, 'utf8');
82
+ config = yaml.load(raw);
83
+
84
+ // Override methods if provided via CLI
85
+ if (options.methods && options.methods.length > 0) {
86
+ config.methods = options.methods;
87
+ }
88
+ } else {
89
+ throw new Error(`Configuration file not found at ${options.config}`);
90
+ }
91
+
79
92
  await generator.run(
80
- options.target,
81
- options.file,
82
- options.exclude || [],
93
+ config,
83
94
  (log) => {
84
95
  console.log(log.message);
85
- },
86
- options.source
96
+ }
87
97
  );
88
98
 
89
99
  console.log('✅ Done.');
package/package.json CHANGED
@@ -1,55 +1,60 @@
1
- {
2
- "name": "http-log-replay",
3
- "version": "1.0.0",
4
- "description": "Traffic Mirror is a regression testing tool that records HTTP traffic from a live environment and replays it against two different environments (e.g., Stable vs. Canary) to detect differences in responses.",
5
- "author": "Xhani Manolis Trungu",
6
- "main": "index.js",
7
- "bin": {
8
- "traffic-mirror": "./index.js"
9
- },
10
- "files": [
11
- "*.js",
12
- "ui/dist"
13
- ],
14
- "scripts": {
15
- "test": "jest tests/",
16
- "lint": "eslint .",
17
- "format": "prettier --write .",
18
- "start:pm2": "pm2 start ecosystem.config.js",
19
- "prepublishOnly": "cd ui && npm install && npm run build"
20
- },
21
- "keywords": [
22
- "http-log",
23
- "http-log-replay",
24
- "traffic-replay",
25
- "regression-testing",
26
- "api-regression",
27
- "api-testing"
28
- ],
29
- "license": "ISC",
30
- "type": "commonjs",
31
- "dependencies": {
32
- "axios": "^1.13.2",
33
- "colors": "^1.4.0",
34
- "commander": "^14.0.2",
35
- "cors": "^2.8.5",
36
- "deep-diff": "^1.0.2",
37
- "diff": "^8.0.2",
38
- "express": "^5.2.1",
39
- "http-proxy": "^1.18.1",
40
- "open": "^11.0.0",
41
- "readline": "^1.3.0",
42
- "socket.io": "^4.8.1"
43
- },
44
- "devDependencies": {
45
- "eslint": "^9.39.2",
46
- "eslint-config-prettier": "^10.1.8",
47
- "jest": "^30.2.0",
48
- "pm2": "^6.0.14",
49
- "prettier": "^3.7.4"
50
- },
51
- "repository": {
52
- "type": "git",
53
- "url": "https://github.com/xhani-manolis-trungu/traffic-mirror"
54
- }
55
- }
1
+ {
2
+ "name": "http-log-replay",
3
+ "version": "1.1.1",
4
+ "description": "Traffic Mirror is a regression testing tool that records HTTP traffic from a live environment and replays it against two different environments (e.g., Stable vs. Canary) to detect differences in responses.",
5
+ "author": "Xhani Manolis Trungu",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "http-log-replay": "./index.js"
9
+ },
10
+ "files": [
11
+ "*.js",
12
+ "ui/dist"
13
+ ],
14
+ "scripts": {
15
+ "test": "jest tests/",
16
+ "lint": "eslint .",
17
+ "format": "prettier --write .",
18
+ "start:pm2": "pm2 start ecosystem.config.js",
19
+ "prepublishOnly": "cd ui && npm install && npm run build"
20
+ },
21
+ "keywords": [
22
+ "http-log",
23
+ "http-log-replay",
24
+ "traffic-replay",
25
+ "regression-testing",
26
+ "api-regression",
27
+ "api-testing"
28
+ ],
29
+ "license": "ISC",
30
+ "type": "commonjs",
31
+ "dependencies": {
32
+ "axios": "^1.13.2",
33
+ "colors": "^1.4.0",
34
+ "commander": "^14.0.2",
35
+ "cors": "^2.8.5",
36
+ "deep-diff": "^1.0.2",
37
+ "diff": "^8.0.2",
38
+ "express": "^5.2.1",
39
+ "http-proxy": "^1.18.1",
40
+ "js-yaml": "^4.1.1",
41
+ "open": "^11.0.0",
42
+ "readline": "^1.3.0",
43
+ "socket.io": "^4.8.1"
44
+ },
45
+ "devDependencies": {
46
+ "@semantic-release/changelog": "^6.0.3",
47
+ "@semantic-release/git": "^10.0.1",
48
+ "conventional-changelog-conventionalcommits": "^8.0.0",
49
+ "eslint": "^9.39.2",
50
+ "eslint-config-prettier": "^10.1.8",
51
+ "jest": "^30.2.0",
52
+ "pm2": "^6.0.14",
53
+ "prettier": "^3.7.4",
54
+ "semantic-release": "^25.0.2"
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://github.com/xhani-manolis-trungu/traffic-mirror"
59
+ }
60
+ }
package/recorder.js CHANGED
@@ -1,114 +1,114 @@
1
- /* eslint-disable no-console */
2
- const http = require('http');
3
- const httpProxy = require('http-proxy');
4
- const fs = require('fs');
5
-
6
- class Recorder {
7
- constructor() {
8
- this.server = null;
9
- this.proxy = httpProxy.createProxyServer({});
10
-
11
- // 1. Handle Proxy Errors (Critical for preventing crashes)
12
- this.proxy.on('error', (err, req, res) => {
13
- console.error(`❌ Proxy Error [${req.url}]:`, err.message);
14
- if (!res.headersSent) {
15
- res.writeHead(502, { 'Content-Type': 'application/json' });
16
- }
17
- res.end(JSON.stringify({ error: 'Proxy Request Failed', details: err.message }));
18
- });
19
- }
20
-
21
- start(target, port, outputFile, onLog = () => { }) {
22
- if (this.server) throw new Error('Recorder already running');
23
-
24
- console.log(`📝 Recording to file: ${outputFile}`);
25
- const stream = fs.createWriteStream(outputFile, { flags: 'a' });
26
-
27
- // Handle file write errors
28
- stream.on('error', (err) => {
29
- console.error('❌ File Write Error:', err.message);
30
- });
31
-
32
- this.server = http.createServer((req, res) => {
33
- // 2. Capture Request Body safely
34
- // We use a separate array but we DO NOT attach 'data' listeners directly
35
- // unless we plan to buffer it for the proxy.
36
- // For simplicity, let's rely on the response capture primarily.
37
-
38
- const reqChunks = [];
39
- // We tap into the stream without consuming it destructively if possible,
40
- // but the safest way with http-proxy is often just to listen to the proxy events.
41
- // HOWEVER, for your specific "Request Stealing" fix:
42
- req.on('data', (chunk) => reqChunks.push(chunk));
43
-
44
- // --- Response Capture Logic ---
45
- const originalWrite = res.write;
46
- const originalEnd = res.end;
47
- const resChunks = [];
48
-
49
- res.write = function (chunk, ...args) {
50
- if (chunk) resChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
51
- return originalWrite.apply(res, [chunk, ...args]);
52
- };
53
-
54
- res.end = function (chunk, ...args) {
55
- if (chunk) resChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
56
-
57
- // WRAP IN TRY/CATCH to prevent silent crashes
58
- try {
59
- const reqBody = Buffer.concat(reqChunks).toString();
60
- const resBody = Buffer.concat(resChunks).toString('utf8');
61
-
62
- const logData = {
63
- timestamp: new Date().toISOString(),
64
- method: req.method,
65
- url: req.url,
66
- status: res.statusCode,
67
- requestBody: reqBody || '',
68
- responseBody: resBody || '',
69
- // requestHeaders: req.headers, // Uncomment if needed (verbose)
70
- };
71
-
72
- const jsonLine = JSON.stringify(logData) + '\n';
73
-
74
- // 3. Write and Log
75
- stream.write(jsonLine);
76
- onLog(logData);
77
-
78
- // Debug Log to prove it worked
79
- process.stdout.write('.'); // Prints a dot for every logged request
80
- } catch (e) {
81
- console.error('\n❌ Error generating log:', e.message);
82
- }
83
-
84
- return originalEnd.apply(res, [chunk, ...args]);
85
- };
86
-
87
- // 4. Force Plain Text (Disable Gzip)
88
- delete req.headers['accept-encoding'];
89
-
90
- // 5. Forward to Target
91
- // We must set 'buffer' to the request stream if we consumed it,
92
- // but since we are just tapping 'data' without pausing, http-proxy might miss it.
93
- // A simple workaround for node streams in this context:
94
- this.proxy.web(req, res, { target });
95
- });
96
-
97
- this.server.listen(port, () => {
98
- console.log(`\n⏺️ Recorder listening on port ${port}`);
99
- console.log(`➡️ Forwarding to ${target}`);
100
- });
101
-
102
- return `Recorder started`;
103
- }
104
-
105
- stop() {
106
- if (this.server) {
107
- this.server.close();
108
- this.server = null;
109
- return 'Recorder stopped';
110
- }
111
- }
112
- }
113
-
114
- module.exports = new Recorder();
1
+ /* eslint-disable no-console */
2
+ const http = require('http');
3
+ const httpProxy = require('http-proxy');
4
+ const fs = require('fs');
5
+
6
+ class Recorder {
7
+ constructor() {
8
+ this.server = null;
9
+ this.proxy = httpProxy.createProxyServer({});
10
+
11
+ // 1. Handle Proxy Errors (Critical for preventing crashes)
12
+ this.proxy.on('error', (err, req, res) => {
13
+ console.error(`❌ Proxy Error [${req.url}]:`, err.message);
14
+ if (!res.headersSent) {
15
+ res.writeHead(502, { 'Content-Type': 'application/json' });
16
+ }
17
+ res.end(JSON.stringify({ error: 'Proxy Request Failed', details: err.message }));
18
+ });
19
+ }
20
+
21
+ start(target, port, outputFile, onLog = () => { }) {
22
+ if (this.server) throw new Error('Recorder already running');
23
+
24
+ console.log(`📝 Recording to file: ${outputFile}`);
25
+ const stream = fs.createWriteStream(outputFile, { flags: 'a' });
26
+
27
+ // Handle file write errors
28
+ stream.on('error', (err) => {
29
+ console.error('❌ File Write Error:', err.message);
30
+ });
31
+
32
+ this.server = http.createServer((req, res) => {
33
+ // 2. Capture Request Body safely
34
+ // We use a separate array but we DO NOT attach 'data' listeners directly
35
+ // unless we plan to buffer it for the proxy.
36
+ // For simplicity, let's rely on the response capture primarily.
37
+
38
+ const reqChunks = [];
39
+ // We tap into the stream without consuming it destructively if possible,
40
+ // but the safest way with http-proxy is often just to listen to the proxy events.
41
+ // HOWEVER, for your specific "Request Stealing" fix:
42
+ req.on('data', (chunk) => reqChunks.push(chunk));
43
+
44
+ // --- Response Capture Logic ---
45
+ const originalWrite = res.write;
46
+ const originalEnd = res.end;
47
+ const resChunks = [];
48
+
49
+ res.write = function (chunk, ...args) {
50
+ if (chunk) resChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
51
+ return originalWrite.apply(res, [chunk, ...args]);
52
+ };
53
+
54
+ res.end = function (chunk, ...args) {
55
+ if (chunk) resChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
56
+
57
+ // WRAP IN TRY/CATCH to prevent silent crashes
58
+ try {
59
+ const reqBody = Buffer.concat(reqChunks).toString();
60
+ const resBody = Buffer.concat(resChunks).toString('utf8');
61
+
62
+ const logData = {
63
+ timestamp: new Date().toISOString(),
64
+ method: req.method,
65
+ url: req.url,
66
+ status: res.statusCode,
67
+ requestBody: reqBody || '',
68
+ responseBody: resBody || '',
69
+ // requestHeaders: req.headers, // Uncomment if needed (verbose)
70
+ };
71
+
72
+ const jsonLine = JSON.stringify(logData) + '\n';
73
+
74
+ // 3. Write and Log
75
+ stream.write(jsonLine);
76
+ onLog(logData);
77
+
78
+ // Debug Log to prove it worked
79
+ process.stdout.write('.'); // Prints a dot for every logged request
80
+ } catch (e) {
81
+ console.error('\n❌ Error generating log:', e.message);
82
+ }
83
+
84
+ return originalEnd.apply(res, [chunk, ...args]);
85
+ };
86
+
87
+ // 4. Force Plain Text (Disable Gzip)
88
+ delete req.headers['accept-encoding'];
89
+
90
+ // 5. Forward to Target
91
+ // We must set 'buffer' to the request stream if we consumed it,
92
+ // but since we are just tapping 'data' without pausing, http-proxy might miss it.
93
+ // A simple workaround for node streams in this context:
94
+ this.proxy.web(req, res, { target });
95
+ });
96
+
97
+ this.server.listen(port, () => {
98
+ console.log(`\n⏺️ Recorder listening on port ${port}`);
99
+ console.log(`➡️ Forwarding to ${target}`);
100
+ });
101
+
102
+ return `Recorder started`;
103
+ }
104
+
105
+ stop() {
106
+ if (this.server) {
107
+ this.server.close();
108
+ this.server = null;
109
+ return 'Recorder stopped';
110
+ }
111
+ }
112
+ }
113
+
114
+ module.exports = new Recorder();