test-proxy-recorder 0.1.7 → 0.1.9

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