test-proxy-recorder 0.1.9 → 0.1.11
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-CG-XcFDa.d.cts → index-Cx_Kflfl.d.cts} +1 -1
- package/dist/{index-CG-XcFDa.d.ts → index-Cx_Kflfl.d.ts} +1 -1
- package/dist/index.cjs +173 -70
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.mjs +171 -68
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/proxy.js +172 -69
- 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
|
@@ -5,9 +5,9 @@ var http = require('http');
|
|
|
5
5
|
var https = require('https');
|
|
6
6
|
var httpProxy = require('http-proxy');
|
|
7
7
|
var ws = require('ws');
|
|
8
|
-
var path = require('path');
|
|
9
8
|
var crypto = require('crypto');
|
|
10
|
-
var
|
|
9
|
+
var path = require('path');
|
|
10
|
+
var filenamify2 = require('filenamify');
|
|
11
11
|
|
|
12
12
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
13
|
|
|
@@ -15,9 +15,9 @@ var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
|
15
15
|
var http__default = /*#__PURE__*/_interopDefault(http);
|
|
16
16
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
17
17
|
var httpProxy__default = /*#__PURE__*/_interopDefault(httpProxy);
|
|
18
|
-
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
19
18
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
20
|
-
var
|
|
19
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
20
|
+
var filenamify2__default = /*#__PURE__*/_interopDefault(filenamify2);
|
|
21
21
|
|
|
22
22
|
// src/ProxyServer.ts
|
|
23
23
|
|
|
@@ -36,23 +36,53 @@ var Modes = {
|
|
|
36
36
|
replay: "replay"
|
|
37
37
|
};
|
|
38
38
|
var JSON_INDENT_SPACES = 2;
|
|
39
|
+
var EXTENSION = ".mock.json";
|
|
40
|
+
var MAX_FILENAME_LENGTH = 255 - EXTENSION.length;
|
|
41
|
+
var HASH_LENGTH = 8;
|
|
42
|
+
function generateHash(str) {
|
|
43
|
+
return crypto__default.default.createHash("shake256", { outputLength: HASH_LENGTH / 2 }).update(str).digest("hex");
|
|
44
|
+
}
|
|
39
45
|
function getRecordingPath(recordingsDir, id) {
|
|
40
|
-
|
|
46
|
+
let processedId = id.replaceAll("/", "__");
|
|
47
|
+
if (processedId.length > MAX_FILENAME_LENGTH) {
|
|
48
|
+
const hash = generateHash(id);
|
|
49
|
+
const maxBaseLength = MAX_FILENAME_LENGTH - HASH_LENGTH - 1;
|
|
50
|
+
processedId = `${processedId.slice(0, maxBaseLength)}_${hash}`;
|
|
51
|
+
}
|
|
52
|
+
const sanitizedId = filenamify2__default.default(processedId, {
|
|
53
|
+
replacement: "_",
|
|
54
|
+
maxLength: 255
|
|
55
|
+
// Set explicit max to prevent filenamify's default truncation
|
|
56
|
+
});
|
|
57
|
+
return path__default.default.join(recordingsDir, `${sanitizedId}${EXTENSION}`);
|
|
41
58
|
}
|
|
42
59
|
async function loadRecordingSession(filePath) {
|
|
43
60
|
const fileContent = await fs__default.default.readFile(filePath, "utf8");
|
|
44
61
|
return JSON.parse(fileContent);
|
|
45
62
|
}
|
|
63
|
+
function processRecordings(recordings) {
|
|
64
|
+
const keySequenceMap = /* @__PURE__ */ new Map();
|
|
65
|
+
return recordings.map((recording) => {
|
|
66
|
+
const key = recording.key;
|
|
67
|
+
const currentSeq = keySequenceMap.get(key) || 0;
|
|
68
|
+
keySequenceMap.set(key, currentSeq + 1);
|
|
69
|
+
return { ...recording, sequence: currentSeq };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
46
72
|
async function saveRecordingSession(recordingsDir, session) {
|
|
47
73
|
const filePath = getRecordingPath(recordingsDir, session.id);
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
await fs__default.default.mkdir(recordingsDir, { recursive: true });
|
|
75
|
+
const processedRecordings = processRecordings(session.recordings);
|
|
76
|
+
const processedSession = {
|
|
77
|
+
...session,
|
|
78
|
+
recordings: processedRecordings
|
|
79
|
+
};
|
|
50
80
|
await fs__default.default.writeFile(
|
|
51
81
|
filePath,
|
|
52
|
-
JSON.stringify(
|
|
82
|
+
JSON.stringify(processedSession, null, JSON_INDENT_SPACES)
|
|
53
83
|
);
|
|
54
84
|
console.log(
|
|
55
|
-
`Saved ${
|
|
85
|
+
`Saved ${processedRecordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
56
86
|
);
|
|
57
87
|
}
|
|
58
88
|
function getReqID(req) {
|
|
@@ -60,10 +90,10 @@ function getReqID(req) {
|
|
|
60
90
|
const pathname = urlParts[0];
|
|
61
91
|
const query = urlParts[1] || "";
|
|
62
92
|
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
63
|
-
const normalizedPath =
|
|
93
|
+
const normalizedPath = filenamify2__default.default(pathPart, { replacement: "_" });
|
|
64
94
|
const queryHash = generateQueryHash(query);
|
|
65
95
|
const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
66
|
-
return
|
|
96
|
+
return filenamify2__default.default(filename, { replacement: "_" });
|
|
67
97
|
}
|
|
68
98
|
function generateQueryHash(query) {
|
|
69
99
|
if (!query) {
|
|
@@ -98,12 +128,10 @@ var ProxyServer = class {
|
|
|
98
128
|
proxy;
|
|
99
129
|
currentSession;
|
|
100
130
|
recordingsDir;
|
|
101
|
-
requestSequenceMap;
|
|
102
|
-
// Track sequence per request key
|
|
103
|
-
replaySequenceMap;
|
|
104
|
-
// Track replay position per request key
|
|
105
131
|
recordingIdCounter;
|
|
106
132
|
// Unique ID for each recording entry
|
|
133
|
+
replaySessions;
|
|
134
|
+
// Track multiple concurrent replay sessions by recording ID
|
|
107
135
|
constructor(targets, recordingsDir) {
|
|
108
136
|
this.targets = targets;
|
|
109
137
|
this.currentTargetIndex = 0;
|
|
@@ -114,11 +142,11 @@ var ProxyServer = class {
|
|
|
114
142
|
this.modeTimeout = null;
|
|
115
143
|
this.currentSession = null;
|
|
116
144
|
this.recordingsDir = recordingsDir;
|
|
117
|
-
this.
|
|
118
|
-
this.replaySequenceMap = /* @__PURE__ */ new Map();
|
|
145
|
+
this.replaySessions = /* @__PURE__ */ new Map();
|
|
119
146
|
this.proxy = httpProxy__default.default.createProxyServer({
|
|
120
147
|
secure: false,
|
|
121
|
-
changeOrigin: true
|
|
148
|
+
changeOrigin: true,
|
|
149
|
+
ws: true
|
|
122
150
|
});
|
|
123
151
|
this.setupProxyEventHandlers();
|
|
124
152
|
}
|
|
@@ -186,6 +214,43 @@ var ProxyServer = class {
|
|
|
186
214
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
187
215
|
return target;
|
|
188
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Extract recording ID from request cookie
|
|
219
|
+
* Used for concurrent replay session routing
|
|
220
|
+
* @param req The incoming HTTP request
|
|
221
|
+
* @returns The recording ID from cookie, or null if not found
|
|
222
|
+
*/
|
|
223
|
+
getRecordingIdFromCookie(req) {
|
|
224
|
+
const cookies = req.headers.cookie;
|
|
225
|
+
if (!cookies) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const match = cookies.match(/proxy-recording-id=([^;]+)/);
|
|
229
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get or create a replay session state for a given recording ID
|
|
233
|
+
* @param recordingId The recording ID to get/create session for
|
|
234
|
+
* @returns The replay session state
|
|
235
|
+
*/
|
|
236
|
+
getOrCreateReplaySession(recordingId) {
|
|
237
|
+
let session = this.replaySessions.get(recordingId);
|
|
238
|
+
if (session) {
|
|
239
|
+
session.lastAccessTime = Date.now();
|
|
240
|
+
} else {
|
|
241
|
+
session = {
|
|
242
|
+
recordingId,
|
|
243
|
+
servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
|
|
244
|
+
loadedSession: null,
|
|
245
|
+
lastAccessTime: Date.now()
|
|
246
|
+
};
|
|
247
|
+
this.replaySessions.set(recordingId, session);
|
|
248
|
+
console.log(
|
|
249
|
+
`[CONCURRENT REPLAY] Created new session for recording: ${recordingId}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return session;
|
|
253
|
+
}
|
|
189
254
|
parseGetParams(req) {
|
|
190
255
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
191
256
|
const mode = url.searchParams.get("mode");
|
|
@@ -202,16 +267,25 @@ var ProxyServer = class {
|
|
|
202
267
|
let data;
|
|
203
268
|
if (req.method === "GET") {
|
|
204
269
|
data = this.parseGetParams(req);
|
|
205
|
-
} else {
|
|
270
|
+
} else if (req.method === "POST") {
|
|
206
271
|
const body = await readRequestBody(req);
|
|
207
|
-
console.log(
|
|
272
|
+
console.log(`MODE CHANGE (${req.method})`, body);
|
|
208
273
|
data = JSON.parse(body);
|
|
274
|
+
} else {
|
|
275
|
+
return;
|
|
209
276
|
}
|
|
210
277
|
const { mode, id, timeout: requestTimeout } = data;
|
|
211
278
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
212
279
|
this.clearModeTimeout();
|
|
213
280
|
await this.switchMode(mode, id);
|
|
214
281
|
this.setupModeTimeout(timeout);
|
|
282
|
+
if (mode === Modes.replay && id) {
|
|
283
|
+
res.setHeader(
|
|
284
|
+
"Set-Cookie",
|
|
285
|
+
`proxy-recording-id=${encodeURIComponent(id)}; HttpOnly; Path=/; SameSite=Lax`
|
|
286
|
+
);
|
|
287
|
+
console.log(`[CONCURRENT REPLAY] Set cookie for recording: ${id}`);
|
|
288
|
+
}
|
|
215
289
|
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
216
290
|
success: true,
|
|
217
291
|
mode: this.mode,
|
|
@@ -226,14 +300,12 @@ var ProxyServer = class {
|
|
|
226
300
|
}
|
|
227
301
|
}
|
|
228
302
|
clearModeTimeout() {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
this.modeTimeout = null;
|
|
232
|
-
}
|
|
303
|
+
clearTimeout(this.modeTimeout || 0);
|
|
304
|
+
this.modeTimeout = null;
|
|
233
305
|
}
|
|
234
306
|
async switchMode(mode, id) {
|
|
235
|
-
|
|
236
|
-
|
|
307
|
+
console.log(`Switching to ${mode.toUpperCase()} mode`);
|
|
308
|
+
if (this.currentSession && this.mode === Modes.record) {
|
|
237
309
|
await this.saveCurrentSession(true);
|
|
238
310
|
console.log("Session saved, continuing with mode switch");
|
|
239
311
|
}
|
|
@@ -243,11 +315,17 @@ var ProxyServer = class {
|
|
|
243
315
|
break;
|
|
244
316
|
}
|
|
245
317
|
case Modes.record: {
|
|
318
|
+
if (!id) {
|
|
319
|
+
throw new Error("Record ID is required");
|
|
320
|
+
}
|
|
246
321
|
this.switchToRecordMode(id);
|
|
247
322
|
break;
|
|
248
323
|
}
|
|
249
324
|
case Modes.replay: {
|
|
250
|
-
|
|
325
|
+
if (!id) {
|
|
326
|
+
throw new Error("Replay ID is required");
|
|
327
|
+
}
|
|
328
|
+
await this.switchToReplayMode(id);
|
|
251
329
|
break;
|
|
252
330
|
}
|
|
253
331
|
default: {
|
|
@@ -264,36 +342,33 @@ var ProxyServer = class {
|
|
|
264
342
|
console.log("Switched to transparent mode");
|
|
265
343
|
}
|
|
266
344
|
switchToRecordMode(id) {
|
|
267
|
-
if (!id) {
|
|
268
|
-
throw new Error("Record ID is required");
|
|
269
|
-
}
|
|
270
345
|
this.mode = Modes.record;
|
|
271
346
|
this.recordingId = id;
|
|
272
347
|
this.replayId = null;
|
|
273
348
|
this.currentSession = { id, recordings: [], websocketRecordings: [] };
|
|
274
|
-
this.requestSequenceMap.clear();
|
|
275
349
|
console.log(`Switched to record mode with ID: ${id}`);
|
|
276
350
|
}
|
|
277
|
-
switchToReplayMode(id) {
|
|
278
|
-
if (!id) {
|
|
279
|
-
throw new Error("Replay ID is required");
|
|
280
|
-
}
|
|
351
|
+
async switchToReplayMode(id) {
|
|
281
352
|
this.mode = Modes.replay;
|
|
282
353
|
this.replayId = id;
|
|
283
354
|
this.recordingId = null;
|
|
284
355
|
this.currentSession = null;
|
|
285
|
-
this.
|
|
356
|
+
const session = this.replaySessions.get(id);
|
|
357
|
+
if (session) {
|
|
358
|
+
session.servedRecordingIdsByKey.clear();
|
|
359
|
+
console.log(`Reset served recordings tracker for session: ${id}`);
|
|
360
|
+
} else {
|
|
361
|
+
this.getOrCreateReplaySession(id);
|
|
362
|
+
}
|
|
286
363
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
287
364
|
}
|
|
288
365
|
setupModeTimeout(timeout) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}, timeout);
|
|
296
|
-
}
|
|
366
|
+
this.modeTimeout = setTimeout(async () => {
|
|
367
|
+
console.log("Timeout reached, switching back to transparent mode");
|
|
368
|
+
await this.saveCurrentSession(true);
|
|
369
|
+
this.switchToTransparentMode();
|
|
370
|
+
this.modeTimeout = null;
|
|
371
|
+
}, timeout);
|
|
297
372
|
}
|
|
298
373
|
async saveCurrentSession(filterIncomplete = false) {
|
|
299
374
|
if (!this.currentSession) {
|
|
@@ -319,9 +394,6 @@ var ProxyServer = class {
|
|
|
319
394
|
return;
|
|
320
395
|
}
|
|
321
396
|
const key = getReqID(req);
|
|
322
|
-
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
323
|
-
const sequence = currentSequence;
|
|
324
|
-
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
325
397
|
const recordingId = this.recordingIdCounter++;
|
|
326
398
|
req.__recordingId = recordingId;
|
|
327
399
|
const record = {
|
|
@@ -333,13 +405,12 @@ var ProxyServer = class {
|
|
|
333
405
|
},
|
|
334
406
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
335
407
|
key,
|
|
336
|
-
sequence,
|
|
337
408
|
recordingId
|
|
338
409
|
};
|
|
339
410
|
this.currentSession.recordings.push(record);
|
|
340
411
|
console.log(
|
|
341
412
|
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
342
|
-
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key},
|
|
413
|
+
`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})`
|
|
343
414
|
);
|
|
344
415
|
}
|
|
345
416
|
updateRequestBodySync(req, body) {
|
|
@@ -399,7 +470,7 @@ var ProxyServer = class {
|
|
|
399
470
|
body: body || null
|
|
400
471
|
};
|
|
401
472
|
console.log(
|
|
402
|
-
`Recorded: ${req.method} ${req.url} (
|
|
473
|
+
`Recorded: ${req.method} ${req.url} (recordingId: ${recordingId})`
|
|
403
474
|
);
|
|
404
475
|
});
|
|
405
476
|
}
|
|
@@ -429,46 +500,77 @@ var ProxyServer = class {
|
|
|
429
500
|
body: body || null
|
|
430
501
|
};
|
|
431
502
|
console.log(
|
|
432
|
-
`recordResponseData: Recorded response for ${req.method} ${req.url} (
|
|
503
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url} (recordingId: ${recordingId})`
|
|
433
504
|
);
|
|
434
505
|
return true;
|
|
435
506
|
}
|
|
436
507
|
async handleReplayRequest(req, res) {
|
|
508
|
+
const recordingId = this.getRecordingIdFromCookie(req) || this.replayId;
|
|
509
|
+
if (!recordingId) {
|
|
510
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
511
|
+
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
512
|
+
"Content-Type": "application/json",
|
|
513
|
+
...corsHeaders
|
|
514
|
+
});
|
|
515
|
+
res.end(JSON.stringify({ error: "No replay session active" }));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
437
518
|
const key = getReqID(req);
|
|
438
|
-
const filePath = getRecordingPath(this.recordingsDir,
|
|
519
|
+
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
439
520
|
try {
|
|
440
|
-
const
|
|
521
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
522
|
+
if (!sessionState.loadedSession) {
|
|
523
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
524
|
+
console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
|
|
525
|
+
}
|
|
526
|
+
const session = sessionState.loadedSession;
|
|
527
|
+
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
528
|
+
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
529
|
+
}
|
|
530
|
+
const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
|
|
441
531
|
const host = req.headers.host || "unknown";
|
|
442
|
-
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.
|
|
532
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.recordingId - b.recordingId);
|
|
443
533
|
if (recordsWithKey.length === 0) {
|
|
444
|
-
|
|
445
|
-
|
|
534
|
+
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
535
|
+
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
536
|
+
console.error(
|
|
537
|
+
`[REPLAY ERROR] This request was not made during recording - possible test non-determinism`
|
|
446
538
|
);
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
539
|
+
const errorResponse = {
|
|
540
|
+
error: "No recording found",
|
|
541
|
+
message: errorMsg,
|
|
542
|
+
key,
|
|
543
|
+
sessionId: recordingId
|
|
544
|
+
};
|
|
453
545
|
const corsHeaders = this.getCorsHeaders(req);
|
|
454
|
-
res.writeHead(
|
|
546
|
+
res.writeHead(HTTP_STATUS_NOT_FOUND, {
|
|
455
547
|
"Content-Type": "application/json",
|
|
456
548
|
...corsHeaders
|
|
457
549
|
});
|
|
458
|
-
res.end(JSON.stringify(
|
|
550
|
+
res.end(JSON.stringify(errorResponse));
|
|
459
551
|
return;
|
|
460
552
|
}
|
|
461
|
-
const
|
|
553
|
+
const requestCount = servedForThisKey.size + 1;
|
|
554
|
+
console.log(
|
|
555
|
+
`[REPLAY REQUEST #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
556
|
+
);
|
|
462
557
|
let record;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
558
|
+
for (const rec of recordsWithKey) {
|
|
559
|
+
if (!servedForThisKey.has(rec.recordingId)) {
|
|
560
|
+
record = rec;
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (!record) {
|
|
565
|
+
console.log(
|
|
566
|
+
`[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
|
|
567
|
+
);
|
|
466
568
|
record = recordsWithKey[recordsWithKey.length - 1];
|
|
467
569
|
}
|
|
570
|
+
servedForThisKey.add(record.recordingId);
|
|
468
571
|
console.log(
|
|
469
|
-
`
|
|
572
|
+
`[REPLAY SERVING] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
|
|
470
573
|
);
|
|
471
|
-
this.replaySequenceMap.set(key, usageCount + 1);
|
|
472
574
|
if (!record.response) {
|
|
473
575
|
throw new Error(
|
|
474
576
|
`No response recorded for this request: ${req.method} ${host}${req.url}`
|
|
@@ -534,6 +636,7 @@ var ProxyServer = class {
|
|
|
534
636
|
this.proxy.web(req, res, { target });
|
|
535
637
|
}
|
|
536
638
|
}
|
|
639
|
+
// TODO: check if can handle streaming requests
|
|
537
640
|
async bufferAndProxyRequest(req, res, target) {
|
|
538
641
|
const chunks = [];
|
|
539
642
|
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-Cx_Kflfl.cjs';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -12,9 +12,8 @@ declare class ProxyServer {
|
|
|
12
12
|
private proxy;
|
|
13
13
|
private currentSession;
|
|
14
14
|
private recordingsDir;
|
|
15
|
-
private requestSequenceMap;
|
|
16
|
-
private replaySequenceMap;
|
|
17
15
|
private recordingIdCounter;
|
|
16
|
+
private replaySessions;
|
|
18
17
|
constructor(targets: string[], recordingsDir: string);
|
|
19
18
|
init(): Promise<void>;
|
|
20
19
|
listen(port: number): http.Server;
|
|
@@ -29,6 +28,19 @@ declare class ProxyServer {
|
|
|
29
28
|
private getCorsHeaders;
|
|
30
29
|
private addCorsHeaders;
|
|
31
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;
|
|
32
44
|
private parseGetParams;
|
|
33
45
|
private handleControlRequest;
|
|
34
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-Cx_Kflfl.js';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
|
|
5
5
|
declare class ProxyServer {
|
|
@@ -12,9 +12,8 @@ declare class ProxyServer {
|
|
|
12
12
|
private proxy;
|
|
13
13
|
private currentSession;
|
|
14
14
|
private recordingsDir;
|
|
15
|
-
private requestSequenceMap;
|
|
16
|
-
private replaySequenceMap;
|
|
17
15
|
private recordingIdCounter;
|
|
16
|
+
private replaySessions;
|
|
18
17
|
constructor(targets: string[], recordingsDir: string);
|
|
19
18
|
init(): Promise<void>;
|
|
20
19
|
listen(port: number): http.Server;
|
|
@@ -29,6 +28,19 @@ declare class ProxyServer {
|
|
|
29
28
|
private getCorsHeaders;
|
|
30
29
|
private addCorsHeaders;
|
|
31
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;
|
|
32
44
|
private parseGetParams;
|
|
33
45
|
private handleControlRequest;
|
|
34
46
|
private clearModeTimeout;
|