test-proxy-recorder 0.3.3 → 0.3.5
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 +180 -524
- package/dist/index.cjs +98 -84
- package/dist/index.d.cts +9 -6
- package/dist/index.d.ts +9 -6
- package/dist/index.mjs +98 -84
- package/dist/playwright/index.cjs +7 -8
- package/dist/playwright/index.mjs +7 -8
- package/dist/proxy.js +131 -105
- package/package.json +8 -2
package/dist/proxy.js
CHANGED
|
@@ -8,6 +8,17 @@ import { WebSocket, WebSocketServer } from 'ws';
|
|
|
8
8
|
import crypto from 'crypto';
|
|
9
9
|
import filenamify2 from 'filenamify';
|
|
10
10
|
|
|
11
|
+
// src/cli.ts
|
|
12
|
+
|
|
13
|
+
// src/constants.ts
|
|
14
|
+
var DEFAULT_TIMEOUT_MS = 120 * 1e3;
|
|
15
|
+
var HTTP_STATUS_BAD_GATEWAY = 502;
|
|
16
|
+
var HTTP_STATUS_OK = 200;
|
|
17
|
+
var HTTP_STATUS_BAD_REQUEST = 400;
|
|
18
|
+
var HTTP_STATUS_NOT_FOUND = 404;
|
|
19
|
+
var CONTROL_ENDPOINT = "/__control";
|
|
20
|
+
var RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
21
|
+
|
|
11
22
|
// src/cli.ts
|
|
12
23
|
var DEFAULT_PORT = 8e3;
|
|
13
24
|
var DEFAULT_RECORDINGS_DIR = "./recordings";
|
|
@@ -16,8 +27,8 @@ function parseCliArgs() {
|
|
|
16
27
|
program.name("dev-proxy").description(
|
|
17
28
|
"Development proxy server with recording and replay capabilities"
|
|
18
29
|
).argument(
|
|
19
|
-
"<
|
|
20
|
-
"Target API service
|
|
30
|
+
"<target>",
|
|
31
|
+
"Target API service URL (e.g., http://localhost:3000)"
|
|
21
32
|
).option(
|
|
22
33
|
"-p, --port <number>",
|
|
23
34
|
"Port number for the proxy server",
|
|
@@ -26,32 +37,32 @@ function parseCliArgs() {
|
|
|
26
37
|
"-d, --dir <path>",
|
|
27
38
|
"Directory to store recordings (relative to CWD)",
|
|
28
39
|
DEFAULT_RECORDINGS_DIR
|
|
40
|
+
).option(
|
|
41
|
+
"-t, --timeout <ms>",
|
|
42
|
+
"Session timeout in milliseconds",
|
|
43
|
+
String(DEFAULT_TIMEOUT_MS)
|
|
29
44
|
).action(() => {
|
|
30
45
|
});
|
|
31
46
|
program.parse();
|
|
32
|
-
const
|
|
47
|
+
const target2 = program.args[0];
|
|
33
48
|
const options = program.opts();
|
|
34
49
|
const port2 = Number.parseInt(options.port, 10);
|
|
35
50
|
if (Number.isNaN(port2) || port2 < 1025 || port2 > 65535) {
|
|
36
51
|
console.error("Error: Invalid port number. Must be between 1 and 65535");
|
|
37
52
|
process.exit(1);
|
|
38
53
|
}
|
|
39
|
-
|
|
54
|
+
const timeout2 = Number.parseInt(options.timeout, 10);
|
|
55
|
+
if (Number.isNaN(timeout2) || timeout2 < 0) {
|
|
56
|
+
console.error("Error: Invalid timeout. Must be a non-negative number");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (!target2) {
|
|
40
60
|
program.help();
|
|
41
61
|
}
|
|
42
62
|
const recordingsDir2 = path.resolve(process.cwd(), options.dir);
|
|
43
|
-
return {
|
|
63
|
+
return { target: target2, port: port2, recordingsDir: recordingsDir2, timeout: timeout2 };
|
|
44
64
|
}
|
|
45
65
|
|
|
46
|
-
// src/constants.ts
|
|
47
|
-
var DEFAULT_TIMEOUT_MS = 120 * 1e3;
|
|
48
|
-
var HTTP_STATUS_BAD_GATEWAY = 502;
|
|
49
|
-
var HTTP_STATUS_OK = 200;
|
|
50
|
-
var HTTP_STATUS_BAD_REQUEST = 400;
|
|
51
|
-
var HTTP_STATUS_NOT_FOUND = 404;
|
|
52
|
-
var CONTROL_ENDPOINT = "/__control";
|
|
53
|
-
var RECORDING_ID_HEADER = "x-test-rcrd-id";
|
|
54
|
-
|
|
55
66
|
// src/types.ts
|
|
56
67
|
var Modes = {
|
|
57
68
|
transparent: "transparent",
|
|
@@ -95,9 +106,9 @@ function processRecordings(recordings) {
|
|
|
95
106
|
const processedRecordings = [];
|
|
96
107
|
for (const [_key, keyRecordings] of recordingsByKey) {
|
|
97
108
|
keyRecordings.sort((a, b) => a.recordingId - b.recordingId);
|
|
98
|
-
keyRecordings.
|
|
109
|
+
for (const [index, recording] of keyRecordings.entries()) {
|
|
99
110
|
processedRecordings.push({ ...recording, sequence: index });
|
|
100
|
-
}
|
|
111
|
+
}
|
|
101
112
|
}
|
|
102
113
|
processedRecordings.sort((a, b) => a.recordingId - b.recordingId);
|
|
103
114
|
return processedRecordings;
|
|
@@ -155,8 +166,7 @@ function sendJsonResponse(res, statusCode, data) {
|
|
|
155
166
|
|
|
156
167
|
// src/ProxyServer.ts
|
|
157
168
|
var ProxyServer = class {
|
|
158
|
-
|
|
159
|
-
currentTargetIndex;
|
|
169
|
+
target;
|
|
160
170
|
mode;
|
|
161
171
|
recordingId;
|
|
162
172
|
replayId;
|
|
@@ -164,19 +174,22 @@ var ProxyServer = class {
|
|
|
164
174
|
proxy;
|
|
165
175
|
currentSession;
|
|
166
176
|
recordingsDir;
|
|
177
|
+
timeoutMs;
|
|
167
178
|
recordingIdCounter;
|
|
168
179
|
// Unique ID for each recording entry
|
|
169
180
|
sequenceCounterByKey;
|
|
170
181
|
// Sequence counter per key (endpoint)
|
|
171
182
|
replaySessions;
|
|
172
183
|
// Track multiple concurrent replay sessions by recording ID
|
|
184
|
+
sessionEvictionTimer;
|
|
185
|
+
// Periodic timer to evict idle replay sessions
|
|
173
186
|
recordingPromises;
|
|
174
187
|
// Stack of promises that resolve to completed recordings
|
|
175
188
|
flushPromise;
|
|
176
189
|
// Promise for in-progress flush operation
|
|
177
|
-
constructor(
|
|
178
|
-
this.
|
|
179
|
-
this.
|
|
190
|
+
constructor(target2, recordingsDir2, timeoutMs) {
|
|
191
|
+
this.target = target2;
|
|
192
|
+
this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
180
193
|
this.mode = Modes.transparent;
|
|
181
194
|
this.recordingId = null;
|
|
182
195
|
this.recordingIdCounter = 0;
|
|
@@ -186,6 +199,7 @@ var ProxyServer = class {
|
|
|
186
199
|
this.currentSession = null;
|
|
187
200
|
this.recordingsDir = recordingsDir2;
|
|
188
201
|
this.replaySessions = /* @__PURE__ */ new Map();
|
|
202
|
+
this.sessionEvictionTimer = null;
|
|
189
203
|
this.recordingPromises = [];
|
|
190
204
|
this.flushPromise = null;
|
|
191
205
|
this.proxy = httpProxy.createProxyServer({
|
|
@@ -248,11 +262,6 @@ var ProxyServer = class {
|
|
|
248
262
|
const corsHeaders = this.getCorsHeaders(req);
|
|
249
263
|
Object.assign(proxyRes.headers, corsHeaders);
|
|
250
264
|
}
|
|
251
|
-
getTarget() {
|
|
252
|
-
const target = this.targets[this.currentTargetIndex];
|
|
253
|
-
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
254
|
-
return target;
|
|
255
|
-
}
|
|
256
265
|
/**
|
|
257
266
|
* Extract recording ID from custom HTTP header
|
|
258
267
|
* Used for concurrent replay session routing, especially with Next.js
|
|
@@ -288,13 +297,7 @@ var ProxyServer = class {
|
|
|
288
297
|
getRecordingIdFromRequest(req) {
|
|
289
298
|
const fromHeader = this.getRecordingIdFromHeader(req);
|
|
290
299
|
const fromCookie = this.getRecordingIdFromCookie(req);
|
|
291
|
-
|
|
292
|
-
return fromHeader;
|
|
293
|
-
}
|
|
294
|
-
if (fromCookie) {
|
|
295
|
-
return fromCookie;
|
|
296
|
-
}
|
|
297
|
-
return null;
|
|
300
|
+
return fromHeader ?? fromCookie ?? null;
|
|
298
301
|
}
|
|
299
302
|
/**
|
|
300
303
|
* Get or create a replay session state for a given recording ID
|
|
@@ -314,6 +317,7 @@ var ProxyServer = class {
|
|
|
314
317
|
sortedRecordingsByKey: /* @__PURE__ */ new Map()
|
|
315
318
|
};
|
|
316
319
|
this.replaySessions.set(recordingId, session);
|
|
320
|
+
this.startSessionEvictionTimer();
|
|
317
321
|
console.log(
|
|
318
322
|
`[CONCURRENT REPLAY] Created new session for recording: ${recordingId}`
|
|
319
323
|
);
|
|
@@ -325,8 +329,9 @@ var ProxyServer = class {
|
|
|
325
329
|
* @param sessionId The session ID to clean up
|
|
326
330
|
*/
|
|
327
331
|
async cleanupSession(sessionId) {
|
|
328
|
-
|
|
329
|
-
|
|
332
|
+
this.replaySessions.delete(sessionId);
|
|
333
|
+
if (this.replaySessions.size === 0) {
|
|
334
|
+
this.stopSessionEvictionTimer();
|
|
330
335
|
}
|
|
331
336
|
if (this.recordingId === sessionId) {
|
|
332
337
|
await this.saveCurrentSession();
|
|
@@ -338,29 +343,44 @@ var ProxyServer = class {
|
|
|
338
343
|
}
|
|
339
344
|
console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
|
|
340
345
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const id = url.searchParams.get("id") || void 0;
|
|
345
|
-
const timeoutParam = url.searchParams.get("timeout");
|
|
346
|
-
const timeout = timeoutParam ? Number.parseInt(timeoutParam, 10) : void 0;
|
|
347
|
-
if (!mode) {
|
|
348
|
-
throw new Error("Mode parameter is required");
|
|
346
|
+
startSessionEvictionTimer() {
|
|
347
|
+
if (this.sessionEvictionTimer) {
|
|
348
|
+
return;
|
|
349
349
|
}
|
|
350
|
-
|
|
350
|
+
const CHECK_INTERVAL_MS = 3e4;
|
|
351
|
+
this.sessionEvictionTimer = setInterval(() => {
|
|
352
|
+
const now = Date.now();
|
|
353
|
+
for (const [id, session] of this.replaySessions) {
|
|
354
|
+
if (now - session.lastAccessTime >= this.timeoutMs) {
|
|
355
|
+
console.log(
|
|
356
|
+
`[EVICTION] Evicting idle replay session: ${id} (idle for ${Math.round((now - session.lastAccessTime) / 1e3)}s)`
|
|
357
|
+
);
|
|
358
|
+
this.replaySessions.delete(id);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (this.replaySessions.size === 0) {
|
|
362
|
+
this.stopSessionEvictionTimer();
|
|
363
|
+
}
|
|
364
|
+
}, CHECK_INTERVAL_MS);
|
|
365
|
+
this.sessionEvictionTimer.unref();
|
|
351
366
|
}
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (req.method === "POST") {
|
|
357
|
-
const body = await readRequestBody(req);
|
|
358
|
-
console.log(`MODE CHANGE (${req.method})`, body);
|
|
359
|
-
return JSON.parse(body);
|
|
367
|
+
stopSessionEvictionTimer() {
|
|
368
|
+
if (this.sessionEvictionTimer) {
|
|
369
|
+
clearInterval(this.sessionEvictionTimer);
|
|
370
|
+
this.sessionEvictionTimer = null;
|
|
360
371
|
}
|
|
361
|
-
|
|
372
|
+
}
|
|
373
|
+
async parseControlBody(req) {
|
|
374
|
+
const body = await readRequestBody(req);
|
|
375
|
+
console.log(`MODE CHANGE (${req.method})`, body);
|
|
376
|
+
return JSON.parse(body);
|
|
362
377
|
}
|
|
363
378
|
async handleControlRequest(req, res) {
|
|
379
|
+
if (req.method === "HEAD") {
|
|
380
|
+
res.writeHead(HTTP_STATUS_OK);
|
|
381
|
+
res.end();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
364
384
|
if (req.method === "GET") {
|
|
365
385
|
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
366
386
|
recordingsDir: this.recordingsDir,
|
|
@@ -369,8 +389,11 @@ var ProxyServer = class {
|
|
|
369
389
|
});
|
|
370
390
|
return;
|
|
371
391
|
}
|
|
392
|
+
await this.handleControlPost(req, res);
|
|
393
|
+
}
|
|
394
|
+
async handleControlPost(req, res) {
|
|
372
395
|
try {
|
|
373
|
-
const data = await this.
|
|
396
|
+
const data = await this.parseControlBody(req);
|
|
374
397
|
const { mode, id, timeout: requestTimeout, cleanup } = data;
|
|
375
398
|
if (cleanup && id) {
|
|
376
399
|
await this.cleanupSession(id);
|
|
@@ -381,29 +404,7 @@ var ProxyServer = class {
|
|
|
381
404
|
});
|
|
382
405
|
return;
|
|
383
406
|
}
|
|
384
|
-
|
|
385
|
-
throw new Error(
|
|
386
|
-
"Mode parameter is required when cleanup is not specified"
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
390
|
-
this.clearModeTimeout();
|
|
391
|
-
await this.switchMode(mode, id);
|
|
392
|
-
this.setupModeTimeout(timeout);
|
|
393
|
-
if (mode === Modes.replay && id) {
|
|
394
|
-
res.setHeader(
|
|
395
|
-
"Set-Cookie",
|
|
396
|
-
`proxy-recording-id=${encodeURIComponent(id)}; HttpOnly; Path=/; SameSite=Lax`
|
|
397
|
-
);
|
|
398
|
-
console.log(`[CONCURRENT REPLAY] Set cookie for recording: ${id}`);
|
|
399
|
-
}
|
|
400
|
-
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
401
|
-
success: true,
|
|
402
|
-
mode: this.mode,
|
|
403
|
-
id: this.recordingId || this.replayId,
|
|
404
|
-
timeout,
|
|
405
|
-
recordingsDir: this.recordingsDir
|
|
406
|
-
});
|
|
407
|
+
await this.applyModeChange(res, mode, id, requestTimeout);
|
|
407
408
|
} catch (error) {
|
|
408
409
|
console.error("Control request error:", error);
|
|
409
410
|
sendJsonResponse(res, HTTP_STATUS_BAD_REQUEST, {
|
|
@@ -411,6 +412,31 @@ var ProxyServer = class {
|
|
|
411
412
|
});
|
|
412
413
|
}
|
|
413
414
|
}
|
|
415
|
+
async applyModeChange(res, mode, id, requestTimeout) {
|
|
416
|
+
if (!mode) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
"Mode parameter is required when cleanup is not specified"
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const timeout2 = requestTimeout ?? this.timeoutMs;
|
|
422
|
+
this.clearModeTimeout();
|
|
423
|
+
await this.switchMode(mode, id);
|
|
424
|
+
this.setupModeTimeout(timeout2);
|
|
425
|
+
if (mode === Modes.replay && id) {
|
|
426
|
+
res.setHeader(
|
|
427
|
+
"Set-Cookie",
|
|
428
|
+
`proxy-recording-id=${encodeURIComponent(id)}; HttpOnly; Path=/; SameSite=Lax`
|
|
429
|
+
);
|
|
430
|
+
console.log(`[CONCURRENT REPLAY] Set cookie for recording: ${id}`);
|
|
431
|
+
}
|
|
432
|
+
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
433
|
+
success: true,
|
|
434
|
+
mode: this.mode,
|
|
435
|
+
id: this.recordingId || this.replayId,
|
|
436
|
+
timeout: timeout2,
|
|
437
|
+
recordingsDir: this.recordingsDir
|
|
438
|
+
});
|
|
439
|
+
}
|
|
414
440
|
clearModeTimeout() {
|
|
415
441
|
clearTimeout(this.modeTimeout || 0);
|
|
416
442
|
this.modeTimeout = null;
|
|
@@ -450,7 +476,7 @@ var ProxyServer = class {
|
|
|
450
476
|
this.recordingId = null;
|
|
451
477
|
this.replayId = null;
|
|
452
478
|
this.currentSession = null;
|
|
453
|
-
|
|
479
|
+
this.clearModeTimeout();
|
|
454
480
|
console.log("Switched to transparent mode");
|
|
455
481
|
}
|
|
456
482
|
switchToRecordMode(id) {
|
|
@@ -480,14 +506,14 @@ var ProxyServer = class {
|
|
|
480
506
|
}
|
|
481
507
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
482
508
|
}
|
|
483
|
-
setupModeTimeout(
|
|
484
|
-
|
|
509
|
+
setupModeTimeout(timeout2) {
|
|
510
|
+
this.clearModeTimeout();
|
|
485
511
|
this.modeTimeout = setTimeout(async () => {
|
|
486
512
|
console.log("Timeout reached, switching back to transparent mode");
|
|
487
513
|
await this.saveCurrentSession();
|
|
488
514
|
this.switchToTransparentMode();
|
|
489
515
|
this.modeTimeout = null;
|
|
490
|
-
},
|
|
516
|
+
}, timeout2);
|
|
491
517
|
}
|
|
492
518
|
async flushPendingRecordings() {
|
|
493
519
|
if (this.flushPromise) {
|
|
@@ -606,11 +632,10 @@ var ProxyServer = class {
|
|
|
606
632
|
try {
|
|
607
633
|
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
608
634
|
if (!sessionState.loadedSession) {
|
|
609
|
-
|
|
610
|
-
`Recording session file not found: ${filePath}`
|
|
635
|
+
throw Object.assign(
|
|
636
|
+
new Error(`Recording session file not found: ${filePath}`),
|
|
637
|
+
{ code: "ENOENT" }
|
|
611
638
|
);
|
|
612
|
-
error.code = "ENOENT";
|
|
613
|
-
throw error;
|
|
614
639
|
}
|
|
615
640
|
const servedForThisKey = this.getServedTracker(sessionState, key);
|
|
616
641
|
const host = req.headers.host || "unknown";
|
|
@@ -705,16 +730,16 @@ var ProxyServer = class {
|
|
|
705
730
|
res.end();
|
|
706
731
|
}
|
|
707
732
|
async handleProxyRequest(req, res) {
|
|
708
|
-
const
|
|
709
|
-
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${
|
|
733
|
+
const target2 = this.target;
|
|
734
|
+
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target2}`);
|
|
710
735
|
if (this.mode === Modes.record) {
|
|
711
|
-
await this.recordAndProxyRequest(req, res,
|
|
736
|
+
await this.recordAndProxyRequest(req, res, target2);
|
|
712
737
|
} else {
|
|
713
|
-
this.proxy.web(req, res, { target });
|
|
738
|
+
this.proxy.web(req, res, { target: target2 });
|
|
714
739
|
}
|
|
715
740
|
}
|
|
716
741
|
// Note: streaming requests are buffered before proxying; streaming passthrough is not yet implemented
|
|
717
|
-
async recordAndProxyRequest(req, res,
|
|
742
|
+
async recordAndProxyRequest(req, res, target2) {
|
|
718
743
|
if (!this.currentSession) {
|
|
719
744
|
return;
|
|
720
745
|
}
|
|
@@ -742,7 +767,7 @@ var ProxyServer = class {
|
|
|
742
767
|
console.error("Error buffering request:", error);
|
|
743
768
|
}
|
|
744
769
|
const requestBody = Buffer.concat(chunks).toString("utf8");
|
|
745
|
-
const targetUrl = new URL(
|
|
770
|
+
const targetUrl = new URL(target2);
|
|
746
771
|
const isHttps = targetUrl.protocol === "https:";
|
|
747
772
|
const requestModule = isHttps ? https : http;
|
|
748
773
|
const defaultPort = isHttps ? 443 : 80;
|
|
@@ -831,15 +856,15 @@ var ProxyServer = class {
|
|
|
831
856
|
this.handleReplayWebSocket(req, socket);
|
|
832
857
|
return;
|
|
833
858
|
}
|
|
834
|
-
const
|
|
835
|
-
console.log(`[${this.mode}] WebSocket upgrade ${req.url} -> ${
|
|
859
|
+
const target2 = this.target;
|
|
860
|
+
console.log(`[${this.mode}] WebSocket upgrade ${req.url} -> ${target2}`);
|
|
836
861
|
if (this.mode === Modes.record) {
|
|
837
|
-
this.handleRecordWebSocket(req, socket, head,
|
|
862
|
+
this.handleRecordWebSocket(req, socket, head, target2);
|
|
838
863
|
} else {
|
|
839
|
-
this.proxy.ws(req, socket, head, { target });
|
|
864
|
+
this.proxy.ws(req, socket, head, { target: target2 });
|
|
840
865
|
}
|
|
841
866
|
}
|
|
842
|
-
handleRecordWebSocket(req, clientSocket, head,
|
|
867
|
+
handleRecordWebSocket(req, clientSocket, head, target2) {
|
|
843
868
|
const url = req.url || "/";
|
|
844
869
|
const key = `WS_${url.replaceAll("/", "_")}`;
|
|
845
870
|
const wsRecording = {
|
|
@@ -851,7 +876,7 @@ var ProxyServer = class {
|
|
|
851
876
|
if (this.currentSession) {
|
|
852
877
|
this.currentSession.websocketRecordings.push(wsRecording);
|
|
853
878
|
}
|
|
854
|
-
const backendWsUrl = `${
|
|
879
|
+
const backendWsUrl = `${target2.replace("http", "ws")}${url}`;
|
|
855
880
|
const backendWs = new WebSocket(backendWsUrl);
|
|
856
881
|
const wss = new WebSocketServer({ noServer: true });
|
|
857
882
|
backendWs.on("open", () => {
|
|
@@ -904,11 +929,12 @@ var ProxyServer = class {
|
|
|
904
929
|
console.error("WebSocket server error:", err);
|
|
905
930
|
});
|
|
906
931
|
}
|
|
907
|
-
handleReplayWebSocket(req, socket) {
|
|
932
|
+
async handleReplayWebSocket(req, socket) {
|
|
908
933
|
const url = req.url || "/";
|
|
909
934
|
const key = `WS_${url.replaceAll("/", "_")}`;
|
|
910
935
|
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
911
|
-
|
|
936
|
+
try {
|
|
937
|
+
const session = await loadRecordingSession(filePath);
|
|
912
938
|
const wsRecording = session.websocketRecordings.find(
|
|
913
939
|
(r) => r.key === key
|
|
914
940
|
);
|
|
@@ -974,25 +1000,25 @@ var ProxyServer = class {
|
|
|
974
1000
|
console.log("Replay WebSocket closed");
|
|
975
1001
|
});
|
|
976
1002
|
});
|
|
977
|
-
}
|
|
1003
|
+
} catch (error) {
|
|
978
1004
|
console.error("Replay error:", error);
|
|
979
1005
|
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
980
1006
|
socket.destroy();
|
|
981
|
-
}
|
|
1007
|
+
}
|
|
982
1008
|
}
|
|
983
1009
|
logServerStartup(port2) {
|
|
984
1010
|
console.log(`Proxy server running on http://localhost:${port2}`);
|
|
985
1011
|
console.log(`Mode: ${this.mode}`);
|
|
986
|
-
console.log(`
|
|
1012
|
+
console.log(`Target: ${this.target}`);
|
|
987
1013
|
console.log(
|
|
988
1014
|
`Control endpoint: http://localhost:${port2}${CONTROL_ENDPOINT}`
|
|
989
1015
|
);
|
|
990
1016
|
}
|
|
991
1017
|
};
|
|
992
1018
|
|
|
993
|
-
// src/proxy.ts
|
|
994
|
-
var {
|
|
995
|
-
var proxy = new ProxyServer(
|
|
1019
|
+
// src/proxy-cli.ts
|
|
1020
|
+
var { target, port, recordingsDir, timeout } = parseCliArgs();
|
|
1021
|
+
var proxy = new ProxyServer(target, recordingsDir, timeout);
|
|
996
1022
|
await proxy.init();
|
|
997
1023
|
proxy.listen(port);
|
|
998
1024
|
console.log(`Recordings will be saved to: ${recordingsDir}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-proxy-recorder",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -38,13 +38,18 @@
|
|
|
38
38
|
"LICENSE"
|
|
39
39
|
],
|
|
40
40
|
"pnpm": {
|
|
41
|
-
"onlyBuiltDependencies": ["esbuild"]
|
|
41
|
+
"onlyBuiltDependencies": ["esbuild", "sharp"]
|
|
42
42
|
},
|
|
43
43
|
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd",
|
|
44
44
|
"scripts": {
|
|
45
45
|
"start": "node dist/proxy.js",
|
|
46
46
|
"dev": "tsx src/proxy.ts",
|
|
47
47
|
"build": "tsup",
|
|
48
|
+
"example:dev": "pnpm --filter example-nextjs16 dev",
|
|
49
|
+
"example:build": "pnpm --filter example-nextjs16 build",
|
|
50
|
+
"example:services": "pnpm build && pnpm --filter example-nextjs16 start:all",
|
|
51
|
+
"example:test:e2e": "pnpm --filter example-nextjs16 test:e2e",
|
|
52
|
+
"example:test:e2e:record": "pnpm --filter example-nextjs16 test:e2e:record",
|
|
48
53
|
"prepublish": "pnpm run build && pnpm run test:run && pnpm run lint",
|
|
49
54
|
"lint": "eslint src --ext .ts",
|
|
50
55
|
"lint:fix": "eslint src --ext .ts --fix",
|
|
@@ -95,6 +100,7 @@
|
|
|
95
100
|
"@playwright/test": ">=1.0.0"
|
|
96
101
|
},
|
|
97
102
|
"devDependencies": {
|
|
103
|
+
"@playwright/test": "^1.59.1",
|
|
98
104
|
"@types/http-proxy": "^1.17.15",
|
|
99
105
|
"@types/node": "^22.0.0",
|
|
100
106
|
"@types/ws": "^8.18.1",
|