test-proxy-recorder 0.1.7 → 0.1.9
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/dist/{index-CBjvm5rb.d.cts → index-CG-XcFDa.d.cts} +1 -0
- package/dist/{index-CBjvm5rb.d.ts → index-CG-XcFDa.d.ts} +1 -0
- package/dist/index.cjs +69 -51
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +68 -51
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/proxy.js +68 -51
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -6,6 +6,7 @@ var https = require('https');
|
|
|
6
6
|
var httpProxy = require('http-proxy');
|
|
7
7
|
var ws = require('ws');
|
|
8
8
|
var path = require('path');
|
|
9
|
+
var crypto = require('crypto');
|
|
9
10
|
var filenamify = require('filenamify');
|
|
10
11
|
|
|
11
12
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -15,6 +16,7 @@ var http__default = /*#__PURE__*/_interopDefault(http);
|
|
|
15
16
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
16
17
|
var httpProxy__default = /*#__PURE__*/_interopDefault(httpProxy);
|
|
17
18
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
19
|
+
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
18
20
|
var filenamify__default = /*#__PURE__*/_interopDefault(filenamify);
|
|
19
21
|
|
|
20
22
|
// src/ProxyServer.ts
|
|
@@ -53,7 +55,6 @@ async function saveRecordingSession(recordingsDir, session) {
|
|
|
53
55
|
`Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
54
56
|
);
|
|
55
57
|
}
|
|
56
|
-
var QUERY_HASH_LENGTH = 8;
|
|
57
58
|
function getReqID(req) {
|
|
58
59
|
const urlParts = req.url.split("?");
|
|
59
60
|
const pathname = urlParts[0];
|
|
@@ -68,7 +69,7 @@ function generateQueryHash(query) {
|
|
|
68
69
|
if (!query) {
|
|
69
70
|
return "";
|
|
70
71
|
}
|
|
71
|
-
const hash =
|
|
72
|
+
const hash = crypto__default.default.createHash("md5").update(query).digest("hex").slice(0, 16);
|
|
72
73
|
return `_${hash}`;
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -101,11 +102,14 @@ var ProxyServer = class {
|
|
|
101
102
|
// Track sequence per request key
|
|
102
103
|
replaySequenceMap;
|
|
103
104
|
// Track replay position per request key
|
|
105
|
+
recordingIdCounter;
|
|
106
|
+
// Unique ID for each recording entry
|
|
104
107
|
constructor(targets, recordingsDir) {
|
|
105
108
|
this.targets = targets;
|
|
106
109
|
this.currentTargetIndex = 0;
|
|
107
110
|
this.mode = Modes.transparent;
|
|
108
111
|
this.recordingId = null;
|
|
112
|
+
this.recordingIdCounter = 0;
|
|
109
113
|
this.replayId = null;
|
|
110
114
|
this.modeTimeout = null;
|
|
111
115
|
this.currentSession = null;
|
|
@@ -293,7 +297,6 @@ var ProxyServer = class {
|
|
|
293
297
|
}
|
|
294
298
|
async saveCurrentSession(filterIncomplete = false) {
|
|
295
299
|
if (!this.currentSession) {
|
|
296
|
-
console.log("No current session to save");
|
|
297
300
|
return;
|
|
298
301
|
}
|
|
299
302
|
if (filterIncomplete) {
|
|
@@ -301,9 +304,6 @@ var ProxyServer = class {
|
|
|
301
304
|
(r) => !r.response
|
|
302
305
|
).length;
|
|
303
306
|
if (incompleteCount > 0) {
|
|
304
|
-
console.log(
|
|
305
|
-
`Removing ${incompleteCount} incomplete recording(s) without responses`
|
|
306
|
-
);
|
|
307
307
|
this.currentSession.recordings = this.currentSession.recordings.filter(
|
|
308
308
|
(r) => r.response
|
|
309
309
|
);
|
|
@@ -316,10 +316,14 @@ var ProxyServer = class {
|
|
|
316
316
|
}
|
|
317
317
|
saveRequestRecordSync(req, body) {
|
|
318
318
|
if (!this.currentSession) {
|
|
319
|
-
console.log("saveRequestRecordSync: No current session");
|
|
320
319
|
return;
|
|
321
320
|
}
|
|
322
321
|
const key = getReqID(req);
|
|
322
|
+
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
323
|
+
const sequence = currentSequence;
|
|
324
|
+
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
325
|
+
const recordingId = this.recordingIdCounter++;
|
|
326
|
+
req.__recordingId = recordingId;
|
|
323
327
|
const record = {
|
|
324
328
|
request: {
|
|
325
329
|
method: req.method,
|
|
@@ -329,45 +333,58 @@ var ProxyServer = class {
|
|
|
329
333
|
},
|
|
330
334
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
331
335
|
key,
|
|
332
|
-
sequence
|
|
333
|
-
|
|
336
|
+
sequence,
|
|
337
|
+
recordingId
|
|
334
338
|
};
|
|
335
339
|
this.currentSession.recordings.push(record);
|
|
336
340
|
console.log(
|
|
337
341
|
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
338
|
-
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
342
|
+
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, seq: ${sequence}, recordingId: ${recordingId}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
339
343
|
);
|
|
340
344
|
}
|
|
341
345
|
updateRequestBodySync(req, body) {
|
|
342
346
|
if (!this.currentSession) {
|
|
343
|
-
console.log("updateRequestBodySync: No current session");
|
|
344
347
|
return;
|
|
345
348
|
}
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
(
|
|
349
|
+
const recordingId = req.__recordingId;
|
|
350
|
+
if (recordingId === void 0) {
|
|
351
|
+
console.error(
|
|
352
|
+
`updateRequestBodySync: No recording ID found on request ${req.method} ${req.url}`
|
|
353
|
+
);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const record = this.currentSession.recordings.find(
|
|
357
|
+
(r) => r.recordingId === recordingId
|
|
349
358
|
);
|
|
350
359
|
if (!record) {
|
|
351
360
|
console.error(
|
|
352
|
-
`updateRequestBodySync: Could not find
|
|
361
|
+
`updateRequestBodySync: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
353
362
|
);
|
|
354
363
|
return;
|
|
355
364
|
}
|
|
356
365
|
record.request.body = body || null;
|
|
357
366
|
console.log(
|
|
358
|
-
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars)`
|
|
367
|
+
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars, recordingId: ${recordingId})`
|
|
359
368
|
);
|
|
360
369
|
}
|
|
361
370
|
async recordResponse(req, proxyRes) {
|
|
362
371
|
if (!this.currentSession) {
|
|
363
372
|
return;
|
|
364
373
|
}
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
(
|
|
374
|
+
const recordingId = req.__recordingId;
|
|
375
|
+
if (recordingId === void 0) {
|
|
376
|
+
console.error(
|
|
377
|
+
`recordResponse: No recording ID found on request ${req.method} ${req.url}`
|
|
378
|
+
);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const record = this.currentSession.recordings.find(
|
|
382
|
+
(r) => r.recordingId === recordingId
|
|
368
383
|
);
|
|
369
384
|
if (!record) {
|
|
370
|
-
console.error(
|
|
385
|
+
console.error(
|
|
386
|
+
`recordResponse: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
387
|
+
);
|
|
371
388
|
return;
|
|
372
389
|
}
|
|
373
390
|
const chunks = [];
|
|
@@ -381,35 +398,28 @@ var ProxyServer = class {
|
|
|
381
398
|
headers: proxyRes.headers,
|
|
382
399
|
body: body || null
|
|
383
400
|
};
|
|
384
|
-
console.log(
|
|
401
|
+
console.log(
|
|
402
|
+
`Recorded: ${req.method} ${req.url} (seq: ${record.sequence}, recordingId: ${recordingId})`
|
|
403
|
+
);
|
|
385
404
|
});
|
|
386
405
|
}
|
|
387
406
|
async recordResponseData(req, proxyRes, body) {
|
|
388
407
|
if (!this.currentSession) {
|
|
389
|
-
console.log("recordResponseData: No current session");
|
|
390
408
|
return false;
|
|
391
409
|
}
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
(r) => r.key === key && !r.response
|
|
395
|
-
);
|
|
396
|
-
if (!record) {
|
|
397
|
-
const host = req.headers.host || "unknown";
|
|
398
|
-
const recordsWithKey = this.currentSession.recordings.filter(
|
|
399
|
-
(r) => r.key === key
|
|
400
|
-
);
|
|
410
|
+
const recordingId = req.__recordingId;
|
|
411
|
+
if (recordingId === void 0) {
|
|
401
412
|
console.error(
|
|
402
|
-
`
|
|
403
|
-
);
|
|
404
|
-
console.error(
|
|
405
|
-
` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
|
|
413
|
+
`recordResponseData: No recording ID found on request ${req.method} ${req.url}`
|
|
406
414
|
);
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
const record = this.currentSession.recordings.find(
|
|
418
|
+
(r) => r.recordingId === recordingId
|
|
419
|
+
);
|
|
420
|
+
if (!record) {
|
|
407
421
|
console.error(
|
|
408
|
-
`
|
|
409
|
-
recordsWithKey.map((r) => ({
|
|
410
|
-
seq: r.sequence,
|
|
411
|
-
hasResponse: !!r.response
|
|
412
|
-
}))
|
|
422
|
+
`recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
413
423
|
);
|
|
414
424
|
return false;
|
|
415
425
|
}
|
|
@@ -418,12 +428,8 @@ var ProxyServer = class {
|
|
|
418
428
|
headers: proxyRes.headers,
|
|
419
429
|
body: body || null
|
|
420
430
|
};
|
|
421
|
-
record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
422
|
-
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
423
|
-
record.sequence = currentSequence;
|
|
424
|
-
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
425
431
|
console.log(
|
|
426
|
-
`recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
|
|
432
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence}, recordingId: ${recordingId})`
|
|
427
433
|
);
|
|
428
434
|
return true;
|
|
429
435
|
}
|
|
@@ -435,17 +441,29 @@ var ProxyServer = class {
|
|
|
435
441
|
const host = req.headers.host || "unknown";
|
|
436
442
|
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
|
|
437
443
|
if (recordsWithKey.length === 0) {
|
|
438
|
-
|
|
439
|
-
`No recording found for ${key} at ${req.method} ${host}${req.url}`
|
|
444
|
+
console.warn(
|
|
445
|
+
`No recording found for ${key} at ${req.method} ${host}${req.url}, returning default response`
|
|
440
446
|
);
|
|
447
|
+
const defaultResponse = req.method === "GET" ? {
|
|
448
|
+
data: [],
|
|
449
|
+
items: [],
|
|
450
|
+
results: [],
|
|
451
|
+
updated_at: "0001-01-01T00:00:00Z"
|
|
452
|
+
} : { success: true };
|
|
453
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
454
|
+
res.writeHead(HTTP_STATUS_OK, {
|
|
455
|
+
"Content-Type": "application/json",
|
|
456
|
+
...corsHeaders
|
|
457
|
+
});
|
|
458
|
+
res.end(JSON.stringify(defaultResponse));
|
|
459
|
+
return;
|
|
441
460
|
}
|
|
442
461
|
const usageCount = this.replaySequenceMap.get(key) || 0;
|
|
443
462
|
let record;
|
|
444
|
-
if (recordsWithKey.length
|
|
445
|
-
record = recordsWithKey[
|
|
463
|
+
if (usageCount < recordsWithKey.length) {
|
|
464
|
+
record = recordsWithKey[usageCount];
|
|
446
465
|
} else {
|
|
447
|
-
|
|
448
|
-
record = recordsWithKey[recordIndex];
|
|
466
|
+
record = recordsWithKey[recordsWithKey.length - 1];
|
|
449
467
|
}
|
|
450
468
|
console.log(
|
|
451
469
|
`Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-
|
|
2
|
+
export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-CG-XcFDa.cjs';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -14,6 +14,7 @@ declare class ProxyServer {
|
|
|
14
14
|
private recordingsDir;
|
|
15
15
|
private requestSequenceMap;
|
|
16
16
|
private replaySequenceMap;
|
|
17
|
+
private recordingIdCounter;
|
|
17
18
|
constructor(targets: string[], recordingsDir: string);
|
|
18
19
|
init(): Promise<void>;
|
|
19
20
|
listen(port: number): http.Server;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-
|
|
2
|
+
export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-CG-XcFDa.js';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -14,6 +14,7 @@ declare class ProxyServer {
|
|
|
14
14
|
private recordingsDir;
|
|
15
15
|
private requestSequenceMap;
|
|
16
16
|
private replaySequenceMap;
|
|
17
|
+
private recordingIdCounter;
|
|
17
18
|
constructor(targets: string[], recordingsDir: string);
|
|
18
19
|
init(): Promise<void>;
|
|
19
20
|
listen(port: number): http.Server;
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import https from 'https';
|
|
|
4
4
|
import httpProxy from 'http-proxy';
|
|
5
5
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import crypto from 'crypto';
|
|
7
8
|
import filenamify from 'filenamify';
|
|
8
9
|
|
|
9
10
|
// src/ProxyServer.ts
|
|
@@ -42,7 +43,6 @@ async function saveRecordingSession(recordingsDir, session) {
|
|
|
42
43
|
`Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
43
44
|
);
|
|
44
45
|
}
|
|
45
|
-
var QUERY_HASH_LENGTH = 8;
|
|
46
46
|
function getReqID(req) {
|
|
47
47
|
const urlParts = req.url.split("?");
|
|
48
48
|
const pathname = urlParts[0];
|
|
@@ -57,7 +57,7 @@ function generateQueryHash(query) {
|
|
|
57
57
|
if (!query) {
|
|
58
58
|
return "";
|
|
59
59
|
}
|
|
60
|
-
const hash =
|
|
60
|
+
const hash = crypto.createHash("md5").update(query).digest("hex").slice(0, 16);
|
|
61
61
|
return `_${hash}`;
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -90,11 +90,14 @@ var ProxyServer = class {
|
|
|
90
90
|
// Track sequence per request key
|
|
91
91
|
replaySequenceMap;
|
|
92
92
|
// Track replay position per request key
|
|
93
|
+
recordingIdCounter;
|
|
94
|
+
// Unique ID for each recording entry
|
|
93
95
|
constructor(targets, recordingsDir) {
|
|
94
96
|
this.targets = targets;
|
|
95
97
|
this.currentTargetIndex = 0;
|
|
96
98
|
this.mode = Modes.transparent;
|
|
97
99
|
this.recordingId = null;
|
|
100
|
+
this.recordingIdCounter = 0;
|
|
98
101
|
this.replayId = null;
|
|
99
102
|
this.modeTimeout = null;
|
|
100
103
|
this.currentSession = null;
|
|
@@ -282,7 +285,6 @@ var ProxyServer = class {
|
|
|
282
285
|
}
|
|
283
286
|
async saveCurrentSession(filterIncomplete = false) {
|
|
284
287
|
if (!this.currentSession) {
|
|
285
|
-
console.log("No current session to save");
|
|
286
288
|
return;
|
|
287
289
|
}
|
|
288
290
|
if (filterIncomplete) {
|
|
@@ -290,9 +292,6 @@ var ProxyServer = class {
|
|
|
290
292
|
(r) => !r.response
|
|
291
293
|
).length;
|
|
292
294
|
if (incompleteCount > 0) {
|
|
293
|
-
console.log(
|
|
294
|
-
`Removing ${incompleteCount} incomplete recording(s) without responses`
|
|
295
|
-
);
|
|
296
295
|
this.currentSession.recordings = this.currentSession.recordings.filter(
|
|
297
296
|
(r) => r.response
|
|
298
297
|
);
|
|
@@ -305,10 +304,14 @@ var ProxyServer = class {
|
|
|
305
304
|
}
|
|
306
305
|
saveRequestRecordSync(req, body) {
|
|
307
306
|
if (!this.currentSession) {
|
|
308
|
-
console.log("saveRequestRecordSync: No current session");
|
|
309
307
|
return;
|
|
310
308
|
}
|
|
311
309
|
const key = getReqID(req);
|
|
310
|
+
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
311
|
+
const sequence = currentSequence;
|
|
312
|
+
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
313
|
+
const recordingId = this.recordingIdCounter++;
|
|
314
|
+
req.__recordingId = recordingId;
|
|
312
315
|
const record = {
|
|
313
316
|
request: {
|
|
314
317
|
method: req.method,
|
|
@@ -318,45 +321,58 @@ var ProxyServer = class {
|
|
|
318
321
|
},
|
|
319
322
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
320
323
|
key,
|
|
321
|
-
sequence
|
|
322
|
-
|
|
324
|
+
sequence,
|
|
325
|
+
recordingId
|
|
323
326
|
};
|
|
324
327
|
this.currentSession.recordings.push(record);
|
|
325
328
|
console.log(
|
|
326
329
|
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
327
|
-
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
330
|
+
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, seq: ${sequence}, recordingId: ${recordingId}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
328
331
|
);
|
|
329
332
|
}
|
|
330
333
|
updateRequestBodySync(req, body) {
|
|
331
334
|
if (!this.currentSession) {
|
|
332
|
-
console.log("updateRequestBodySync: No current session");
|
|
333
335
|
return;
|
|
334
336
|
}
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
(
|
|
337
|
+
const recordingId = req.__recordingId;
|
|
338
|
+
if (recordingId === void 0) {
|
|
339
|
+
console.error(
|
|
340
|
+
`updateRequestBodySync: No recording ID found on request ${req.method} ${req.url}`
|
|
341
|
+
);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const record = this.currentSession.recordings.find(
|
|
345
|
+
(r) => r.recordingId === recordingId
|
|
338
346
|
);
|
|
339
347
|
if (!record) {
|
|
340
348
|
console.error(
|
|
341
|
-
`updateRequestBodySync: Could not find
|
|
349
|
+
`updateRequestBodySync: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
342
350
|
);
|
|
343
351
|
return;
|
|
344
352
|
}
|
|
345
353
|
record.request.body = body || null;
|
|
346
354
|
console.log(
|
|
347
|
-
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars)`
|
|
355
|
+
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars, recordingId: ${recordingId})`
|
|
348
356
|
);
|
|
349
357
|
}
|
|
350
358
|
async recordResponse(req, proxyRes) {
|
|
351
359
|
if (!this.currentSession) {
|
|
352
360
|
return;
|
|
353
361
|
}
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
(
|
|
362
|
+
const recordingId = req.__recordingId;
|
|
363
|
+
if (recordingId === void 0) {
|
|
364
|
+
console.error(
|
|
365
|
+
`recordResponse: No recording ID found on request ${req.method} ${req.url}`
|
|
366
|
+
);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const record = this.currentSession.recordings.find(
|
|
370
|
+
(r) => r.recordingId === recordingId
|
|
357
371
|
);
|
|
358
372
|
if (!record) {
|
|
359
|
-
console.error(
|
|
373
|
+
console.error(
|
|
374
|
+
`recordResponse: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
375
|
+
);
|
|
360
376
|
return;
|
|
361
377
|
}
|
|
362
378
|
const chunks = [];
|
|
@@ -370,35 +386,28 @@ var ProxyServer = class {
|
|
|
370
386
|
headers: proxyRes.headers,
|
|
371
387
|
body: body || null
|
|
372
388
|
};
|
|
373
|
-
console.log(
|
|
389
|
+
console.log(
|
|
390
|
+
`Recorded: ${req.method} ${req.url} (seq: ${record.sequence}, recordingId: ${recordingId})`
|
|
391
|
+
);
|
|
374
392
|
});
|
|
375
393
|
}
|
|
376
394
|
async recordResponseData(req, proxyRes, body) {
|
|
377
395
|
if (!this.currentSession) {
|
|
378
|
-
console.log("recordResponseData: No current session");
|
|
379
396
|
return false;
|
|
380
397
|
}
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
(r) => r.key === key && !r.response
|
|
384
|
-
);
|
|
385
|
-
if (!record) {
|
|
386
|
-
const host = req.headers.host || "unknown";
|
|
387
|
-
const recordsWithKey = this.currentSession.recordings.filter(
|
|
388
|
-
(r) => r.key === key
|
|
389
|
-
);
|
|
398
|
+
const recordingId = req.__recordingId;
|
|
399
|
+
if (recordingId === void 0) {
|
|
390
400
|
console.error(
|
|
391
|
-
`
|
|
392
|
-
);
|
|
393
|
-
console.error(
|
|
394
|
-
` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
|
|
401
|
+
`recordResponseData: No recording ID found on request ${req.method} ${req.url}`
|
|
395
402
|
);
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
const record = this.currentSession.recordings.find(
|
|
406
|
+
(r) => r.recordingId === recordingId
|
|
407
|
+
);
|
|
408
|
+
if (!record) {
|
|
396
409
|
console.error(
|
|
397
|
-
`
|
|
398
|
-
recordsWithKey.map((r) => ({
|
|
399
|
-
seq: r.sequence,
|
|
400
|
-
hasResponse: !!r.response
|
|
401
|
-
}))
|
|
410
|
+
`recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
402
411
|
);
|
|
403
412
|
return false;
|
|
404
413
|
}
|
|
@@ -407,12 +416,8 @@ var ProxyServer = class {
|
|
|
407
416
|
headers: proxyRes.headers,
|
|
408
417
|
body: body || null
|
|
409
418
|
};
|
|
410
|
-
record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
411
|
-
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
412
|
-
record.sequence = currentSequence;
|
|
413
|
-
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
414
419
|
console.log(
|
|
415
|
-
`recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
|
|
420
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence}, recordingId: ${recordingId})`
|
|
416
421
|
);
|
|
417
422
|
return true;
|
|
418
423
|
}
|
|
@@ -424,17 +429,29 @@ var ProxyServer = class {
|
|
|
424
429
|
const host = req.headers.host || "unknown";
|
|
425
430
|
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
|
|
426
431
|
if (recordsWithKey.length === 0) {
|
|
427
|
-
|
|
428
|
-
`No recording found for ${key} at ${req.method} ${host}${req.url}`
|
|
432
|
+
console.warn(
|
|
433
|
+
`No recording found for ${key} at ${req.method} ${host}${req.url}, returning default response`
|
|
429
434
|
);
|
|
435
|
+
const defaultResponse = req.method === "GET" ? {
|
|
436
|
+
data: [],
|
|
437
|
+
items: [],
|
|
438
|
+
results: [],
|
|
439
|
+
updated_at: "0001-01-01T00:00:00Z"
|
|
440
|
+
} : { success: true };
|
|
441
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
442
|
+
res.writeHead(HTTP_STATUS_OK, {
|
|
443
|
+
"Content-Type": "application/json",
|
|
444
|
+
...corsHeaders
|
|
445
|
+
});
|
|
446
|
+
res.end(JSON.stringify(defaultResponse));
|
|
447
|
+
return;
|
|
430
448
|
}
|
|
431
449
|
const usageCount = this.replaySequenceMap.get(key) || 0;
|
|
432
450
|
let record;
|
|
433
|
-
if (recordsWithKey.length
|
|
434
|
-
record = recordsWithKey[
|
|
451
|
+
if (usageCount < recordsWithKey.length) {
|
|
452
|
+
record = recordsWithKey[usageCount];
|
|
435
453
|
} else {
|
|
436
|
-
|
|
437
|
-
record = recordsWithKey[recordIndex];
|
|
454
|
+
record = recordsWithKey[recordsWithKey.length - 1];
|
|
438
455
|
}
|
|
439
456
|
console.log(
|
|
440
457
|
`Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@playwright/test';
|
|
2
|
-
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-
|
|
2
|
+
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-CG-XcFDa.cjs';
|
|
3
3
|
import 'node:http';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@playwright/test';
|
|
2
|
-
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-
|
|
2
|
+
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-CG-XcFDa.js';
|
|
3
3
|
import 'node:http';
|
package/dist/proxy.js
CHANGED
|
@@ -5,6 +5,7 @@ import http from 'http';
|
|
|
5
5
|
import https from 'https';
|
|
6
6
|
import httpProxy from 'http-proxy';
|
|
7
7
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
8
|
+
import crypto from 'crypto';
|
|
8
9
|
import filenamify from 'filenamify';
|
|
9
10
|
|
|
10
11
|
// src/cli.ts
|
|
@@ -76,7 +77,6 @@ async function saveRecordingSession(recordingsDir2, session) {
|
|
|
76
77
|
`Saved ${session.recordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
77
78
|
);
|
|
78
79
|
}
|
|
79
|
-
var QUERY_HASH_LENGTH = 8;
|
|
80
80
|
function getReqID(req) {
|
|
81
81
|
const urlParts = req.url.split("?");
|
|
82
82
|
const pathname = urlParts[0];
|
|
@@ -91,7 +91,7 @@ function generateQueryHash(query) {
|
|
|
91
91
|
if (!query) {
|
|
92
92
|
return "";
|
|
93
93
|
}
|
|
94
|
-
const hash =
|
|
94
|
+
const hash = crypto.createHash("md5").update(query).digest("hex").slice(0, 16);
|
|
95
95
|
return `_${hash}`;
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -124,11 +124,14 @@ var ProxyServer = class {
|
|
|
124
124
|
// Track sequence per request key
|
|
125
125
|
replaySequenceMap;
|
|
126
126
|
// Track replay position per request key
|
|
127
|
+
recordingIdCounter;
|
|
128
|
+
// Unique ID for each recording entry
|
|
127
129
|
constructor(targets2, recordingsDir2) {
|
|
128
130
|
this.targets = targets2;
|
|
129
131
|
this.currentTargetIndex = 0;
|
|
130
132
|
this.mode = Modes.transparent;
|
|
131
133
|
this.recordingId = null;
|
|
134
|
+
this.recordingIdCounter = 0;
|
|
132
135
|
this.replayId = null;
|
|
133
136
|
this.modeTimeout = null;
|
|
134
137
|
this.currentSession = null;
|
|
@@ -316,7 +319,6 @@ var ProxyServer = class {
|
|
|
316
319
|
}
|
|
317
320
|
async saveCurrentSession(filterIncomplete = false) {
|
|
318
321
|
if (!this.currentSession) {
|
|
319
|
-
console.log("No current session to save");
|
|
320
322
|
return;
|
|
321
323
|
}
|
|
322
324
|
if (filterIncomplete) {
|
|
@@ -324,9 +326,6 @@ var ProxyServer = class {
|
|
|
324
326
|
(r) => !r.response
|
|
325
327
|
).length;
|
|
326
328
|
if (incompleteCount > 0) {
|
|
327
|
-
console.log(
|
|
328
|
-
`Removing ${incompleteCount} incomplete recording(s) without responses`
|
|
329
|
-
);
|
|
330
329
|
this.currentSession.recordings = this.currentSession.recordings.filter(
|
|
331
330
|
(r) => r.response
|
|
332
331
|
);
|
|
@@ -339,10 +338,14 @@ var ProxyServer = class {
|
|
|
339
338
|
}
|
|
340
339
|
saveRequestRecordSync(req, body) {
|
|
341
340
|
if (!this.currentSession) {
|
|
342
|
-
console.log("saveRequestRecordSync: No current session");
|
|
343
341
|
return;
|
|
344
342
|
}
|
|
345
343
|
const key = getReqID(req);
|
|
344
|
+
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
345
|
+
const sequence = currentSequence;
|
|
346
|
+
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
347
|
+
const recordingId = this.recordingIdCounter++;
|
|
348
|
+
req.__recordingId = recordingId;
|
|
346
349
|
const record = {
|
|
347
350
|
request: {
|
|
348
351
|
method: req.method,
|
|
@@ -352,45 +355,58 @@ var ProxyServer = class {
|
|
|
352
355
|
},
|
|
353
356
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
354
357
|
key,
|
|
355
|
-
sequence
|
|
356
|
-
|
|
358
|
+
sequence,
|
|
359
|
+
recordingId
|
|
357
360
|
};
|
|
358
361
|
this.currentSession.recordings.push(record);
|
|
359
362
|
console.log(
|
|
360
363
|
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
361
|
-
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
364
|
+
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, seq: ${sequence}, recordingId: ${recordingId}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
362
365
|
);
|
|
363
366
|
}
|
|
364
367
|
updateRequestBodySync(req, body) {
|
|
365
368
|
if (!this.currentSession) {
|
|
366
|
-
console.log("updateRequestBodySync: No current session");
|
|
367
369
|
return;
|
|
368
370
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
(
|
|
371
|
+
const recordingId = req.__recordingId;
|
|
372
|
+
if (recordingId === void 0) {
|
|
373
|
+
console.error(
|
|
374
|
+
`updateRequestBodySync: No recording ID found on request ${req.method} ${req.url}`
|
|
375
|
+
);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const record = this.currentSession.recordings.find(
|
|
379
|
+
(r) => r.recordingId === recordingId
|
|
372
380
|
);
|
|
373
381
|
if (!record) {
|
|
374
382
|
console.error(
|
|
375
|
-
`updateRequestBodySync: Could not find
|
|
383
|
+
`updateRequestBodySync: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
376
384
|
);
|
|
377
385
|
return;
|
|
378
386
|
}
|
|
379
387
|
record.request.body = body || null;
|
|
380
388
|
console.log(
|
|
381
|
-
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars)`
|
|
389
|
+
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars, recordingId: ${recordingId})`
|
|
382
390
|
);
|
|
383
391
|
}
|
|
384
392
|
async recordResponse(req, proxyRes) {
|
|
385
393
|
if (!this.currentSession) {
|
|
386
394
|
return;
|
|
387
395
|
}
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
(
|
|
396
|
+
const recordingId = req.__recordingId;
|
|
397
|
+
if (recordingId === void 0) {
|
|
398
|
+
console.error(
|
|
399
|
+
`recordResponse: No recording ID found on request ${req.method} ${req.url}`
|
|
400
|
+
);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const record = this.currentSession.recordings.find(
|
|
404
|
+
(r) => r.recordingId === recordingId
|
|
391
405
|
);
|
|
392
406
|
if (!record) {
|
|
393
|
-
console.error(
|
|
407
|
+
console.error(
|
|
408
|
+
`recordResponse: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
409
|
+
);
|
|
394
410
|
return;
|
|
395
411
|
}
|
|
396
412
|
const chunks = [];
|
|
@@ -404,35 +420,28 @@ var ProxyServer = class {
|
|
|
404
420
|
headers: proxyRes.headers,
|
|
405
421
|
body: body || null
|
|
406
422
|
};
|
|
407
|
-
console.log(
|
|
423
|
+
console.log(
|
|
424
|
+
`Recorded: ${req.method} ${req.url} (seq: ${record.sequence}, recordingId: ${recordingId})`
|
|
425
|
+
);
|
|
408
426
|
});
|
|
409
427
|
}
|
|
410
428
|
async recordResponseData(req, proxyRes, body) {
|
|
411
429
|
if (!this.currentSession) {
|
|
412
|
-
console.log("recordResponseData: No current session");
|
|
413
430
|
return false;
|
|
414
431
|
}
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
(r) => r.key === key && !r.response
|
|
418
|
-
);
|
|
419
|
-
if (!record) {
|
|
420
|
-
const host = req.headers.host || "unknown";
|
|
421
|
-
const recordsWithKey = this.currentSession.recordings.filter(
|
|
422
|
-
(r) => r.key === key
|
|
423
|
-
);
|
|
432
|
+
const recordingId = req.__recordingId;
|
|
433
|
+
if (recordingId === void 0) {
|
|
424
434
|
console.error(
|
|
425
|
-
`
|
|
426
|
-
);
|
|
427
|
-
console.error(
|
|
428
|
-
` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
|
|
435
|
+
`recordResponseData: No recording ID found on request ${req.method} ${req.url}`
|
|
429
436
|
);
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
const record = this.currentSession.recordings.find(
|
|
440
|
+
(r) => r.recordingId === recordingId
|
|
441
|
+
);
|
|
442
|
+
if (!record) {
|
|
430
443
|
console.error(
|
|
431
|
-
`
|
|
432
|
-
recordsWithKey.map((r) => ({
|
|
433
|
-
seq: r.sequence,
|
|
434
|
-
hasResponse: !!r.response
|
|
435
|
-
}))
|
|
444
|
+
`recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
436
445
|
);
|
|
437
446
|
return false;
|
|
438
447
|
}
|
|
@@ -441,12 +450,8 @@ var ProxyServer = class {
|
|
|
441
450
|
headers: proxyRes.headers,
|
|
442
451
|
body: body || null
|
|
443
452
|
};
|
|
444
|
-
record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
445
|
-
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
446
|
-
record.sequence = currentSequence;
|
|
447
|
-
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
448
453
|
console.log(
|
|
449
|
-
`recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
|
|
454
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence}, recordingId: ${recordingId})`
|
|
450
455
|
);
|
|
451
456
|
return true;
|
|
452
457
|
}
|
|
@@ -458,17 +463,29 @@ var ProxyServer = class {
|
|
|
458
463
|
const host = req.headers.host || "unknown";
|
|
459
464
|
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
|
|
460
465
|
if (recordsWithKey.length === 0) {
|
|
461
|
-
|
|
462
|
-
`No recording found for ${key} at ${req.method} ${host}${req.url}`
|
|
466
|
+
console.warn(
|
|
467
|
+
`No recording found for ${key} at ${req.method} ${host}${req.url}, returning default response`
|
|
463
468
|
);
|
|
469
|
+
const defaultResponse = req.method === "GET" ? {
|
|
470
|
+
data: [],
|
|
471
|
+
items: [],
|
|
472
|
+
results: [],
|
|
473
|
+
updated_at: "0001-01-01T00:00:00Z"
|
|
474
|
+
} : { success: true };
|
|
475
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
476
|
+
res.writeHead(HTTP_STATUS_OK, {
|
|
477
|
+
"Content-Type": "application/json",
|
|
478
|
+
...corsHeaders
|
|
479
|
+
});
|
|
480
|
+
res.end(JSON.stringify(defaultResponse));
|
|
481
|
+
return;
|
|
464
482
|
}
|
|
465
483
|
const usageCount = this.replaySequenceMap.get(key) || 0;
|
|
466
484
|
let record;
|
|
467
|
-
if (recordsWithKey.length
|
|
468
|
-
record = recordsWithKey[
|
|
485
|
+
if (usageCount < recordsWithKey.length) {
|
|
486
|
+
record = recordsWithKey[usageCount];
|
|
469
487
|
} else {
|
|
470
|
-
|
|
471
|
-
record = recordsWithKey[recordIndex];
|
|
488
|
+
record = recordsWithKey[recordsWithKey.length - 1];
|
|
472
489
|
}
|
|
473
490
|
console.log(
|
|
474
491
|
`Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-proxy-recorder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|