test-proxy-recorder 0.3.2 → 0.3.4
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 +67 -73
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +67 -73
- package/dist/playwright/index.cjs +1 -24
- package/dist/playwright/index.mjs +1 -24
- package/dist/proxy.js +85 -68
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -131,8 +131,7 @@ function sendJsonResponse(res, statusCode, data) {
|
|
|
131
131
|
|
|
132
132
|
// src/ProxyServer.ts
|
|
133
133
|
var ProxyServer = class {
|
|
134
|
-
|
|
135
|
-
currentTargetIndex;
|
|
134
|
+
target;
|
|
136
135
|
mode;
|
|
137
136
|
recordingId;
|
|
138
137
|
replayId;
|
|
@@ -148,9 +147,10 @@ var ProxyServer = class {
|
|
|
148
147
|
// Track multiple concurrent replay sessions by recording ID
|
|
149
148
|
recordingPromises;
|
|
150
149
|
// Stack of promises that resolve to completed recordings
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
flushPromise;
|
|
151
|
+
// Promise for in-progress flush operation
|
|
152
|
+
constructor(target, recordingsDir) {
|
|
153
|
+
this.target = target;
|
|
154
154
|
this.mode = Modes.transparent;
|
|
155
155
|
this.recordingId = null;
|
|
156
156
|
this.recordingIdCounter = 0;
|
|
@@ -161,6 +161,7 @@ var ProxyServer = class {
|
|
|
161
161
|
this.recordingsDir = recordingsDir;
|
|
162
162
|
this.replaySessions = /* @__PURE__ */ new Map();
|
|
163
163
|
this.recordingPromises = [];
|
|
164
|
+
this.flushPromise = null;
|
|
164
165
|
this.proxy = httpProxy__default.default.createProxyServer({
|
|
165
166
|
secure: false,
|
|
166
167
|
changeOrigin: true,
|
|
@@ -222,9 +223,7 @@ var ProxyServer = class {
|
|
|
222
223
|
Object.assign(proxyRes.headers, corsHeaders);
|
|
223
224
|
}
|
|
224
225
|
getTarget() {
|
|
225
|
-
|
|
226
|
-
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
227
|
-
return target;
|
|
226
|
+
return this.target;
|
|
228
227
|
}
|
|
229
228
|
/**
|
|
230
229
|
* Extract recording ID from custom HTTP header
|
|
@@ -283,7 +282,8 @@ var ProxyServer = class {
|
|
|
283
282
|
recordingId,
|
|
284
283
|
servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
|
|
285
284
|
loadedSession: null,
|
|
286
|
-
lastAccessTime: Date.now()
|
|
285
|
+
lastAccessTime: Date.now(),
|
|
286
|
+
sortedRecordingsByKey: /* @__PURE__ */ new Map()
|
|
287
287
|
};
|
|
288
288
|
this.replaySessions.set(recordingId, session);
|
|
289
289
|
console.log(
|
|
@@ -298,17 +298,14 @@ var ProxyServer = class {
|
|
|
298
298
|
*/
|
|
299
299
|
async cleanupSession(sessionId) {
|
|
300
300
|
if (this.replaySessions.has(sessionId)) {
|
|
301
|
-
console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
|
|
302
301
|
this.replaySessions.delete(sessionId);
|
|
303
302
|
}
|
|
304
303
|
if (this.recordingId === sessionId) {
|
|
305
|
-
console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
|
|
306
304
|
await this.saveCurrentSession();
|
|
307
305
|
this.currentSession = null;
|
|
308
306
|
this.recordingId = null;
|
|
309
307
|
}
|
|
310
308
|
if (this.replayId === sessionId) {
|
|
311
|
-
console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
|
|
312
309
|
this.replayId = null;
|
|
313
310
|
}
|
|
314
311
|
console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
|
|
@@ -357,7 +354,9 @@ var ProxyServer = class {
|
|
|
357
354
|
return;
|
|
358
355
|
}
|
|
359
356
|
if (!mode) {
|
|
360
|
-
throw new Error(
|
|
357
|
+
throw new Error(
|
|
358
|
+
"Mode parameter is required when cleanup is not specified"
|
|
359
|
+
);
|
|
361
360
|
}
|
|
362
361
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
363
362
|
this.clearModeTimeout();
|
|
@@ -440,12 +439,16 @@ var ProxyServer = class {
|
|
|
440
439
|
this.replayId = id;
|
|
441
440
|
this.recordingId = null;
|
|
442
441
|
this.currentSession = null;
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
442
|
+
const sessionState = this.getOrCreateReplaySession(id);
|
|
443
|
+
sessionState.servedRecordingIdsByKey.clear();
|
|
444
|
+
sessionState.sortedRecordingsByKey.clear();
|
|
445
|
+
const filePath = getRecordingPath(this.recordingsDir, id);
|
|
446
|
+
try {
|
|
447
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
448
|
+
console.log(`[REPLAY] Loaded recording session: ${id}`);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
|
|
451
|
+
sessionState.loadedSession = null;
|
|
449
452
|
}
|
|
450
453
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
451
454
|
}
|
|
@@ -459,21 +462,32 @@ var ProxyServer = class {
|
|
|
459
462
|
}, timeout);
|
|
460
463
|
}
|
|
461
464
|
async flushPendingRecordings() {
|
|
465
|
+
if (this.flushPromise) {
|
|
466
|
+
await this.flushPromise;
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
462
469
|
if (this.recordingPromises.length === 0) {
|
|
463
470
|
return;
|
|
464
471
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (
|
|
469
|
-
|
|
472
|
+
this.flushPromise = (async () => {
|
|
473
|
+
try {
|
|
474
|
+
const results = await Promise.allSettled(this.recordingPromises);
|
|
475
|
+
if (this.currentSession) {
|
|
476
|
+
for (const result of results) {
|
|
477
|
+
if (result.status === "fulfilled" && result.value) {
|
|
478
|
+
this.currentSession.recordings.push(result.value);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
console.log(
|
|
482
|
+
`Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
|
|
483
|
+
);
|
|
470
484
|
}
|
|
485
|
+
this.recordingPromises = [];
|
|
486
|
+
} finally {
|
|
487
|
+
this.flushPromise = null;
|
|
471
488
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
this.recordingPromises = [];
|
|
489
|
+
})();
|
|
490
|
+
await this.flushPromise;
|
|
477
491
|
}
|
|
478
492
|
async saveCurrentSession() {
|
|
479
493
|
if (!this.currentSession) {
|
|
@@ -523,20 +537,25 @@ var ProxyServer = class {
|
|
|
523
537
|
);
|
|
524
538
|
return recordingId;
|
|
525
539
|
}
|
|
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
540
|
getServedTracker(sessionState, key) {
|
|
535
541
|
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
536
542
|
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
537
543
|
}
|
|
538
544
|
return sessionState.servedRecordingIdsByKey.get(key);
|
|
539
545
|
}
|
|
546
|
+
getSortedRecordings(sessionState, key) {
|
|
547
|
+
if (sessionState.sortedRecordingsByKey.has(key)) {
|
|
548
|
+
return sessionState.sortedRecordingsByKey.get(key);
|
|
549
|
+
}
|
|
550
|
+
const session = sessionState.loadedSession;
|
|
551
|
+
const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
552
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
553
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
554
|
+
return aSeq - bSeq;
|
|
555
|
+
});
|
|
556
|
+
sessionState.sortedRecordingsByKey.set(key, sortedRecords);
|
|
557
|
+
return sortedRecords;
|
|
558
|
+
}
|
|
540
559
|
selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
|
|
541
560
|
for (const rec of recordsWithKey) {
|
|
542
561
|
if (!servedForThisKey.has(rec.recordingId)) {
|
|
@@ -557,18 +576,17 @@ var ProxyServer = class {
|
|
|
557
576
|
const key = getReqID(req);
|
|
558
577
|
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
559
578
|
try {
|
|
560
|
-
const sessionState =
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
579
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
580
|
+
if (!sessionState.loadedSession) {
|
|
581
|
+
const error = new Error(
|
|
582
|
+
`Recording session file not found: ${filePath}`
|
|
583
|
+
);
|
|
584
|
+
error.code = "ENOENT";
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
565
587
|
const servedForThisKey = this.getServedTracker(sessionState, key);
|
|
566
588
|
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
|
-
});
|
|
589
|
+
const recordsWithKey = this.getSortedRecordings(sessionState, key);
|
|
572
590
|
if (recordsWithKey.length === 0) {
|
|
573
591
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
574
592
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -779,7 +797,6 @@ var ProxyServer = class {
|
|
|
779
797
|
})();
|
|
780
798
|
});
|
|
781
799
|
this.recordingPromises.push(recordingPromise);
|
|
782
|
-
await recordingPromise;
|
|
783
800
|
}
|
|
784
801
|
handleUpgrade(req, socket, head) {
|
|
785
802
|
if (this.mode === Modes.replay) {
|
|
@@ -938,7 +955,7 @@ var ProxyServer = class {
|
|
|
938
955
|
logServerStartup(port) {
|
|
939
956
|
console.log(`Proxy server running on http://localhost:${port}`);
|
|
940
957
|
console.log(`Mode: ${this.mode}`);
|
|
941
|
-
console.log(`
|
|
958
|
+
console.log(`Target: ${this.target}`);
|
|
942
959
|
console.log(
|
|
943
960
|
`Control endpoint: http://localhost:${port}${CONTROL_ENDPOINT}`
|
|
944
961
|
);
|
|
@@ -973,7 +990,6 @@ async function setProxyMode(mode, sessionId, timeout) {
|
|
|
973
990
|
throw new Error(`Failed to set proxy mode: ${text}`);
|
|
974
991
|
}
|
|
975
992
|
await response.json();
|
|
976
|
-
console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
|
|
977
993
|
} catch (error) {
|
|
978
994
|
console.error(`Error setting proxy mode:`, error);
|
|
979
995
|
throw error;
|
|
@@ -997,9 +1013,8 @@ async function cleanupSession(sessionId) {
|
|
|
997
1013
|
throw new Error(`Failed to cleanup session: ${text}`);
|
|
998
1014
|
}
|
|
999
1015
|
await response.json();
|
|
1000
|
-
console.log(`Session cleaned up: ${sessionId}`);
|
|
1001
1016
|
} catch (error) {
|
|
1002
|
-
console.error(`Error cleaning up session
|
|
1017
|
+
console.error(`Error cleaning up session: ${sessionId}`, error);
|
|
1003
1018
|
throw error;
|
|
1004
1019
|
}
|
|
1005
1020
|
}
|
|
@@ -1072,9 +1087,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
|
|
|
1072
1087
|
const harFileName = sessionId.replaceAll("/", "__");
|
|
1073
1088
|
const recordingsDir = await getRecordingsDir();
|
|
1074
1089
|
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
1090
|
try {
|
|
1079
1091
|
await page.routeFromHAR(harPath, {
|
|
1080
1092
|
url,
|
|
@@ -1107,41 +1119,27 @@ var playwrightProxy = {
|
|
|
1107
1119
|
await page.setExtraHTTPHeaders({
|
|
1108
1120
|
[RECORDING_ID_HEADER]: sessionId
|
|
1109
1121
|
});
|
|
1110
|
-
console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
|
|
1111
1122
|
await setProxyMode(mode, sessionId, timeout);
|
|
1112
|
-
console.log(`[Setup] Proxy mode set successfully`);
|
|
1113
1123
|
if (clientSideOptions?.url) {
|
|
1114
|
-
console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
|
|
1115
1124
|
await setupClientSideRecording(
|
|
1116
1125
|
page,
|
|
1117
1126
|
sessionId,
|
|
1118
1127
|
mode,
|
|
1119
1128
|
clientSideOptions.url
|
|
1120
1129
|
);
|
|
1121
|
-
console.log(`[Setup] Client-side recording setup complete`);
|
|
1122
1130
|
}
|
|
1123
1131
|
const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
|
|
1124
1132
|
const proxyUrl = `localhost:${proxyPort}`;
|
|
1125
|
-
console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
|
|
1126
1133
|
await page.route(
|
|
1127
1134
|
(url) => {
|
|
1128
1135
|
const urlStr = url.toString();
|
|
1129
1136
|
const matches = urlStr.includes(proxyUrl);
|
|
1130
|
-
if (matches) {
|
|
1131
|
-
console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
|
|
1132
|
-
}
|
|
1133
1137
|
return matches;
|
|
1134
1138
|
},
|
|
1135
1139
|
async (route) => {
|
|
1136
1140
|
try {
|
|
1137
|
-
const url = route.request().url();
|
|
1138
|
-
const method = route.request().method();
|
|
1139
1141
|
const headers = route.request().headers();
|
|
1140
|
-
const hadHeader = !!headers[RECORDING_ID_HEADER];
|
|
1141
1142
|
headers[RECORDING_ID_HEADER] = sessionId;
|
|
1142
|
-
console.log(
|
|
1143
|
-
`[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
|
|
1144
|
-
);
|
|
1145
1143
|
await route.continue({ headers });
|
|
1146
1144
|
} catch (error) {
|
|
1147
1145
|
console.error(
|
|
@@ -1154,7 +1152,6 @@ var playwrightProxy = {
|
|
|
1154
1152
|
{ times: Infinity }
|
|
1155
1153
|
// Ensure the handler applies to all matching requests
|
|
1156
1154
|
);
|
|
1157
|
-
console.log(`[Setup] Proxy route handler registered`);
|
|
1158
1155
|
const context = page.context();
|
|
1159
1156
|
const contextId = context._guid || "default";
|
|
1160
1157
|
const handlerKey = `cleanup_${contextId}`;
|
|
@@ -1162,9 +1159,6 @@ var playwrightProxy = {
|
|
|
1162
1159
|
globalThis[handlerKey] = true;
|
|
1163
1160
|
context.on("close", async () => {
|
|
1164
1161
|
try {
|
|
1165
|
-
console.log(
|
|
1166
|
-
`[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
|
|
1167
|
-
);
|
|
1168
1162
|
await cleanupSession(sessionId);
|
|
1169
1163
|
} catch (error) {
|
|
1170
1164
|
console.warn(
|
package/dist/index.d.cts
CHANGED
|
@@ -4,8 +4,7 @@ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording
|
|
|
4
4
|
import '@playwright/test';
|
|
5
5
|
|
|
6
6
|
declare class ProxyServer {
|
|
7
|
-
private
|
|
8
|
-
private currentTargetIndex;
|
|
7
|
+
private target;
|
|
9
8
|
private mode;
|
|
10
9
|
private recordingId;
|
|
11
10
|
private replayId;
|
|
@@ -17,7 +16,8 @@ declare class ProxyServer {
|
|
|
17
16
|
private sequenceCounterByKey;
|
|
18
17
|
private replaySessions;
|
|
19
18
|
private recordingPromises;
|
|
20
|
-
|
|
19
|
+
private flushPromise;
|
|
20
|
+
constructor(target: string, recordingsDir: string);
|
|
21
21
|
init(): Promise<void>;
|
|
22
22
|
listen(port: number): http.Server;
|
|
23
23
|
private setupProxyEventHandlers;
|
|
@@ -73,8 +73,8 @@ declare class ProxyServer {
|
|
|
73
73
|
private flushPendingRecordings;
|
|
74
74
|
private saveCurrentSession;
|
|
75
75
|
private getRecordingIdOrError;
|
|
76
|
-
private ensureSessionLoaded;
|
|
77
76
|
private getServedTracker;
|
|
77
|
+
private getSortedRecordings;
|
|
78
78
|
private selectReplayRecord;
|
|
79
79
|
private handleReplayRequest;
|
|
80
80
|
private handleReplayError;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,7 @@ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording
|
|
|
4
4
|
import '@playwright/test';
|
|
5
5
|
|
|
6
6
|
declare class ProxyServer {
|
|
7
|
-
private
|
|
8
|
-
private currentTargetIndex;
|
|
7
|
+
private target;
|
|
9
8
|
private mode;
|
|
10
9
|
private recordingId;
|
|
11
10
|
private replayId;
|
|
@@ -17,7 +16,8 @@ declare class ProxyServer {
|
|
|
17
16
|
private sequenceCounterByKey;
|
|
18
17
|
private replaySessions;
|
|
19
18
|
private recordingPromises;
|
|
20
|
-
|
|
19
|
+
private flushPromise;
|
|
20
|
+
constructor(target: string, recordingsDir: string);
|
|
21
21
|
init(): Promise<void>;
|
|
22
22
|
listen(port: number): http.Server;
|
|
23
23
|
private setupProxyEventHandlers;
|
|
@@ -73,8 +73,8 @@ declare class ProxyServer {
|
|
|
73
73
|
private flushPendingRecordings;
|
|
74
74
|
private saveCurrentSession;
|
|
75
75
|
private getRecordingIdOrError;
|
|
76
|
-
private ensureSessionLoaded;
|
|
77
76
|
private getServedTracker;
|
|
77
|
+
private getSortedRecordings;
|
|
78
78
|
private selectReplayRecord;
|
|
79
79
|
private handleReplayRequest;
|
|
80
80
|
private handleReplayError;
|
package/dist/index.mjs
CHANGED
|
@@ -119,8 +119,7 @@ function sendJsonResponse(res, statusCode, data) {
|
|
|
119
119
|
|
|
120
120
|
// src/ProxyServer.ts
|
|
121
121
|
var ProxyServer = class {
|
|
122
|
-
|
|
123
|
-
currentTargetIndex;
|
|
122
|
+
target;
|
|
124
123
|
mode;
|
|
125
124
|
recordingId;
|
|
126
125
|
replayId;
|
|
@@ -136,9 +135,10 @@ var ProxyServer = class {
|
|
|
136
135
|
// Track multiple concurrent replay sessions by recording ID
|
|
137
136
|
recordingPromises;
|
|
138
137
|
// Stack of promises that resolve to completed recordings
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
flushPromise;
|
|
139
|
+
// Promise for in-progress flush operation
|
|
140
|
+
constructor(target, recordingsDir) {
|
|
141
|
+
this.target = target;
|
|
142
142
|
this.mode = Modes.transparent;
|
|
143
143
|
this.recordingId = null;
|
|
144
144
|
this.recordingIdCounter = 0;
|
|
@@ -149,6 +149,7 @@ var ProxyServer = class {
|
|
|
149
149
|
this.recordingsDir = recordingsDir;
|
|
150
150
|
this.replaySessions = /* @__PURE__ */ new Map();
|
|
151
151
|
this.recordingPromises = [];
|
|
152
|
+
this.flushPromise = null;
|
|
152
153
|
this.proxy = httpProxy.createProxyServer({
|
|
153
154
|
secure: false,
|
|
154
155
|
changeOrigin: true,
|
|
@@ -210,9 +211,7 @@ var ProxyServer = class {
|
|
|
210
211
|
Object.assign(proxyRes.headers, corsHeaders);
|
|
211
212
|
}
|
|
212
213
|
getTarget() {
|
|
213
|
-
|
|
214
|
-
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
215
|
-
return target;
|
|
214
|
+
return this.target;
|
|
216
215
|
}
|
|
217
216
|
/**
|
|
218
217
|
* Extract recording ID from custom HTTP header
|
|
@@ -271,7 +270,8 @@ var ProxyServer = class {
|
|
|
271
270
|
recordingId,
|
|
272
271
|
servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
|
|
273
272
|
loadedSession: null,
|
|
274
|
-
lastAccessTime: Date.now()
|
|
273
|
+
lastAccessTime: Date.now(),
|
|
274
|
+
sortedRecordingsByKey: /* @__PURE__ */ new Map()
|
|
275
275
|
};
|
|
276
276
|
this.replaySessions.set(recordingId, session);
|
|
277
277
|
console.log(
|
|
@@ -286,17 +286,14 @@ var ProxyServer = class {
|
|
|
286
286
|
*/
|
|
287
287
|
async cleanupSession(sessionId) {
|
|
288
288
|
if (this.replaySessions.has(sessionId)) {
|
|
289
|
-
console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
|
|
290
289
|
this.replaySessions.delete(sessionId);
|
|
291
290
|
}
|
|
292
291
|
if (this.recordingId === sessionId) {
|
|
293
|
-
console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
|
|
294
292
|
await this.saveCurrentSession();
|
|
295
293
|
this.currentSession = null;
|
|
296
294
|
this.recordingId = null;
|
|
297
295
|
}
|
|
298
296
|
if (this.replayId === sessionId) {
|
|
299
|
-
console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
|
|
300
297
|
this.replayId = null;
|
|
301
298
|
}
|
|
302
299
|
console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
|
|
@@ -345,7 +342,9 @@ var ProxyServer = class {
|
|
|
345
342
|
return;
|
|
346
343
|
}
|
|
347
344
|
if (!mode) {
|
|
348
|
-
throw new Error(
|
|
345
|
+
throw new Error(
|
|
346
|
+
"Mode parameter is required when cleanup is not specified"
|
|
347
|
+
);
|
|
349
348
|
}
|
|
350
349
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
351
350
|
this.clearModeTimeout();
|
|
@@ -428,12 +427,16 @@ var ProxyServer = class {
|
|
|
428
427
|
this.replayId = id;
|
|
429
428
|
this.recordingId = null;
|
|
430
429
|
this.currentSession = null;
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
430
|
+
const sessionState = this.getOrCreateReplaySession(id);
|
|
431
|
+
sessionState.servedRecordingIdsByKey.clear();
|
|
432
|
+
sessionState.sortedRecordingsByKey.clear();
|
|
433
|
+
const filePath = getRecordingPath(this.recordingsDir, id);
|
|
434
|
+
try {
|
|
435
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
436
|
+
console.log(`[REPLAY] Loaded recording session: ${id}`);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
|
|
439
|
+
sessionState.loadedSession = null;
|
|
437
440
|
}
|
|
438
441
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
439
442
|
}
|
|
@@ -447,21 +450,32 @@ var ProxyServer = class {
|
|
|
447
450
|
}, timeout);
|
|
448
451
|
}
|
|
449
452
|
async flushPendingRecordings() {
|
|
453
|
+
if (this.flushPromise) {
|
|
454
|
+
await this.flushPromise;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
450
457
|
if (this.recordingPromises.length === 0) {
|
|
451
458
|
return;
|
|
452
459
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
|
|
460
|
+
this.flushPromise = (async () => {
|
|
461
|
+
try {
|
|
462
|
+
const results = await Promise.allSettled(this.recordingPromises);
|
|
463
|
+
if (this.currentSession) {
|
|
464
|
+
for (const result of results) {
|
|
465
|
+
if (result.status === "fulfilled" && result.value) {
|
|
466
|
+
this.currentSession.recordings.push(result.value);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
console.log(
|
|
470
|
+
`Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
|
|
471
|
+
);
|
|
458
472
|
}
|
|
473
|
+
this.recordingPromises = [];
|
|
474
|
+
} finally {
|
|
475
|
+
this.flushPromise = null;
|
|
459
476
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
this.recordingPromises = [];
|
|
477
|
+
})();
|
|
478
|
+
await this.flushPromise;
|
|
465
479
|
}
|
|
466
480
|
async saveCurrentSession() {
|
|
467
481
|
if (!this.currentSession) {
|
|
@@ -511,20 +525,25 @@ var ProxyServer = class {
|
|
|
511
525
|
);
|
|
512
526
|
return recordingId;
|
|
513
527
|
}
|
|
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
528
|
getServedTracker(sessionState, key) {
|
|
523
529
|
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
524
530
|
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
525
531
|
}
|
|
526
532
|
return sessionState.servedRecordingIdsByKey.get(key);
|
|
527
533
|
}
|
|
534
|
+
getSortedRecordings(sessionState, key) {
|
|
535
|
+
if (sessionState.sortedRecordingsByKey.has(key)) {
|
|
536
|
+
return sessionState.sortedRecordingsByKey.get(key);
|
|
537
|
+
}
|
|
538
|
+
const session = sessionState.loadedSession;
|
|
539
|
+
const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
540
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
541
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
542
|
+
return aSeq - bSeq;
|
|
543
|
+
});
|
|
544
|
+
sessionState.sortedRecordingsByKey.set(key, sortedRecords);
|
|
545
|
+
return sortedRecords;
|
|
546
|
+
}
|
|
528
547
|
selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
|
|
529
548
|
for (const rec of recordsWithKey) {
|
|
530
549
|
if (!servedForThisKey.has(rec.recordingId)) {
|
|
@@ -545,18 +564,17 @@ var ProxyServer = class {
|
|
|
545
564
|
const key = getReqID(req);
|
|
546
565
|
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
547
566
|
try {
|
|
548
|
-
const sessionState =
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
567
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
568
|
+
if (!sessionState.loadedSession) {
|
|
569
|
+
const error = new Error(
|
|
570
|
+
`Recording session file not found: ${filePath}`
|
|
571
|
+
);
|
|
572
|
+
error.code = "ENOENT";
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
553
575
|
const servedForThisKey = this.getServedTracker(sessionState, key);
|
|
554
576
|
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
|
-
});
|
|
577
|
+
const recordsWithKey = this.getSortedRecordings(sessionState, key);
|
|
560
578
|
if (recordsWithKey.length === 0) {
|
|
561
579
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
562
580
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -767,7 +785,6 @@ var ProxyServer = class {
|
|
|
767
785
|
})();
|
|
768
786
|
});
|
|
769
787
|
this.recordingPromises.push(recordingPromise);
|
|
770
|
-
await recordingPromise;
|
|
771
788
|
}
|
|
772
789
|
handleUpgrade(req, socket, head) {
|
|
773
790
|
if (this.mode === Modes.replay) {
|
|
@@ -926,7 +943,7 @@ var ProxyServer = class {
|
|
|
926
943
|
logServerStartup(port) {
|
|
927
944
|
console.log(`Proxy server running on http://localhost:${port}`);
|
|
928
945
|
console.log(`Mode: ${this.mode}`);
|
|
929
|
-
console.log(`
|
|
946
|
+
console.log(`Target: ${this.target}`);
|
|
930
947
|
console.log(
|
|
931
948
|
`Control endpoint: http://localhost:${port}${CONTROL_ENDPOINT}`
|
|
932
949
|
);
|
|
@@ -961,7 +978,6 @@ async function setProxyMode(mode, sessionId, timeout) {
|
|
|
961
978
|
throw new Error(`Failed to set proxy mode: ${text}`);
|
|
962
979
|
}
|
|
963
980
|
await response.json();
|
|
964
|
-
console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
|
|
965
981
|
} catch (error) {
|
|
966
982
|
console.error(`Error setting proxy mode:`, error);
|
|
967
983
|
throw error;
|
|
@@ -985,9 +1001,8 @@ async function cleanupSession(sessionId) {
|
|
|
985
1001
|
throw new Error(`Failed to cleanup session: ${text}`);
|
|
986
1002
|
}
|
|
987
1003
|
await response.json();
|
|
988
|
-
console.log(`Session cleaned up: ${sessionId}`);
|
|
989
1004
|
} catch (error) {
|
|
990
|
-
console.error(`Error cleaning up session
|
|
1005
|
+
console.error(`Error cleaning up session: ${sessionId}`, error);
|
|
991
1006
|
throw error;
|
|
992
1007
|
}
|
|
993
1008
|
}
|
|
@@ -1060,9 +1075,6 @@ async function setupClientSideRecording(page, sessionId, mode, url) {
|
|
|
1060
1075
|
const harFileName = sessionId.replaceAll("/", "__");
|
|
1061
1076
|
const recordingsDir = await getRecordingsDir();
|
|
1062
1077
|
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
1078
|
try {
|
|
1067
1079
|
await page.routeFromHAR(harPath, {
|
|
1068
1080
|
url,
|
|
@@ -1095,41 +1107,27 @@ var playwrightProxy = {
|
|
|
1095
1107
|
await page.setExtraHTTPHeaders({
|
|
1096
1108
|
[RECORDING_ID_HEADER]: sessionId
|
|
1097
1109
|
});
|
|
1098
|
-
console.log(`[Setup] Setting proxy mode: ${mode}, session: ${sessionId}`);
|
|
1099
1110
|
await setProxyMode(mode, sessionId, timeout);
|
|
1100
|
-
console.log(`[Setup] Proxy mode set successfully`);
|
|
1101
1111
|
if (clientSideOptions?.url) {
|
|
1102
|
-
console.log(`[Setup] Setting up client-side recording with pattern: ${clientSideOptions.url}`);
|
|
1103
1112
|
await setupClientSideRecording(
|
|
1104
1113
|
page,
|
|
1105
1114
|
sessionId,
|
|
1106
1115
|
mode,
|
|
1107
1116
|
clientSideOptions.url
|
|
1108
1117
|
);
|
|
1109
|
-
console.log(`[Setup] Client-side recording setup complete`);
|
|
1110
1118
|
}
|
|
1111
1119
|
const proxyPort = process.env.TEST_PROXY_RECORDER_PORT || "8100";
|
|
1112
1120
|
const proxyUrl = `localhost:${proxyPort}`;
|
|
1113
|
-
console.log(`[Setup] Registering proxy route handler for: ${proxyUrl}`);
|
|
1114
1121
|
await page.route(
|
|
1115
1122
|
(url) => {
|
|
1116
1123
|
const urlStr = url.toString();
|
|
1117
1124
|
const matches = urlStr.includes(proxyUrl);
|
|
1118
|
-
if (matches) {
|
|
1119
|
-
console.log(`[Route Matcher] Matched proxy request: ${urlStr}`);
|
|
1120
|
-
}
|
|
1121
1125
|
return matches;
|
|
1122
1126
|
},
|
|
1123
1127
|
async (route) => {
|
|
1124
1128
|
try {
|
|
1125
|
-
const url = route.request().url();
|
|
1126
|
-
const method = route.request().method();
|
|
1127
1129
|
const headers = route.request().headers();
|
|
1128
|
-
const hadHeader = !!headers[RECORDING_ID_HEADER];
|
|
1129
1130
|
headers[RECORDING_ID_HEADER] = sessionId;
|
|
1130
|
-
console.log(
|
|
1131
|
-
`[Route Intercept] ${method} ${url} (had header: ${hadHeader}, adding session: ${sessionId})`
|
|
1132
|
-
);
|
|
1133
1131
|
await route.continue({ headers });
|
|
1134
1132
|
} catch (error) {
|
|
1135
1133
|
console.error(
|
|
@@ -1142,7 +1140,6 @@ var playwrightProxy = {
|
|
|
1142
1140
|
{ times: Infinity }
|
|
1143
1141
|
// Ensure the handler applies to all matching requests
|
|
1144
1142
|
);
|
|
1145
|
-
console.log(`[Setup] Proxy route handler registered`);
|
|
1146
1143
|
const context = page.context();
|
|
1147
1144
|
const contextId = context._guid || "default";
|
|
1148
1145
|
const handlerKey = `cleanup_${contextId}`;
|
|
@@ -1150,9 +1147,6 @@ var playwrightProxy = {
|
|
|
1150
1147
|
globalThis[handlerKey] = true;
|
|
1151
1148
|
context.on("close", async () => {
|
|
1152
1149
|
try {
|
|
1153
|
-
console.log(
|
|
1154
|
-
`[Cleanup] Browser context closed, cleaning up session: ${sessionId}`
|
|
1155
|
-
);
|
|
1156
1150
|
await cleanupSession(sessionId);
|
|
1157
1151
|
} catch (error) {
|
|
1158
1152
|
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
|
@@ -16,8 +16,8 @@ function parseCliArgs() {
|
|
|
16
16
|
program.name("dev-proxy").description(
|
|
17
17
|
"Development proxy server with recording and replay capabilities"
|
|
18
18
|
).argument(
|
|
19
|
-
"<
|
|
20
|
-
"Target API service
|
|
19
|
+
"<target>",
|
|
20
|
+
"Target API service URL (e.g., http://localhost:3000)"
|
|
21
21
|
).option(
|
|
22
22
|
"-p, --port <number>",
|
|
23
23
|
"Port number for the proxy server",
|
|
@@ -29,18 +29,18 @@ function parseCliArgs() {
|
|
|
29
29
|
).action(() => {
|
|
30
30
|
});
|
|
31
31
|
program.parse();
|
|
32
|
-
const
|
|
32
|
+
const target2 = program.args[0];
|
|
33
33
|
const options = program.opts();
|
|
34
34
|
const port2 = Number.parseInt(options.port, 10);
|
|
35
35
|
if (Number.isNaN(port2) || port2 < 1025 || port2 > 65535) {
|
|
36
36
|
console.error("Error: Invalid port number. Must be between 1 and 65535");
|
|
37
37
|
process.exit(1);
|
|
38
38
|
}
|
|
39
|
-
if (
|
|
39
|
+
if (!target2) {
|
|
40
40
|
program.help();
|
|
41
41
|
}
|
|
42
42
|
const recordingsDir2 = path.resolve(process.cwd(), options.dir);
|
|
43
|
-
return {
|
|
43
|
+
return { target: target2, port: port2, recordingsDir: recordingsDir2 };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// src/constants.ts
|
|
@@ -155,8 +155,7 @@ function sendJsonResponse(res, statusCode, data) {
|
|
|
155
155
|
|
|
156
156
|
// src/ProxyServer.ts
|
|
157
157
|
var ProxyServer = class {
|
|
158
|
-
|
|
159
|
-
currentTargetIndex;
|
|
158
|
+
target;
|
|
160
159
|
mode;
|
|
161
160
|
recordingId;
|
|
162
161
|
replayId;
|
|
@@ -172,9 +171,10 @@ var ProxyServer = class {
|
|
|
172
171
|
// Track multiple concurrent replay sessions by recording ID
|
|
173
172
|
recordingPromises;
|
|
174
173
|
// Stack of promises that resolve to completed recordings
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
flushPromise;
|
|
175
|
+
// Promise for in-progress flush operation
|
|
176
|
+
constructor(target2, recordingsDir2) {
|
|
177
|
+
this.target = target2;
|
|
178
178
|
this.mode = Modes.transparent;
|
|
179
179
|
this.recordingId = null;
|
|
180
180
|
this.recordingIdCounter = 0;
|
|
@@ -185,6 +185,7 @@ var ProxyServer = class {
|
|
|
185
185
|
this.recordingsDir = recordingsDir2;
|
|
186
186
|
this.replaySessions = /* @__PURE__ */ new Map();
|
|
187
187
|
this.recordingPromises = [];
|
|
188
|
+
this.flushPromise = null;
|
|
188
189
|
this.proxy = httpProxy.createProxyServer({
|
|
189
190
|
secure: false,
|
|
190
191
|
changeOrigin: true,
|
|
@@ -246,9 +247,7 @@ var ProxyServer = class {
|
|
|
246
247
|
Object.assign(proxyRes.headers, corsHeaders);
|
|
247
248
|
}
|
|
248
249
|
getTarget() {
|
|
249
|
-
|
|
250
|
-
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
251
|
-
return target;
|
|
250
|
+
return this.target;
|
|
252
251
|
}
|
|
253
252
|
/**
|
|
254
253
|
* Extract recording ID from custom HTTP header
|
|
@@ -307,7 +306,8 @@ var ProxyServer = class {
|
|
|
307
306
|
recordingId,
|
|
308
307
|
servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
|
|
309
308
|
loadedSession: null,
|
|
310
|
-
lastAccessTime: Date.now()
|
|
309
|
+
lastAccessTime: Date.now(),
|
|
310
|
+
sortedRecordingsByKey: /* @__PURE__ */ new Map()
|
|
311
311
|
};
|
|
312
312
|
this.replaySessions.set(recordingId, session);
|
|
313
313
|
console.log(
|
|
@@ -322,17 +322,14 @@ var ProxyServer = class {
|
|
|
322
322
|
*/
|
|
323
323
|
async cleanupSession(sessionId) {
|
|
324
324
|
if (this.replaySessions.has(sessionId)) {
|
|
325
|
-
console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
|
|
326
325
|
this.replaySessions.delete(sessionId);
|
|
327
326
|
}
|
|
328
327
|
if (this.recordingId === sessionId) {
|
|
329
|
-
console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
|
|
330
328
|
await this.saveCurrentSession();
|
|
331
329
|
this.currentSession = null;
|
|
332
330
|
this.recordingId = null;
|
|
333
331
|
}
|
|
334
332
|
if (this.replayId === sessionId) {
|
|
335
|
-
console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
|
|
336
333
|
this.replayId = null;
|
|
337
334
|
}
|
|
338
335
|
console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
|
|
@@ -381,7 +378,9 @@ var ProxyServer = class {
|
|
|
381
378
|
return;
|
|
382
379
|
}
|
|
383
380
|
if (!mode) {
|
|
384
|
-
throw new Error(
|
|
381
|
+
throw new Error(
|
|
382
|
+
"Mode parameter is required when cleanup is not specified"
|
|
383
|
+
);
|
|
385
384
|
}
|
|
386
385
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
387
386
|
this.clearModeTimeout();
|
|
@@ -464,12 +463,16 @@ var ProxyServer = class {
|
|
|
464
463
|
this.replayId = id;
|
|
465
464
|
this.recordingId = null;
|
|
466
465
|
this.currentSession = null;
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
466
|
+
const sessionState = this.getOrCreateReplaySession(id);
|
|
467
|
+
sessionState.servedRecordingIdsByKey.clear();
|
|
468
|
+
sessionState.sortedRecordingsByKey.clear();
|
|
469
|
+
const filePath = getRecordingPath(this.recordingsDir, id);
|
|
470
|
+
try {
|
|
471
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
472
|
+
console.log(`[REPLAY] Loaded recording session: ${id}`);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
console.error(`[REPLAY ERROR] Failed to load session ${id}:`, error);
|
|
475
|
+
sessionState.loadedSession = null;
|
|
473
476
|
}
|
|
474
477
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
475
478
|
}
|
|
@@ -483,21 +486,32 @@ var ProxyServer = class {
|
|
|
483
486
|
}, timeout);
|
|
484
487
|
}
|
|
485
488
|
async flushPendingRecordings() {
|
|
489
|
+
if (this.flushPromise) {
|
|
490
|
+
await this.flushPromise;
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
486
493
|
if (this.recordingPromises.length === 0) {
|
|
487
494
|
return;
|
|
488
495
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (
|
|
493
|
-
|
|
496
|
+
this.flushPromise = (async () => {
|
|
497
|
+
try {
|
|
498
|
+
const results = await Promise.allSettled(this.recordingPromises);
|
|
499
|
+
if (this.currentSession) {
|
|
500
|
+
for (const result of results) {
|
|
501
|
+
if (result.status === "fulfilled" && result.value) {
|
|
502
|
+
this.currentSession.recordings.push(result.value);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
console.log(
|
|
506
|
+
`Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
|
|
507
|
+
);
|
|
494
508
|
}
|
|
509
|
+
this.recordingPromises = [];
|
|
510
|
+
} finally {
|
|
511
|
+
this.flushPromise = null;
|
|
495
512
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
this.recordingPromises = [];
|
|
513
|
+
})();
|
|
514
|
+
await this.flushPromise;
|
|
501
515
|
}
|
|
502
516
|
async saveCurrentSession() {
|
|
503
517
|
if (!this.currentSession) {
|
|
@@ -547,20 +561,25 @@ var ProxyServer = class {
|
|
|
547
561
|
);
|
|
548
562
|
return recordingId;
|
|
549
563
|
}
|
|
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
564
|
getServedTracker(sessionState, key) {
|
|
559
565
|
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
560
566
|
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
561
567
|
}
|
|
562
568
|
return sessionState.servedRecordingIdsByKey.get(key);
|
|
563
569
|
}
|
|
570
|
+
getSortedRecordings(sessionState, key) {
|
|
571
|
+
if (sessionState.sortedRecordingsByKey.has(key)) {
|
|
572
|
+
return sessionState.sortedRecordingsByKey.get(key);
|
|
573
|
+
}
|
|
574
|
+
const session = sessionState.loadedSession;
|
|
575
|
+
const sortedRecords = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
576
|
+
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
577
|
+
const bSeq = b.sequence !== void 0 ? b.sequence : b.recordingId;
|
|
578
|
+
return aSeq - bSeq;
|
|
579
|
+
});
|
|
580
|
+
sessionState.sortedRecordingsByKey.set(key, sortedRecords);
|
|
581
|
+
return sortedRecords;
|
|
582
|
+
}
|
|
564
583
|
selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
|
|
565
584
|
for (const rec of recordsWithKey) {
|
|
566
585
|
if (!servedForThisKey.has(rec.recordingId)) {
|
|
@@ -581,18 +600,17 @@ var ProxyServer = class {
|
|
|
581
600
|
const key = getReqID(req);
|
|
582
601
|
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
583
602
|
try {
|
|
584
|
-
const sessionState =
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
603
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
604
|
+
if (!sessionState.loadedSession) {
|
|
605
|
+
const error = new Error(
|
|
606
|
+
`Recording session file not found: ${filePath}`
|
|
607
|
+
);
|
|
608
|
+
error.code = "ENOENT";
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
589
611
|
const servedForThisKey = this.getServedTracker(sessionState, key);
|
|
590
612
|
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
|
-
});
|
|
613
|
+
const recordsWithKey = this.getSortedRecordings(sessionState, key);
|
|
596
614
|
if (recordsWithKey.length === 0) {
|
|
597
615
|
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
598
616
|
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
@@ -683,16 +701,16 @@ var ProxyServer = class {
|
|
|
683
701
|
res.end();
|
|
684
702
|
}
|
|
685
703
|
async handleProxyRequest(req, res) {
|
|
686
|
-
const
|
|
687
|
-
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${
|
|
704
|
+
const target2 = this.getTarget();
|
|
705
|
+
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target2}`);
|
|
688
706
|
if (this.mode === Modes.record) {
|
|
689
|
-
await this.recordAndProxyRequest(req, res,
|
|
707
|
+
await this.recordAndProxyRequest(req, res, target2);
|
|
690
708
|
} else {
|
|
691
|
-
this.proxy.web(req, res, { target });
|
|
709
|
+
this.proxy.web(req, res, { target: target2 });
|
|
692
710
|
}
|
|
693
711
|
}
|
|
694
712
|
// Note: streaming requests are buffered before proxying; streaming passthrough is not yet implemented
|
|
695
|
-
async recordAndProxyRequest(req, res,
|
|
713
|
+
async recordAndProxyRequest(req, res, target2) {
|
|
696
714
|
if (!this.currentSession) {
|
|
697
715
|
return;
|
|
698
716
|
}
|
|
@@ -720,7 +738,7 @@ var ProxyServer = class {
|
|
|
720
738
|
console.error("Error buffering request:", error);
|
|
721
739
|
}
|
|
722
740
|
const requestBody = Buffer.concat(chunks).toString("utf8");
|
|
723
|
-
const targetUrl = new URL(
|
|
741
|
+
const targetUrl = new URL(target2);
|
|
724
742
|
const isHttps = targetUrl.protocol === "https:";
|
|
725
743
|
const requestModule = isHttps ? https : http;
|
|
726
744
|
const defaultPort = isHttps ? 443 : 80;
|
|
@@ -803,22 +821,21 @@ var ProxyServer = class {
|
|
|
803
821
|
})();
|
|
804
822
|
});
|
|
805
823
|
this.recordingPromises.push(recordingPromise);
|
|
806
|
-
await recordingPromise;
|
|
807
824
|
}
|
|
808
825
|
handleUpgrade(req, socket, head) {
|
|
809
826
|
if (this.mode === Modes.replay) {
|
|
810
827
|
this.handleReplayWebSocket(req, socket);
|
|
811
828
|
return;
|
|
812
829
|
}
|
|
813
|
-
const
|
|
814
|
-
console.log(`[${this.mode}] WebSocket upgrade ${req.url} -> ${
|
|
830
|
+
const target2 = this.getTarget();
|
|
831
|
+
console.log(`[${this.mode}] WebSocket upgrade ${req.url} -> ${target2}`);
|
|
815
832
|
if (this.mode === Modes.record) {
|
|
816
|
-
this.handleRecordWebSocket(req, socket, head,
|
|
833
|
+
this.handleRecordWebSocket(req, socket, head, target2);
|
|
817
834
|
} else {
|
|
818
|
-
this.proxy.ws(req, socket, head, { target });
|
|
835
|
+
this.proxy.ws(req, socket, head, { target: target2 });
|
|
819
836
|
}
|
|
820
837
|
}
|
|
821
|
-
handleRecordWebSocket(req, clientSocket, head,
|
|
838
|
+
handleRecordWebSocket(req, clientSocket, head, target2) {
|
|
822
839
|
const url = req.url || "/";
|
|
823
840
|
const key = `WS_${url.replaceAll("/", "_")}`;
|
|
824
841
|
const wsRecording = {
|
|
@@ -830,7 +847,7 @@ var ProxyServer = class {
|
|
|
830
847
|
if (this.currentSession) {
|
|
831
848
|
this.currentSession.websocketRecordings.push(wsRecording);
|
|
832
849
|
}
|
|
833
|
-
const backendWsUrl = `${
|
|
850
|
+
const backendWsUrl = `${target2.replace("http", "ws")}${url}`;
|
|
834
851
|
const backendWs = new WebSocket(backendWsUrl);
|
|
835
852
|
const wss = new WebSocketServer({ noServer: true });
|
|
836
853
|
backendWs.on("open", () => {
|
|
@@ -962,7 +979,7 @@ var ProxyServer = class {
|
|
|
962
979
|
logServerStartup(port2) {
|
|
963
980
|
console.log(`Proxy server running on http://localhost:${port2}`);
|
|
964
981
|
console.log(`Mode: ${this.mode}`);
|
|
965
|
-
console.log(`
|
|
982
|
+
console.log(`Target: ${this.target}`);
|
|
966
983
|
console.log(
|
|
967
984
|
`Control endpoint: http://localhost:${port2}${CONTROL_ENDPOINT}`
|
|
968
985
|
);
|
|
@@ -970,8 +987,8 @@ var ProxyServer = class {
|
|
|
970
987
|
};
|
|
971
988
|
|
|
972
989
|
// src/proxy.ts
|
|
973
|
-
var {
|
|
974
|
-
var proxy = new ProxyServer(
|
|
990
|
+
var { target, port, recordingsDir } = parseCliArgs();
|
|
991
|
+
var proxy = new ProxyServer(target, recordingsDir);
|
|
975
992
|
await proxy.init();
|
|
976
993
|
proxy.listen(port);
|
|
977
994
|
console.log(`Recordings will be saved to: ${recordingsDir}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-proxy-recorder",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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",
|