test-proxy-recorder 0.1.8 → 0.1.10
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 +2 -2
- package/dist/{index-CBjvm5rb.d.cts → index-CjM3evKb.d.cts} +1 -1
- package/dist/{index-CBjvm5rb.d.ts → index-CjM3evKb.d.ts} +1 -1
- package/dist/index.cjs +184 -81
- package/dist/index.d.cts +16 -3
- package/dist/index.d.ts +16 -3
- package/dist/index.mjs +183 -81
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/proxy.js +183 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ Create `e2e/global-teardown.ts`:
|
|
|
102
102
|
import { setProxyMode } from 'test-proxy-recorder';
|
|
103
103
|
|
|
104
104
|
async function globalTeardown() {
|
|
105
|
-
await setProxyMode('transparent');
|
|
105
|
+
await setProxyMode('transparent').catch(err => { console.error(err) });
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
export default globalTeardown;
|
|
@@ -247,7 +247,7 @@ Create `e2e/global-teardown.ts`:
|
|
|
247
247
|
import { setProxyMode } from 'test-proxy-recorder';
|
|
248
248
|
|
|
249
249
|
async function globalTeardown() {
|
|
250
|
-
await setProxyMode('transparent');
|
|
250
|
+
await setProxyMode('transparent').catch(err => { console.error(err) });
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
export default globalTeardown;
|
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
|
|
|
@@ -97,24 +98,25 @@ var ProxyServer = class {
|
|
|
97
98
|
proxy;
|
|
98
99
|
currentSession;
|
|
99
100
|
recordingsDir;
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
// Track replay
|
|
101
|
+
recordingIdCounter;
|
|
102
|
+
// Unique ID for each recording entry
|
|
103
|
+
replaySessions;
|
|
104
|
+
// Track multiple concurrent replay sessions by recording ID
|
|
104
105
|
constructor(targets, recordingsDir) {
|
|
105
106
|
this.targets = targets;
|
|
106
107
|
this.currentTargetIndex = 0;
|
|
107
108
|
this.mode = Modes.transparent;
|
|
108
109
|
this.recordingId = null;
|
|
110
|
+
this.recordingIdCounter = 0;
|
|
109
111
|
this.replayId = null;
|
|
110
112
|
this.modeTimeout = null;
|
|
111
113
|
this.currentSession = null;
|
|
112
114
|
this.recordingsDir = recordingsDir;
|
|
113
|
-
this.
|
|
114
|
-
this.replaySequenceMap = /* @__PURE__ */ new Map();
|
|
115
|
+
this.replaySessions = /* @__PURE__ */ new Map();
|
|
115
116
|
this.proxy = httpProxy__default.default.createProxyServer({
|
|
116
117
|
secure: false,
|
|
117
|
-
changeOrigin: true
|
|
118
|
+
changeOrigin: true,
|
|
119
|
+
ws: true
|
|
118
120
|
});
|
|
119
121
|
this.setupProxyEventHandlers();
|
|
120
122
|
}
|
|
@@ -182,6 +184,43 @@ var ProxyServer = class {
|
|
|
182
184
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
183
185
|
return target;
|
|
184
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Extract recording ID from request cookie
|
|
189
|
+
* Used for concurrent replay session routing
|
|
190
|
+
* @param req The incoming HTTP request
|
|
191
|
+
* @returns The recording ID from cookie, or null if not found
|
|
192
|
+
*/
|
|
193
|
+
getRecordingIdFromCookie(req) {
|
|
194
|
+
const cookies = req.headers.cookie;
|
|
195
|
+
if (!cookies) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const match = cookies.match(/proxy-recording-id=([^;]+)/);
|
|
199
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get or create a replay session state for a given recording ID
|
|
203
|
+
* @param recordingId The recording ID to get/create session for
|
|
204
|
+
* @returns The replay session state
|
|
205
|
+
*/
|
|
206
|
+
getOrCreateReplaySession(recordingId) {
|
|
207
|
+
let session = this.replaySessions.get(recordingId);
|
|
208
|
+
if (session) {
|
|
209
|
+
session.lastAccessTime = Date.now();
|
|
210
|
+
} else {
|
|
211
|
+
session = {
|
|
212
|
+
recordingId,
|
|
213
|
+
servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
|
|
214
|
+
loadedSession: null,
|
|
215
|
+
lastAccessTime: Date.now()
|
|
216
|
+
};
|
|
217
|
+
this.replaySessions.set(recordingId, session);
|
|
218
|
+
console.log(
|
|
219
|
+
`[CONCURRENT REPLAY] Created new session for recording: ${recordingId}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return session;
|
|
223
|
+
}
|
|
185
224
|
parseGetParams(req) {
|
|
186
225
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
187
226
|
const mode = url.searchParams.get("mode");
|
|
@@ -198,16 +237,25 @@ var ProxyServer = class {
|
|
|
198
237
|
let data;
|
|
199
238
|
if (req.method === "GET") {
|
|
200
239
|
data = this.parseGetParams(req);
|
|
201
|
-
} else {
|
|
240
|
+
} else if (req.method === "POST") {
|
|
202
241
|
const body = await readRequestBody(req);
|
|
203
|
-
console.log(
|
|
242
|
+
console.log(`MODE CHANGE (${req.method})`, body);
|
|
204
243
|
data = JSON.parse(body);
|
|
244
|
+
} else {
|
|
245
|
+
return;
|
|
205
246
|
}
|
|
206
247
|
const { mode, id, timeout: requestTimeout } = data;
|
|
207
248
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
208
249
|
this.clearModeTimeout();
|
|
209
250
|
await this.switchMode(mode, id);
|
|
210
251
|
this.setupModeTimeout(timeout);
|
|
252
|
+
if (mode === Modes.replay && id) {
|
|
253
|
+
res.setHeader(
|
|
254
|
+
"Set-Cookie",
|
|
255
|
+
`proxy-recording-id=${encodeURIComponent(id)}; HttpOnly; Path=/; SameSite=Lax`
|
|
256
|
+
);
|
|
257
|
+
console.log(`[CONCURRENT REPLAY] Set cookie for recording: ${id}`);
|
|
258
|
+
}
|
|
211
259
|
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
212
260
|
success: true,
|
|
213
261
|
mode: this.mode,
|
|
@@ -222,14 +270,12 @@ var ProxyServer = class {
|
|
|
222
270
|
}
|
|
223
271
|
}
|
|
224
272
|
clearModeTimeout() {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
this.modeTimeout = null;
|
|
228
|
-
}
|
|
273
|
+
clearTimeout(this.modeTimeout || 0);
|
|
274
|
+
this.modeTimeout = null;
|
|
229
275
|
}
|
|
230
276
|
async switchMode(mode, id) {
|
|
231
|
-
|
|
232
|
-
|
|
277
|
+
console.log(`Switching to ${mode.toUpperCase()} mode`);
|
|
278
|
+
if (this.currentSession && this.mode === Modes.record) {
|
|
233
279
|
await this.saveCurrentSession(true);
|
|
234
280
|
console.log("Session saved, continuing with mode switch");
|
|
235
281
|
}
|
|
@@ -239,11 +285,17 @@ var ProxyServer = class {
|
|
|
239
285
|
break;
|
|
240
286
|
}
|
|
241
287
|
case Modes.record: {
|
|
288
|
+
if (!id) {
|
|
289
|
+
throw new Error("Record ID is required");
|
|
290
|
+
}
|
|
242
291
|
this.switchToRecordMode(id);
|
|
243
292
|
break;
|
|
244
293
|
}
|
|
245
294
|
case Modes.replay: {
|
|
246
|
-
|
|
295
|
+
if (!id) {
|
|
296
|
+
throw new Error("Replay ID is required");
|
|
297
|
+
}
|
|
298
|
+
await this.switchToReplayMode(id);
|
|
247
299
|
break;
|
|
248
300
|
}
|
|
249
301
|
default: {
|
|
@@ -260,36 +312,33 @@ var ProxyServer = class {
|
|
|
260
312
|
console.log("Switched to transparent mode");
|
|
261
313
|
}
|
|
262
314
|
switchToRecordMode(id) {
|
|
263
|
-
if (!id) {
|
|
264
|
-
throw new Error("Record ID is required");
|
|
265
|
-
}
|
|
266
315
|
this.mode = Modes.record;
|
|
267
316
|
this.recordingId = id;
|
|
268
317
|
this.replayId = null;
|
|
269
318
|
this.currentSession = { id, recordings: [], websocketRecordings: [] };
|
|
270
|
-
this.requestSequenceMap.clear();
|
|
271
319
|
console.log(`Switched to record mode with ID: ${id}`);
|
|
272
320
|
}
|
|
273
|
-
switchToReplayMode(id) {
|
|
274
|
-
if (!id) {
|
|
275
|
-
throw new Error("Replay ID is required");
|
|
276
|
-
}
|
|
321
|
+
async switchToReplayMode(id) {
|
|
277
322
|
this.mode = Modes.replay;
|
|
278
323
|
this.replayId = id;
|
|
279
324
|
this.recordingId = null;
|
|
280
325
|
this.currentSession = null;
|
|
281
|
-
this.
|
|
326
|
+
const session = this.replaySessions.get(id);
|
|
327
|
+
if (session) {
|
|
328
|
+
session.servedRecordingIdsByKey.clear();
|
|
329
|
+
console.log(`Reset served recordings tracker for session: ${id}`);
|
|
330
|
+
} else {
|
|
331
|
+
this.getOrCreateReplaySession(id);
|
|
332
|
+
}
|
|
282
333
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
283
334
|
}
|
|
284
335
|
setupModeTimeout(timeout) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}, timeout);
|
|
292
|
-
}
|
|
336
|
+
this.modeTimeout = setTimeout(async () => {
|
|
337
|
+
console.log("Timeout reached, switching back to transparent mode");
|
|
338
|
+
await this.saveCurrentSession(true);
|
|
339
|
+
this.switchToTransparentMode();
|
|
340
|
+
this.modeTimeout = null;
|
|
341
|
+
}, timeout);
|
|
293
342
|
}
|
|
294
343
|
async saveCurrentSession(filterIncomplete = false) {
|
|
295
344
|
if (!this.currentSession) {
|
|
@@ -315,6 +364,8 @@ var ProxyServer = class {
|
|
|
315
364
|
return;
|
|
316
365
|
}
|
|
317
366
|
const key = getReqID(req);
|
|
367
|
+
const recordingId = this.recordingIdCounter++;
|
|
368
|
+
req.__recordingId = recordingId;
|
|
318
369
|
const record = {
|
|
319
370
|
request: {
|
|
320
371
|
method: req.method,
|
|
@@ -324,44 +375,57 @@ var ProxyServer = class {
|
|
|
324
375
|
},
|
|
325
376
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
326
377
|
key,
|
|
327
|
-
|
|
328
|
-
// Temporary, will be set when response arrives
|
|
378
|
+
recordingId
|
|
329
379
|
};
|
|
330
380
|
this.currentSession.recordings.push(record);
|
|
331
381
|
console.log(
|
|
332
382
|
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
333
|
-
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
383
|
+
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, recordingId: ${recordingId}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
334
384
|
);
|
|
335
385
|
}
|
|
336
386
|
updateRequestBodySync(req, body) {
|
|
337
387
|
if (!this.currentSession) {
|
|
338
388
|
return;
|
|
339
389
|
}
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
(
|
|
390
|
+
const recordingId = req.__recordingId;
|
|
391
|
+
if (recordingId === void 0) {
|
|
392
|
+
console.error(
|
|
393
|
+
`updateRequestBodySync: No recording ID found on request ${req.method} ${req.url}`
|
|
394
|
+
);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const record = this.currentSession.recordings.find(
|
|
398
|
+
(r) => r.recordingId === recordingId
|
|
343
399
|
);
|
|
344
400
|
if (!record) {
|
|
345
401
|
console.error(
|
|
346
|
-
`updateRequestBodySync: Could not find
|
|
402
|
+
`updateRequestBodySync: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
347
403
|
);
|
|
348
404
|
return;
|
|
349
405
|
}
|
|
350
406
|
record.request.body = body || null;
|
|
351
407
|
console.log(
|
|
352
|
-
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars)`
|
|
408
|
+
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars, recordingId: ${recordingId})`
|
|
353
409
|
);
|
|
354
410
|
}
|
|
355
411
|
async recordResponse(req, proxyRes) {
|
|
356
412
|
if (!this.currentSession) {
|
|
357
413
|
return;
|
|
358
414
|
}
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
(
|
|
415
|
+
const recordingId = req.__recordingId;
|
|
416
|
+
if (recordingId === void 0) {
|
|
417
|
+
console.error(
|
|
418
|
+
`recordResponse: No recording ID found on request ${req.method} ${req.url}`
|
|
419
|
+
);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const record = this.currentSession.recordings.find(
|
|
423
|
+
(r) => r.recordingId === recordingId
|
|
362
424
|
);
|
|
363
425
|
if (!record) {
|
|
364
|
-
console.error(
|
|
426
|
+
console.error(
|
|
427
|
+
`recordResponse: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
428
|
+
);
|
|
365
429
|
return;
|
|
366
430
|
}
|
|
367
431
|
const chunks = [];
|
|
@@ -375,34 +439,28 @@ var ProxyServer = class {
|
|
|
375
439
|
headers: proxyRes.headers,
|
|
376
440
|
body: body || null
|
|
377
441
|
};
|
|
378
|
-
console.log(
|
|
442
|
+
console.log(
|
|
443
|
+
`Recorded: ${req.method} ${req.url} (recordingId: ${recordingId})`
|
|
444
|
+
);
|
|
379
445
|
});
|
|
380
446
|
}
|
|
381
447
|
async recordResponseData(req, proxyRes, body) {
|
|
382
448
|
if (!this.currentSession) {
|
|
383
449
|
return false;
|
|
384
450
|
}
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
(r) => r.key === key && !r.response
|
|
388
|
-
);
|
|
389
|
-
if (!record) {
|
|
390
|
-
const host = req.headers.host || "unknown";
|
|
391
|
-
const recordsWithKey = this.currentSession.recordings.filter(
|
|
392
|
-
(r) => r.key === key
|
|
393
|
-
);
|
|
451
|
+
const recordingId = req.__recordingId;
|
|
452
|
+
if (recordingId === void 0) {
|
|
394
453
|
console.error(
|
|
395
|
-
`
|
|
396
|
-
);
|
|
397
|
-
console.error(
|
|
398
|
-
` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
|
|
454
|
+
`recordResponseData: No recording ID found on request ${req.method} ${req.url}`
|
|
399
455
|
);
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
const record = this.currentSession.recordings.find(
|
|
459
|
+
(r) => r.recordingId === recordingId
|
|
460
|
+
);
|
|
461
|
+
if (!record) {
|
|
400
462
|
console.error(
|
|
401
|
-
`
|
|
402
|
-
recordsWithKey.map((r) => ({
|
|
403
|
-
seq: r.sequence,
|
|
404
|
-
hasResponse: !!r.response
|
|
405
|
-
}))
|
|
463
|
+
`recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
406
464
|
);
|
|
407
465
|
return false;
|
|
408
466
|
}
|
|
@@ -411,34 +469,78 @@ var ProxyServer = class {
|
|
|
411
469
|
headers: proxyRes.headers,
|
|
412
470
|
body: body || null
|
|
413
471
|
};
|
|
414
|
-
record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
415
|
-
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
416
|
-
record.sequence = currentSequence;
|
|
417
|
-
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
418
472
|
console.log(
|
|
419
|
-
`recordResponseData: Recorded response for ${req.method} ${req.url} (
|
|
473
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url} (recordingId: ${recordingId})`
|
|
420
474
|
);
|
|
421
475
|
return true;
|
|
422
476
|
}
|
|
423
477
|
async handleReplayRequest(req, res) {
|
|
478
|
+
const recordingId = this.getRecordingIdFromCookie(req) || this.replayId;
|
|
479
|
+
if (!recordingId) {
|
|
480
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
481
|
+
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
482
|
+
"Content-Type": "application/json",
|
|
483
|
+
...corsHeaders
|
|
484
|
+
});
|
|
485
|
+
res.end(JSON.stringify({ error: "No replay session active" }));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
424
488
|
const key = getReqID(req);
|
|
425
|
-
const filePath = getRecordingPath(this.recordingsDir,
|
|
489
|
+
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
426
490
|
try {
|
|
427
|
-
const
|
|
491
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
492
|
+
if (!sessionState.loadedSession) {
|
|
493
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
494
|
+
console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
|
|
495
|
+
}
|
|
496
|
+
const session = sessionState.loadedSession;
|
|
497
|
+
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
498
|
+
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
499
|
+
}
|
|
500
|
+
const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
|
|
428
501
|
const host = req.headers.host || "unknown";
|
|
429
|
-
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.
|
|
502
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.recordingId - b.recordingId);
|
|
430
503
|
if (recordsWithKey.length === 0) {
|
|
431
|
-
|
|
432
|
-
|
|
504
|
+
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
505
|
+
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
506
|
+
console.error(
|
|
507
|
+
`[REPLAY ERROR] This request was not made during recording - possible test non-determinism`
|
|
508
|
+
);
|
|
509
|
+
const errorResponse = {
|
|
510
|
+
error: "No recording found",
|
|
511
|
+
message: errorMsg,
|
|
512
|
+
key,
|
|
513
|
+
sessionId: recordingId
|
|
514
|
+
};
|
|
515
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
516
|
+
res.writeHead(HTTP_STATUS_NOT_FOUND, {
|
|
517
|
+
"Content-Type": "application/json",
|
|
518
|
+
...corsHeaders
|
|
519
|
+
});
|
|
520
|
+
res.end(JSON.stringify(errorResponse));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const requestCount = servedForThisKey.size + 1;
|
|
524
|
+
console.log(
|
|
525
|
+
`[REPLAY REQUEST #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
526
|
+
);
|
|
527
|
+
let record;
|
|
528
|
+
for (const rec of recordsWithKey) {
|
|
529
|
+
if (!servedForThisKey.has(rec.recordingId)) {
|
|
530
|
+
record = rec;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (!record) {
|
|
535
|
+
console.log(
|
|
536
|
+
`[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
|
|
433
537
|
);
|
|
538
|
+
record = recordsWithKey[recordsWithKey.length - 1];
|
|
434
539
|
}
|
|
435
|
-
|
|
436
|
-
const recordIndex = usageCount % recordsWithKey.length;
|
|
437
|
-
const record = recordsWithKey[recordIndex];
|
|
540
|
+
servedForThisKey.add(record.recordingId);
|
|
438
541
|
console.log(
|
|
439
|
-
`
|
|
542
|
+
`[REPLAY SERVING] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
|
|
440
543
|
);
|
|
441
|
-
this.replaySequenceMap.set(key, usageCount + 1);
|
|
442
544
|
if (!record.response) {
|
|
443
545
|
throw new Error(
|
|
444
546
|
`No response recorded for this request: ${req.method} ${host}${req.url}`
|
|
@@ -504,6 +606,7 @@ var ProxyServer = class {
|
|
|
504
606
|
this.proxy.web(req, res, { target });
|
|
505
607
|
}
|
|
506
608
|
}
|
|
609
|
+
// TODO: check if can handle streaming requests
|
|
507
610
|
async bufferAndProxyRequest(req, res, target) {
|
|
508
611
|
const chunks = [];
|
|
509
612
|
req.on("data", (chunk) => {
|
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-CjM3evKb.cjs';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -12,8 +12,8 @@ declare class ProxyServer {
|
|
|
12
12
|
private proxy;
|
|
13
13
|
private currentSession;
|
|
14
14
|
private recordingsDir;
|
|
15
|
-
private
|
|
16
|
-
private
|
|
15
|
+
private recordingIdCounter;
|
|
16
|
+
private replaySessions;
|
|
17
17
|
constructor(targets: string[], recordingsDir: string);
|
|
18
18
|
init(): Promise<void>;
|
|
19
19
|
listen(port: number): http.Server;
|
|
@@ -28,6 +28,19 @@ declare class ProxyServer {
|
|
|
28
28
|
private getCorsHeaders;
|
|
29
29
|
private addCorsHeaders;
|
|
30
30
|
private getTarget;
|
|
31
|
+
/**
|
|
32
|
+
* Extract recording ID from request cookie
|
|
33
|
+
* Used for concurrent replay session routing
|
|
34
|
+
* @param req The incoming HTTP request
|
|
35
|
+
* @returns The recording ID from cookie, or null if not found
|
|
36
|
+
*/
|
|
37
|
+
private getRecordingIdFromCookie;
|
|
38
|
+
/**
|
|
39
|
+
* Get or create a replay session state for a given recording ID
|
|
40
|
+
* @param recordingId The recording ID to get/create session for
|
|
41
|
+
* @returns The replay session state
|
|
42
|
+
*/
|
|
43
|
+
private getOrCreateReplaySession;
|
|
31
44
|
private parseGetParams;
|
|
32
45
|
private handleControlRequest;
|
|
33
46
|
private clearModeTimeout;
|
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-CjM3evKb.js';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -12,8 +12,8 @@ declare class ProxyServer {
|
|
|
12
12
|
private proxy;
|
|
13
13
|
private currentSession;
|
|
14
14
|
private recordingsDir;
|
|
15
|
-
private
|
|
16
|
-
private
|
|
15
|
+
private recordingIdCounter;
|
|
16
|
+
private replaySessions;
|
|
17
17
|
constructor(targets: string[], recordingsDir: string);
|
|
18
18
|
init(): Promise<void>;
|
|
19
19
|
listen(port: number): http.Server;
|
|
@@ -28,6 +28,19 @@ declare class ProxyServer {
|
|
|
28
28
|
private getCorsHeaders;
|
|
29
29
|
private addCorsHeaders;
|
|
30
30
|
private getTarget;
|
|
31
|
+
/**
|
|
32
|
+
* Extract recording ID from request cookie
|
|
33
|
+
* Used for concurrent replay session routing
|
|
34
|
+
* @param req The incoming HTTP request
|
|
35
|
+
* @returns The recording ID from cookie, or null if not found
|
|
36
|
+
*/
|
|
37
|
+
private getRecordingIdFromCookie;
|
|
38
|
+
/**
|
|
39
|
+
* Get or create a replay session state for a given recording ID
|
|
40
|
+
* @param recordingId The recording ID to get/create session for
|
|
41
|
+
* @returns The replay session state
|
|
42
|
+
*/
|
|
43
|
+
private getOrCreateReplaySession;
|
|
31
44
|
private parseGetParams;
|
|
32
45
|
private handleControlRequest;
|
|
33
46
|
private clearModeTimeout;
|