test-proxy-recorder 0.3.2 → 0.3.3
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.cjs +62 -64
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +62 -64
- package/dist/playwright/index.cjs +1 -24
- package/dist/playwright/index.mjs +1 -24
- package/dist/proxy.js +61 -40
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -148,6 +148,8 @@ var ProxyServer = class {
|
|
|
148
148
|
// Track multiple concurrent replay sessions by recording ID
|
|
149
149
|
recordingPromises;
|
|
150
150
|
// Stack of promises that resolve to completed recordings
|
|
151
|
+
flushPromise;
|
|
152
|
+
// Promise for in-progress flush operation
|
|
151
153
|
constructor(targets, recordingsDir) {
|
|
152
154
|
this.targets = targets;
|
|
153
155
|
this.currentTargetIndex = 0;
|
|
@@ -161,6 +163,7 @@ var ProxyServer = class {
|
|
|
161
163
|
this.recordingsDir = recordingsDir;
|
|
162
164
|
this.replaySessions = /* @__PURE__ */ new Map();
|
|
163
165
|
this.recordingPromises = [];
|
|
166
|
+
this.flushPromise = null;
|
|
164
167
|
this.proxy = httpProxy__default.default.createProxyServer({
|
|
165
168
|
secure: false,
|
|
166
169
|
changeOrigin: true,
|
|
@@ -283,7 +286,8 @@ var ProxyServer = class {
|
|
|
283
286
|
recordingId,
|
|
284
287
|
servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
|
|
285
288
|
loadedSession: null,
|
|
286
|
-
lastAccessTime: Date.now()
|
|
289
|
+
lastAccessTime: Date.now(),
|
|
290
|
+
sortedRecordingsByKey: /* @__PURE__ */ new Map()
|
|
287
291
|
};
|
|
288
292
|
this.replaySessions.set(recordingId, session);
|
|
289
293
|
console.log(
|
|
@@ -298,17 +302,14 @@ var ProxyServer = class {
|
|
|
298
302
|
*/
|
|
299
303
|
async cleanupSession(sessionId) {
|
|
300
304
|
if (this.replaySessions.has(sessionId)) {
|
|
301
|
-
console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
|
|
302
305
|
this.replaySessions.delete(sessionId);
|
|
303
306
|
}
|
|
304
307
|
if (this.recordingId === sessionId) {
|
|
305
|
-
console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
|
|
306
308
|
await this.saveCurrentSession();
|
|
307
309
|
this.currentSession = null;
|
|
308
310
|
this.recordingId = null;
|
|
309
311
|
}
|
|
310
312
|
if (this.replayId === sessionId) {
|
|
311
|
-
console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
|
|
312
313
|
this.replayId = null;
|
|
313
314
|
}
|
|
314
315
|
console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
|
|
@@ -357,7 +358,9 @@ var ProxyServer = class {
|
|
|
357
358
|
return;
|
|
358
359
|
}
|
|
359
360
|
if (!mode) {
|
|
360
|
-
throw new Error(
|
|
361
|
+
throw new Error(
|
|
362
|
+
"Mode parameter is required when cleanup is not specified"
|
|
363
|
+
);
|
|
361
364
|
}
|
|
362
365
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
363
366
|
this.clearModeTimeout();
|
|
@@ -440,12 +443,16 @@ var ProxyServer = class {
|
|
|
440
443
|
this.replayId = id;
|
|
441
444
|
this.recordingId = null;
|
|
442
445
|
this.currentSession = null;
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
446
|
+
const sessionState = this.getOrCreateReplaySession(id);
|
|
447
|
+
sessionState.servedRecordingIdsByKey.clear();
|
|
448
|
+
sessionState.sortedRecordingsByKey.clear();
|
|
449
|
+
const filePath = getRecordingPath(this.recordingsDir, id);
|
|
450
|
+
try {
|
|
451
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
452
|
+
console.log(`[REPLAY] Loaded recording session: ${id}`);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
|
|
455
|
+
sessionState.loadedSession = null;
|
|
449
456
|
}
|
|
450
457
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
451
458
|
}
|
|
@@ -459,21 +466,32 @@ var ProxyServer = class {
|
|
|
459
466
|
}, timeout);
|
|
460
467
|
}
|
|
461
468
|
async flushPendingRecordings() {
|
|
469
|
+
if (this.flushPromise) {
|
|
470
|
+
await this.flushPromise;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
462
473
|
if (this.recordingPromises.length === 0) {
|
|
463
474
|
return;
|
|
464
475
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (
|
|
469
|
-
|
|
476
|
+
this.flushPromise = (async () => {
|
|
477
|
+
try {
|
|
478
|
+
const results = await Promise.allSettled(this.recordingPromises);
|
|
479
|
+
if (this.currentSession) {
|
|
480
|
+
for (const result of results) {
|
|
481
|
+
if (result.status === "fulfilled" && result.value) {
|
|
482
|
+
this.currentSession.recordings.push(result.value);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
console.log(
|
|
486
|
+
`Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
|
|
487
|
+
);
|
|
470
488
|
}
|
|
489
|
+
this.recordingPromises = [];
|
|
490
|
+
} finally {
|
|
491
|
+
this.flushPromise = null;
|
|
471
492
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
this.recordingPromises = [];
|
|
493
|
+
})();
|
|
494
|
+
await this.flushPromise;
|
|
477
495
|
}
|
|
478
496
|
async saveCurrentSession() {
|
|
479
497
|
if (!this.currentSession) {
|
|
@@ -523,20 +541,25 @@ var ProxyServer = class {
|
|
|
523
541
|
);
|
|
524
542
|
return recordingId;
|
|
525
543
|
}
|
|
526
|
-
async ensureSessionLoaded(recordingId, filePath) {
|
|
527
|
-
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
528
|
-
if (!sessionState.loadedSession) {
|
|
529
|
-
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
530
|
-
console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
|
|
531
|
-
}
|
|
532
|
-
return sessionState;
|
|
533
|
-
}
|
|
534
544
|
getServedTracker(sessionState, key) {
|
|
535
545
|
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
536
546
|
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
537
547
|
}
|
|
538
548
|
return sessionState.servedRecordingIdsByKey.get(key);
|
|
539
549
|
}
|
|
550
|
+
getSortedRecordings(sessionState, key) {
|
|
551
|
+
if (sessionState.sortedRecordingsByKey.has(key)) {
|
|
552
|
+
return sessionState.sortedRecordingsByKey.get(key);
|
|
553
|
+
}
|
|
554
|
+
const session = sessionState.loadedSession;
|
|
555
|
+
const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
556
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
557
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
558
|
+
return aSeq - bSeq;
|
|
559
|
+
});
|
|
560
|
+
sessionState.sortedRecordingsByKey.set(key, sortedRecords);
|
|
561
|
+
return sortedRecords;
|
|
562
|
+
}
|
|
540
563
|
selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
|
|
541
564
|
for (const rec of recordsWithKey) {
|
|
542
565
|
if (!servedForThisKey.has(rec.recordingId)) {
|
|
@@ -557,18 +580,17 @@ var ProxyServer = class {
|
|
|
557
580
|
const key = getReqID(req);
|
|
558
581
|
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
559
582
|
try {
|
|
560
|
-
const sessionState =
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
583
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
584
|
+
if (!sessionState.loadedSession) {
|
|
585
|
+
const error = new Error(
|
|
586
|
+
`Recording session file not found: ${filePath}`
|
|
587
|
+
);
|
|
588
|
+
error.code = "ENOENT";
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
565
591
|
const servedForThisKey = this.getServedTracker(sessionState, key);
|
|
566
592
|
const host = req.headers.host || "unknown";
|
|
567
|
-
const recordsWithKey =
|
|
568
|
-
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
569
|
-
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
570
|
-
return aSeq - bSeq;
|
|
571
|
-
});
|
|
593
|
+
const recordsWithKey = this.getSortedRecordings(sessionState, key);
|
|
572
594
|
if (recordsWithKey.length === 0) {
|
|
573
595
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
574
596
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -779,7 +801,6 @@ var ProxyServer = class {
|
|
|
779
801
|
})();
|
|
780
802
|
});
|
|
781
803
|
this.recordingPromises.push(recordingPromise);
|
|
782
|
-
await recordingPromise;
|
|
783
804
|
}
|
|
784
805
|
handleUpgrade(req, socket, head) {
|
|
785
806
|
if (this.mode === Modes.replay) {
|
|
@@ -973,7 +994,6 @@ async function setProxyMode(mode, sessionId, timeout) {
|
|
|
973
994
|
throw new Error(`Failed to set proxy mode: ${text}`);
|
|
974
995
|
}
|
|
975
996
|
await response.json();
|
|
976
|
-
console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
|
|
977
997
|
} catch (error) {
|
|
978
998
|
console.error(`Error setting proxy mode:`, error);
|
|
979
999
|
throw error;
|
|
@@ -997,9 +1017,8 @@ async function cleanupSession(sessionId) {
|
|
|
997
1017
|
throw new Error(`Failed to cleanup session: ${text}`);
|
|
998
1018
|
}
|
|
999
1019
|
await response.json();
|
|
1000
|
-
console.log(`Session cleaned up: ${sessionId}`);
|
|
1001
1020
|
} catch (error) {
|
|
1002
|
-
console.error(`Error cleaning up session
|
|
1021
|
+
console.error(`Error cleaning up session: ${sessionId}`, error);
|
|
1003
1022
|
throw error;
|
|
1004
1023
|
}
|
|
1005
1024
|
}
|
|
@@ -1072,9 +1091,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
|
|
|
1072
1091
|
const harFileName = sessionId.replaceAll("/", "__");
|
|
1073
1092
|
const recordingsDir = await getRecordingsDir();
|
|
1074
1093
|
const harPath = path2__default.default.join(recordingsDir, `${harFileName}.har`);
|
|
1075
|
-
console.log(
|
|
1076
|
-
`[Client-Side Recording] Setting up HAR for session: ${sessionId}, mode: ${mode}, path: ${harPath}`
|
|
1077
|
-
);
|
|
1078
1094
|
try {
|
|
1079
1095
|
await page.routeFromHAR(harPath, {
|
|
1080
1096
|
url,
|
|
@@ -1107,41 +1123,27 @@ var playwrightProxy = {
|
|
|
1107
1123
|
await page.setExtraHTTPHeaders({
|
|
1108
1124
|
[RECORDING_ID_HEADER]: sessionId
|
|
1109
1125
|
});
|
|
1110
|
-
console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
|
|
1111
1126
|
await setProxyMode(mode, sessionId, timeout);
|
|
1112
|
-
console.log(`[Setup] Proxy mode set successfully`);
|
|
1113
1127
|
if (clientSideOptions?.url) {
|
|
1114
|
-
console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
|
|
1115
1128
|
await setupClientSideRecording(
|
|
1116
1129
|
page,
|
|
1117
1130
|
sessionId,
|
|
1118
1131
|
mode,
|
|
1119
1132
|
clientSideOptions.url
|
|
1120
1133
|
);
|
|
1121
|
-
console.log(`[Setup] Client-side recording setup complete`);
|
|
1122
1134
|
}
|
|
1123
1135
|
const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
|
|
1124
1136
|
const proxyUrl = `localhost:${proxyPort}`;
|
|
1125
|
-
console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
|
|
1126
1137
|
await page.route(
|
|
1127
1138
|
(url) => {
|
|
1128
1139
|
const urlStr = url.toString();
|
|
1129
1140
|
const matches = urlStr.includes(proxyUrl);
|
|
1130
|
-
if (matches) {
|
|
1131
|
-
console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
|
|
1132
|
-
}
|
|
1133
1141
|
return matches;
|
|
1134
1142
|
},
|
|
1135
1143
|
async (route) => {
|
|
1136
1144
|
try {
|
|
1137
|
-
const url = route.request().url();
|
|
1138
|
-
const method = route.request().method();
|
|
1139
1145
|
const headers = route.request().headers();
|
|
1140
|
-
const hadHeader = !!headers[RECORDING_ID_HEADER];
|
|
1141
1146
|
headers[RECORDING_ID_HEADER] = sessionId;
|
|
1142
|
-
console.log(
|
|
1143
|
-
`[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
|
|
1144
|
-
);
|
|
1145
1147
|
await route.continue({ headers });
|
|
1146
1148
|
} catch (error) {
|
|
1147
1149
|
console.error(
|
|
@@ -1154,7 +1156,6 @@ var playwrightProxy = {
|
|
|
1154
1156
|
{ times: Infinity }
|
|
1155
1157
|
// Ensure the handler applies to all matching requests
|
|
1156
1158
|
);
|
|
1157
|
-
console.log(`[Setup] Proxy route handler registered`);
|
|
1158
1159
|
const context = page.context();
|
|
1159
1160
|
const contextId = context._guid || "default";
|
|
1160
1161
|
const handlerKey = `cleanup_${contextId}`;
|
|
@@ -1162,9 +1163,6 @@ var playwrightProxy = {
|
|
|
1162
1163
|
globalThis[handlerKey] = true;
|
|
1163
1164
|
context.on("close", async () => {
|
|
1164
1165
|
try {
|
|
1165
|
-
console.log(
|
|
1166
|
-
`[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
|
|
1167
|
-
);
|
|
1168
1166
|
await cleanupSession(sessionId);
|
|
1169
1167
|
} catch (error) {
|
|
1170
1168
|
console.warn(
|
package/dist/index.d.cts
CHANGED
|
@@ -17,6 +17,7 @@ declare class ProxyServer {
|
|
|
17
17
|
private sequenceCounterByKey;
|
|
18
18
|
private replaySessions;
|
|
19
19
|
private recordingPromises;
|
|
20
|
+
private flushPromise;
|
|
20
21
|
constructor(targets: string[], recordingsDir: string);
|
|
21
22
|
init(): Promise<void>;
|
|
22
23
|
listen(port: number): http.Server;
|
|
@@ -73,8 +74,8 @@ declare class ProxyServer {
|
|
|
73
74
|
private flushPendingRecordings;
|
|
74
75
|
private saveCurrentSession;
|
|
75
76
|
private getRecordingIdOrError;
|
|
76
|
-
private ensureSessionLoaded;
|
|
77
77
|
private getServedTracker;
|
|
78
|
+
private getSortedRecordings;
|
|
78
79
|
private selectReplayRecord;
|
|
79
80
|
private handleReplayRequest;
|
|
80
81
|
private handleReplayError;
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ declare class ProxyServer {
|
|
|
17
17
|
private sequenceCounterByKey;
|
|
18
18
|
private replaySessions;
|
|
19
19
|
private recordingPromises;
|
|
20
|
+
private flushPromise;
|
|
20
21
|
constructor(targets: string[], recordingsDir: string);
|
|
21
22
|
init(): Promise<void>;
|
|
22
23
|
listen(port: number): http.Server;
|
|
@@ -73,8 +74,8 @@ declare class ProxyServer {
|
|
|
73
74
|
private flushPendingRecordings;
|
|
74
75
|
private saveCurrentSession;
|
|
75
76
|
private getRecordingIdOrError;
|
|
76
|
-
private ensureSessionLoaded;
|
|
77
77
|
private getServedTracker;
|
|
78
|
+
private getSortedRecordings;
|
|
78
79
|
private selectReplayRecord;
|
|
79
80
|
private handleReplayRequest;
|
|
80
81
|
private handleReplayError;
|
package/dist/index.mjs
CHANGED
|
@@ -136,6 +136,8 @@ var ProxyServer = class {
|
|
|
136
136
|
// Track multiple concurrent replay sessions by recording ID
|
|
137
137
|
recordingPromises;
|
|
138
138
|
// Stack of promises that resolve to completed recordings
|
|
139
|
+
flushPromise;
|
|
140
|
+
// Promise for in-progress flush operation
|
|
139
141
|
constructor(targets, recordingsDir) {
|
|
140
142
|
this.targets = targets;
|
|
141
143
|
this.currentTargetIndex = 0;
|
|
@@ -149,6 +151,7 @@ var ProxyServer = class {
|
|
|
149
151
|
this.recordingsDir = recordingsDir;
|
|
150
152
|
this.replaySessions = /* @__PURE__ */ new Map();
|
|
151
153
|
this.recordingPromises = [];
|
|
154
|
+
this.flushPromise = null;
|
|
152
155
|
this.proxy = httpProxy.createProxyServer({
|
|
153
156
|
secure: false,
|
|
154
157
|
changeOrigin: true,
|
|
@@ -271,7 +274,8 @@ var ProxyServer = class {
|
|
|
271
274
|
recordingId,
|
|
272
275
|
servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
|
|
273
276
|
loadedSession: null,
|
|
274
|
-
lastAccessTime: Date.now()
|
|
277
|
+
lastAccessTime: Date.now(),
|
|
278
|
+
sortedRecordingsByKey: /* @__PURE__ */ new Map()
|
|
275
279
|
};
|
|
276
280
|
this.replaySessions.set(recordingId, session);
|
|
277
281
|
console.log(
|
|
@@ -286,17 +290,14 @@ var ProxyServer = class {
|
|
|
286
290
|
*/
|
|
287
291
|
async cleanupSession(sessionId) {
|
|
288
292
|
if (this.replaySessions.has(sessionId)) {
|
|
289
|
-
console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
|
|
290
293
|
this.replaySessions.delete(sessionId);
|
|
291
294
|
}
|
|
292
295
|
if (this.recordingId === sessionId) {
|
|
293
|
-
console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
|
|
294
296
|
await this.saveCurrentSession();
|
|
295
297
|
this.currentSession = null;
|
|
296
298
|
this.recordingId = null;
|
|
297
299
|
}
|
|
298
300
|
if (this.replayId === sessionId) {
|
|
299
|
-
console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
|
|
300
301
|
this.replayId = null;
|
|
301
302
|
}
|
|
302
303
|
console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
|
|
@@ -345,7 +346,9 @@ var ProxyServer = class {
|
|
|
345
346
|
return;
|
|
346
347
|
}
|
|
347
348
|
if (!mode) {
|
|
348
|
-
throw new Error(
|
|
349
|
+
throw new Error(
|
|
350
|
+
"Mode parameter is required when cleanup is not specified"
|
|
351
|
+
);
|
|
349
352
|
}
|
|
350
353
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
351
354
|
this.clearModeTimeout();
|
|
@@ -428,12 +431,16 @@ var ProxyServer = class {
|
|
|
428
431
|
this.replayId = id;
|
|
429
432
|
this.recordingId = null;
|
|
430
433
|
this.currentSession = null;
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
434
|
+
const sessionState = this.getOrCreateReplaySession(id);
|
|
435
|
+
sessionState.servedRecordingIdsByKey.clear();
|
|
436
|
+
sessionState.sortedRecordingsByKey.clear();
|
|
437
|
+
const filePath = getRecordingPath(this.recordingsDir, id);
|
|
438
|
+
try {
|
|
439
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
440
|
+
console.log(`[REPLAY] Loaded recording session: ${id}`);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
|
|
443
|
+
sessionState.loadedSession = null;
|
|
437
444
|
}
|
|
438
445
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
439
446
|
}
|
|
@@ -447,21 +454,32 @@ var ProxyServer = class {
|
|
|
447
454
|
}, timeout);
|
|
448
455
|
}
|
|
449
456
|
async flushPendingRecordings() {
|
|
457
|
+
if (this.flushPromise) {
|
|
458
|
+
await this.flushPromise;
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
450
461
|
if (this.recordingPromises.length === 0) {
|
|
451
462
|
return;
|
|
452
463
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
|
|
464
|
+
this.flushPromise = (async () => {
|
|
465
|
+
try {
|
|
466
|
+
const results = await Promise.allSettled(this.recordingPromises);
|
|
467
|
+
if (this.currentSession) {
|
|
468
|
+
for (const result of results) {
|
|
469
|
+
if (result.status === "fulfilled" && result.value) {
|
|
470
|
+
this.currentSession.recordings.push(result.value);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
console.log(
|
|
474
|
+
`Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
|
|
475
|
+
);
|
|
458
476
|
}
|
|
477
|
+
this.recordingPromises = [];
|
|
478
|
+
} finally {
|
|
479
|
+
this.flushPromise = null;
|
|
459
480
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
this.recordingPromises = [];
|
|
481
|
+
})();
|
|
482
|
+
await this.flushPromise;
|
|
465
483
|
}
|
|
466
484
|
async saveCurrentSession() {
|
|
467
485
|
if (!this.currentSession) {
|
|
@@ -511,20 +529,25 @@ var ProxyServer = class {
|
|
|
511
529
|
);
|
|
512
530
|
return recordingId;
|
|
513
531
|
}
|
|
514
|
-
async ensureSessionLoaded(recordingId, filePath) {
|
|
515
|
-
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
516
|
-
if (!sessionState.loadedSession) {
|
|
517
|
-
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
518
|
-
console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
|
|
519
|
-
}
|
|
520
|
-
return sessionState;
|
|
521
|
-
}
|
|
522
532
|
getServedTracker(sessionState, key) {
|
|
523
533
|
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
524
534
|
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
525
535
|
}
|
|
526
536
|
return sessionState.servedRecordingIdsByKey.get(key);
|
|
527
537
|
}
|
|
538
|
+
getSortedRecordings(sessionState, key) {
|
|
539
|
+
if (sessionState.sortedRecordingsByKey.has(key)) {
|
|
540
|
+
return sessionState.sortedRecordingsByKey.get(key);
|
|
541
|
+
}
|
|
542
|
+
const session = sessionState.loadedSession;
|
|
543
|
+
const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
544
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
545
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
546
|
+
return aSeq - bSeq;
|
|
547
|
+
});
|
|
548
|
+
sessionState.sortedRecordingsByKey.set(key, sortedRecords);
|
|
549
|
+
return sortedRecords;
|
|
550
|
+
}
|
|
528
551
|
selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
|
|
529
552
|
for (const rec of recordsWithKey) {
|
|
530
553
|
if (!servedForThisKey.has(rec.recordingId)) {
|
|
@@ -545,18 +568,17 @@ var ProxyServer = class {
|
|
|
545
568
|
const key = getReqID(req);
|
|
546
569
|
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
547
570
|
try {
|
|
548
|
-
const sessionState =
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
571
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
572
|
+
if (!sessionState.loadedSession) {
|
|
573
|
+
const error = new Error(
|
|
574
|
+
`Recording session file not found: ${filePath}`
|
|
575
|
+
);
|
|
576
|
+
error.code = "ENOENT";
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
553
579
|
const servedForThisKey = this.getServedTracker(sessionState, key);
|
|
554
580
|
const host = req.headers.host || "unknown";
|
|
555
|
-
const recordsWithKey =
|
|
556
|
-
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
557
|
-
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
558
|
-
return aSeq - bSeq;
|
|
559
|
-
});
|
|
581
|
+
const recordsWithKey = this.getSortedRecordings(sessionState, key);
|
|
560
582
|
if (recordsWithKey.length === 0) {
|
|
561
583
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
562
584
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -767,7 +789,6 @@ var ProxyServer = class {
|
|
|
767
789
|
})();
|
|
768
790
|
});
|
|
769
791
|
this.recordingPromises.push(recordingPromise);
|
|
770
|
-
await recordingPromise;
|
|
771
792
|
}
|
|
772
793
|
handleUpgrade(req, socket, head) {
|
|
773
794
|
if (this.mode === Modes.replay) {
|
|
@@ -961,7 +982,6 @@ async function setProxyMode(mode, sessionId, timeout) {
|
|
|
961
982
|
throw new Error(`Failed to set proxy mode: ${text}`);
|
|
962
983
|
}
|
|
963
984
|
await response.json();
|
|
964
|
-
console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
|
|
965
985
|
} catch (error) {
|
|
966
986
|
console.error(`Error setting proxy mode:`, error);
|
|
967
987
|
throw error;
|
|
@@ -985,9 +1005,8 @@ async function cleanupSession(sessionId) {
|
|
|
985
1005
|
throw new Error(`Failed to cleanup session: ${text}`);
|
|
986
1006
|
}
|
|
987
1007
|
await response.json();
|
|
988
|
-
console.log(`Session cleaned up: ${sessionId}`);
|
|
989
1008
|
} catch (error) {
|
|
990
|
-
console.error(`Error cleaning up session
|
|
1009
|
+
console.error(`Error cleaning up session: ${sessionId}`, error);
|
|
991
1010
|
throw error;
|
|
992
1011
|
}
|
|
993
1012
|
}
|
|
@@ -1060,9 +1079,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
|
|
|
1060
1079
|
const harFileName = sessionId.replaceAll("/", "__");
|
|
1061
1080
|
const recordingsDir = await getRecordingsDir();
|
|
1062
1081
|
const harPath = path2.join(recordingsDir, `${harFileName}.har`);
|
|
1063
|
-
console.log(
|
|
1064
|
-
`[Client-Side Recording] Setting up HAR for session: ${sessionId}, mode: ${mode}, path: ${harPath}`
|
|
1065
|
-
);
|
|
1066
1082
|
try {
|
|
1067
1083
|
await page.routeFromHAR(harPath, {
|
|
1068
1084
|
url,
|
|
@@ -1095,41 +1111,27 @@ var playwrightProxy = {
|
|
|
1095
1111
|
await page.setExtraHTTPHeaders({
|
|
1096
1112
|
[RECORDING_ID_HEADER]: sessionId
|
|
1097
1113
|
});
|
|
1098
|
-
console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
|
|
1099
1114
|
await setProxyMode(mode, sessionId, timeout);
|
|
1100
|
-
console.log(`[Setup] Proxy mode set successfully`);
|
|
1101
1115
|
if (clientSideOptions?.url) {
|
|
1102
|
-
console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
|
|
1103
1116
|
await setupClientSideRecording(
|
|
1104
1117
|
page,
|
|
1105
1118
|
sessionId,
|
|
1106
1119
|
mode,
|
|
1107
1120
|
clientSideOptions.url
|
|
1108
1121
|
);
|
|
1109
|
-
console.log(`[Setup] Client-side recording setup complete`);
|
|
1110
1122
|
}
|
|
1111
1123
|
const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
|
|
1112
1124
|
const proxyUrl = `localhost:${proxyPort}`;
|
|
1113
|
-
console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
|
|
1114
1125
|
await page.route(
|
|
1115
1126
|
(url) => {
|
|
1116
1127
|
const urlStr = url.toString();
|
|
1117
1128
|
const matches = urlStr.includes(proxyUrl);
|
|
1118
|
-
if (matches) {
|
|
1119
|
-
console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
|
|
1120
|
-
}
|
|
1121
1129
|
return matches;
|
|
1122
1130
|
},
|
|
1123
1131
|
async (route) => {
|
|
1124
1132
|
try {
|
|
1125
|
-
const url = route.request().url();
|
|
1126
|
-
const method = route.request().method();
|
|
1127
1133
|
const headers = route.request().headers();
|
|
1128
|
-
const hadHeader = !!headers[RECORDING_ID_HEADER];
|
|
1129
1134
|
headers[RECORDING_ID_HEADER] = sessionId;
|
|
1130
|
-
console.log(
|
|
1131
|
-
`[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
|
|
1132
|
-
);
|
|
1133
1135
|
await route.continue({ headers });
|
|
1134
1136
|
} catch (error) {
|
|
1135
1137
|
console.error(
|
|
@@ -1142,7 +1144,6 @@ var playwrightProxy = {
|
|
|
1142
1144
|
{ times: Infinity }
|
|
1143
1145
|
// Ensure the handler applies to all matching requests
|
|
1144
1146
|
);
|
|
1145
|
-
console.log(`[Setup] Proxy route handler registered`);
|
|
1146
1147
|
const context = page.context();
|
|
1147
1148
|
const contextId = context._guid || "default";
|
|
1148
1149
|
const handlerKey = `cleanup_${contextId}`;
|
|
@@ -1150,9 +1151,6 @@ var playwrightProxy = {
|
|
|
1150
1151
|
globalThis[handlerKey] = true;
|
|
1151
1152
|
context.on("close", async () => {
|
|
1152
1153
|
try {
|
|
1153
|
-
console.log(
|
|
1154
|
-
`[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
|
|
1155
|
-
);
|
|
1156
1154
|
await cleanupSession(sessionId);
|
|
1157
1155
|
} catch (error) {
|
|
1158
1156
|
console.warn(
|
|
@@ -46,7 +46,6 @@ async function setProxyMode(mode, sessionId, timeout) {
|
|
|
46
46
|
throw new Error(`Failed to set proxy mode: ${text}`);
|
|
47
47
|
}
|
|
48
48
|
await response.json();
|
|
49
|
-
console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
|
|
50
49
|
} catch (error) {
|
|
51
50
|
console.error(`Error setting proxy mode:`, error);
|
|
52
51
|
throw error;
|
|
@@ -70,9 +69,8 @@ async function cleanupSession(sessionId) {
|
|
|
70
69
|
throw new Error(`Failed to cleanup session: ${text}`);
|
|
71
70
|
}
|
|
72
71
|
await response.json();
|
|
73
|
-
console.log(`Session cleaned up: ${sessionId}`);
|
|
74
72
|
} catch (error) {
|
|
75
|
-
console.error(`Error cleaning up session
|
|
73
|
+
console.error(`Error cleaning up session: ${sessionId}`, error);
|
|
76
74
|
throw error;
|
|
77
75
|
}
|
|
78
76
|
}
|
|
@@ -145,9 +143,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
|
|
|
145
143
|
const harFileName = sessionId.replaceAll("/", "__");
|
|
146
144
|
const recordingsDir = await getRecordingsDir();
|
|
147
145
|
const harPath = path__default.default.join(recordingsDir, `${harFileName}.har`);
|
|
148
|
-
console.log(
|
|
149
|
-
`[Client-Side Recording] Setting up HAR for session: ${sessionId}, mode: ${mode}, path: ${harPath}`
|
|
150
|
-
);
|
|
151
146
|
try {
|
|
152
147
|
await page.routeFromHAR(harPath, {
|
|
153
148
|
url,
|
|
@@ -180,41 +175,27 @@ var playwrightProxy = {
|
|
|
180
175
|
await page.setExtraHTTPHeaders({
|
|
181
176
|
[RECORDING_ID_HEADER]: sessionId
|
|
182
177
|
});
|
|
183
|
-
console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
|
|
184
178
|
await setProxyMode(mode, sessionId, timeout);
|
|
185
|
-
console.log(`[Setup] Proxy mode set successfully`);
|
|
186
179
|
if (clientSideOptions?.url) {
|
|
187
|
-
console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
|
|
188
180
|
await setupClientSideRecording(
|
|
189
181
|
page,
|
|
190
182
|
sessionId,
|
|
191
183
|
mode,
|
|
192
184
|
clientSideOptions.url
|
|
193
185
|
);
|
|
194
|
-
console.log(`[Setup] Client-side recording setup complete`);
|
|
195
186
|
}
|
|
196
187
|
const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
|
|
197
188
|
const proxyUrl = `localhost:${proxyPort}`;
|
|
198
|
-
console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
|
|
199
189
|
await page.route(
|
|
200
190
|
(url) => {
|
|
201
191
|
const urlStr = url.toString();
|
|
202
192
|
const matches = urlStr.includes(proxyUrl);
|
|
203
|
-
if (matches) {
|
|
204
|
-
console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
|
|
205
|
-
}
|
|
206
193
|
return matches;
|
|
207
194
|
},
|
|
208
195
|
async (route) => {
|
|
209
196
|
try {
|
|
210
|
-
const url = route.request().url();
|
|
211
|
-
const method = route.request().method();
|
|
212
197
|
const headers = route.request().headers();
|
|
213
|
-
const hadHeader = !!headers[RECORDING_ID_HEADER];
|
|
214
198
|
headers[RECORDING_ID_HEADER] = sessionId;
|
|
215
|
-
console.log(
|
|
216
|
-
`[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
|
|
217
|
-
);
|
|
218
199
|
await route.continue({ headers });
|
|
219
200
|
} catch (error) {
|
|
220
201
|
console.error(
|
|
@@ -227,7 +208,6 @@ var playwrightProxy = {
|
|
|
227
208
|
{ times: Infinity }
|
|
228
209
|
// Ensure the handler applies to all matching requests
|
|
229
210
|
);
|
|
230
|
-
console.log(`[Setup] Proxy route handler registered`);
|
|
231
211
|
const context = page.context();
|
|
232
212
|
const contextId = context._guid || "default";
|
|
233
213
|
const handlerKey = `cleanup_${contextId}`;
|
|
@@ -235,9 +215,6 @@ var playwrightProxy = {
|
|
|
235
215
|
globalThis[handlerKey] = true;
|
|
236
216
|
context.on("close", async () => {
|
|
237
217
|
try {
|
|
238
|
-
console.log(
|
|
239
|
-
`[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
|
|
240
|
-
);
|
|
241
218
|
await cleanupSession(sessionId);
|
|
242
219
|
} catch (error) {
|
|
243
220
|
console.warn(
|
|
@@ -40,7 +40,6 @@ async function setProxyMode(mode, sessionId, timeout) {
|
|
|
40
40
|
throw new Error(`Failed to set proxy mode: ${text}`);
|
|
41
41
|
}
|
|
42
42
|
await response.json();
|
|
43
|
-
console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
|
|
44
43
|
} catch (error) {
|
|
45
44
|
console.error(`Error setting proxy mode:`, error);
|
|
46
45
|
throw error;
|
|
@@ -64,9 +63,8 @@ async function cleanupSession(sessionId) {
|
|
|
64
63
|
throw new Error(`Failed to cleanup session: ${text}`);
|
|
65
64
|
}
|
|
66
65
|
await response.json();
|
|
67
|
-
console.log(`Session cleaned up: ${sessionId}`);
|
|
68
66
|
} catch (error) {
|
|
69
|
-
console.error(`Error cleaning up session
|
|
67
|
+
console.error(`Error cleaning up session: ${sessionId}`, error);
|
|
70
68
|
throw error;
|
|
71
69
|
}
|
|
72
70
|
}
|
|
@@ -139,9 +137,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
|
|
|
139
137
|
const harFileName = sessionId.replaceAll("/", "__");
|
|
140
138
|
const recordingsDir = await getRecordingsDir();
|
|
141
139
|
const harPath = path.join(recordingsDir, `${harFileName}.har`);
|
|
142
|
-
console.log(
|
|
143
|
-
`[Client-Side Recording] Setting up HAR for session: ${sessionId}, mode: ${mode}, path: ${harPath}`
|
|
144
|
-
);
|
|
145
140
|
try {
|
|
146
141
|
await page.routeFromHAR(harPath, {
|
|
147
142
|
url,
|
|
@@ -174,41 +169,27 @@ var playwrightProxy = {
|
|
|
174
169
|
await page.setExtraHTTPHeaders({
|
|
175
170
|
[RECORDING_ID_HEADER]: sessionId
|
|
176
171
|
});
|
|
177
|
-
console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
|
|
178
172
|
await setProxyMode(mode, sessionId, timeout);
|
|
179
|
-
console.log(`[Setup] Proxy mode set successfully`);
|
|
180
173
|
if (clientSideOptions?.url) {
|
|
181
|
-
console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
|
|
182
174
|
await setupClientSideRecording(
|
|
183
175
|
page,
|
|
184
176
|
sessionId,
|
|
185
177
|
mode,
|
|
186
178
|
clientSideOptions.url
|
|
187
179
|
);
|
|
188
|
-
console.log(`[Setup] Client-side recording setup complete`);
|
|
189
180
|
}
|
|
190
181
|
const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
|
|
191
182
|
const proxyUrl = `localhost:${proxyPort}`;
|
|
192
|
-
console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
|
|
193
183
|
await page.route(
|
|
194
184
|
(url) => {
|
|
195
185
|
const urlStr = url.toString();
|
|
196
186
|
const matches = urlStr.includes(proxyUrl);
|
|
197
|
-
if (matches) {
|
|
198
|
-
console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
|
|
199
|
-
}
|
|
200
187
|
return matches;
|
|
201
188
|
},
|
|
202
189
|
async (route) => {
|
|
203
190
|
try {
|
|
204
|
-
const url = route.request().url();
|
|
205
|
-
const method = route.request().method();
|
|
206
191
|
const headers = route.request().headers();
|
|
207
|
-
const hadHeader = !!headers[RECORDING_ID_HEADER];
|
|
208
192
|
headers[RECORDING_ID_HEADER] = sessionId;
|
|
209
|
-
console.log(
|
|
210
|
-
`[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
|
|
211
|
-
);
|
|
212
193
|
await route.continue({ headers });
|
|
213
194
|
} catch (error) {
|
|
214
195
|
console.error(
|
|
@@ -221,7 +202,6 @@ var playwrightProxy = {
|
|
|
221
202
|
{ times: Infinity }
|
|
222
203
|
// Ensure the handler applies to all matching requests
|
|
223
204
|
);
|
|
224
|
-
console.log(`[Setup] Proxy route handler registered`);
|
|
225
205
|
const context = page.context();
|
|
226
206
|
const contextId = context._guid || "default";
|
|
227
207
|
const handlerKey = `cleanup_${contextId}`;
|
|
@@ -229,9 +209,6 @@ var playwrightProxy = {
|
|
|
229
209
|
globalThis[handlerKey] = true;
|
|
230
210
|
context.on("close", async () => {
|
|
231
211
|
try {
|
|
232
|
-
console.log(
|
|
233
|
-
`[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
|
|
234
|
-
);
|
|
235
212
|
await cleanupSession(sessionId);
|
|
236
213
|
} catch (error) {
|
|
237
214
|
console.warn(
|
package/dist/proxy.js
CHANGED
|
@@ -172,6 +172,8 @@ var ProxyServer = class {
|
|
|
172
172
|
// Track multiple concurrent replay sessions by recording ID
|
|
173
173
|
recordingPromises;
|
|
174
174
|
// Stack of promises that resolve to completed recordings
|
|
175
|
+
flushPromise;
|
|
176
|
+
// Promise for in-progress flush operation
|
|
175
177
|
constructor(targets2, recordingsDir2) {
|
|
176
178
|
this.targets = targets2;
|
|
177
179
|
this.currentTargetIndex = 0;
|
|
@@ -185,6 +187,7 @@ var ProxyServer = class {
|
|
|
185
187
|
this.recordingsDir = recordingsDir2;
|
|
186
188
|
this.replaySessions = /* @__PURE__ */ new Map();
|
|
187
189
|
this.recordingPromises = [];
|
|
190
|
+
this.flushPromise = null;
|
|
188
191
|
this.proxy = httpProxy.createProxyServer({
|
|
189
192
|
secure: false,
|
|
190
193
|
changeOrigin: true,
|
|
@@ -307,7 +310,8 @@ var ProxyServer = class {
|
|
|
307
310
|
recordingId,
|
|
308
311
|
servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
|
|
309
312
|
loadedSession: null,
|
|
310
|
-
lastAccessTime: Date.now()
|
|
313
|
+
lastAccessTime: Date.now(),
|
|
314
|
+
sortedRecordingsByKey: /* @__PURE__ */ new Map()
|
|
311
315
|
};
|
|
312
316
|
this.replaySessions.set(recordingId, session);
|
|
313
317
|
console.log(
|
|
@@ -322,17 +326,14 @@ var ProxyServer = class {
|
|
|
322
326
|
*/
|
|
323
327
|
async cleanupSession(sessionId) {
|
|
324
328
|
if (this.replaySessions.has(sessionId)) {
|
|
325
|
-
console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
|
|
326
329
|
this.replaySessions.delete(sessionId);
|
|
327
330
|
}
|
|
328
331
|
if (this.recordingId === sessionId) {
|
|
329
|
-
console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
|
|
330
332
|
await this.saveCurrentSession();
|
|
331
333
|
this.currentSession = null;
|
|
332
334
|
this.recordingId = null;
|
|
333
335
|
}
|
|
334
336
|
if (this.replayId === sessionId) {
|
|
335
|
-
console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
|
|
336
337
|
this.replayId = null;
|
|
337
338
|
}
|
|
338
339
|
console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
|
|
@@ -381,7 +382,9 @@ var ProxyServer = class {
|
|
|
381
382
|
return;
|
|
382
383
|
}
|
|
383
384
|
if (!mode) {
|
|
384
|
-
throw new Error(
|
|
385
|
+
throw new Error(
|
|
386
|
+
"Mode parameter is required when cleanup is not specified"
|
|
387
|
+
);
|
|
385
388
|
}
|
|
386
389
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
387
390
|
this.clearModeTimeout();
|
|
@@ -464,12 +467,16 @@ var ProxyServer = class {
|
|
|
464
467
|
this.replayId = id;
|
|
465
468
|
this.recordingId = null;
|
|
466
469
|
this.currentSession = null;
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
470
|
+
const sessionState = this.getOrCreateReplaySession(id);
|
|
471
|
+
sessionState.servedRecordingIdsByKey.clear();
|
|
472
|
+
sessionState.sortedRecordingsByKey.clear();
|
|
473
|
+
const filePath = getRecordingPath(this.recordingsDir, id);
|
|
474
|
+
try {
|
|
475
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
476
|
+
console.log(`[REPLAY] Loaded recording session: ${id}`);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
|
|
479
|
+
sessionState.loadedSession = null;
|
|
473
480
|
}
|
|
474
481
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
475
482
|
}
|
|
@@ -483,21 +490,32 @@ var ProxyServer = class {
|
|
|
483
490
|
}, timeout);
|
|
484
491
|
}
|
|
485
492
|
async flushPendingRecordings() {
|
|
493
|
+
if (this.flushPromise) {
|
|
494
|
+
await this.flushPromise;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
486
497
|
if (this.recordingPromises.length === 0) {
|
|
487
498
|
return;
|
|
488
499
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (
|
|
493
|
-
|
|
500
|
+
this.flushPromise = (async () => {
|
|
501
|
+
try {
|
|
502
|
+
const results = await Promise.allSettled(this.recordingPromises);
|
|
503
|
+
if (this.currentSession) {
|
|
504
|
+
for (const result of results) {
|
|
505
|
+
if (result.status === "fulfilled" && result.value) {
|
|
506
|
+
this.currentSession.recordings.push(result.value);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
console.log(
|
|
510
|
+
`Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
|
|
511
|
+
);
|
|
494
512
|
}
|
|
513
|
+
this.recordingPromises = [];
|
|
514
|
+
} finally {
|
|
515
|
+
this.flushPromise = null;
|
|
495
516
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
this.recordingPromises = [];
|
|
517
|
+
})();
|
|
518
|
+
await this.flushPromise;
|
|
501
519
|
}
|
|
502
520
|
async saveCurrentSession() {
|
|
503
521
|
if (!this.currentSession) {
|
|
@@ -547,20 +565,25 @@ var ProxyServer = class {
|
|
|
547
565
|
);
|
|
548
566
|
return recordingId;
|
|
549
567
|
}
|
|
550
|
-
async ensureSessionLoaded(recordingId, filePath) {
|
|
551
|
-
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
552
|
-
if (!sessionState.loadedSession) {
|
|
553
|
-
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
554
|
-
console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
|
|
555
|
-
}
|
|
556
|
-
return sessionState;
|
|
557
|
-
}
|
|
558
568
|
getServedTracker(sessionState, key) {
|
|
559
569
|
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
560
570
|
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
561
571
|
}
|
|
562
572
|
return sessionState.servedRecordingIdsByKey.get(key);
|
|
563
573
|
}
|
|
574
|
+
getSortedRecordings(sessionState, key) {
|
|
575
|
+
if (sessionState.sortedRecordingsByKey.has(key)) {
|
|
576
|
+
return sessionState.sortedRecordingsByKey.get(key);
|
|
577
|
+
}
|
|
578
|
+
const session = sessionState.loadedSession;
|
|
579
|
+
const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
580
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
581
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
582
|
+
return aSeq - bSeq;
|
|
583
|
+
});
|
|
584
|
+
sessionState.sortedRecordingsByKey.set(key, sortedRecords);
|
|
585
|
+
return sortedRecords;
|
|
586
|
+
}
|
|
564
587
|
selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
|
|
565
588
|
for (const rec of recordsWithKey) {
|
|
566
589
|
if (!servedForThisKey.has(rec.recordingId)) {
|
|
@@ -581,18 +604,17 @@ var ProxyServer = class {
|
|
|
581
604
|
const key = getReqID(req);
|
|
582
605
|
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
583
606
|
try {
|
|
584
|
-
const sessionState =
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
607
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
608
|
+
if (!sessionState.loadedSession) {
|
|
609
|
+
const error = new Error(
|
|
610
|
+
`Recording session file not found: ${filePath}`
|
|
611
|
+
);
|
|
612
|
+
error.code = "ENOENT";
|
|
613
|
+
throw error;
|
|
614
|
+
}
|
|
589
615
|
const servedForThisKey = this.getServedTracker(sessionState, key);
|
|
590
616
|
const host = req.headers.host || "unknown";
|
|
591
|
-
const recordsWithKey =
|
|
592
|
-
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
593
|
-
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
594
|
-
return aSeq - bSeq;
|
|
595
|
-
});
|
|
617
|
+
const recordsWithKey = this.getSortedRecordings(sessionState, key);
|
|
596
618
|
if (recordsWithKey.length === 0) {
|
|
597
619
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
598
620
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -803,7 +825,6 @@ var ProxyServer = class {
|
|
|
803
825
|
})();
|
|
804
826
|
});
|
|
805
827
|
this.recordingPromises.push(recordingPromise);
|
|
806
|
-
await recordingPromise;
|
|
807
828
|
}
|
|
808
829
|
handleUpgrade(req, socket, head) {
|
|
809
830
|
if (this.mode === Modes.replay) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-proxy-recorder",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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",
|