test-proxy-recorder 0.1.8 → 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;
@@ -315,6 +319,11 @@ var ProxyServer = class {
315
319
  return;
316
320
  }
317
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;
318
327
  const record = {
319
328
  request: {
320
329
  method: req.method,
@@ -324,44 +333,58 @@ var ProxyServer = class {
324
333
  },
325
334
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
326
335
  key,
327
- sequence: -1
328
- // Temporary, will be set when response arrives
336
+ sequence,
337
+ recordingId
329
338
  };
330
339
  this.currentSession.recordings.push(record);
331
340
  console.log(
332
341
  // eslint-disable-next-line sonarjs/no-nested-template-literals
333
- `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
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})`
334
343
  );
335
344
  }
336
345
  updateRequestBodySync(req, body) {
337
346
  if (!this.currentSession) {
338
347
  return;
339
348
  }
340
- const key = getReqID(req);
341
- const record = this.currentSession.recordings.findLast(
342
- (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
343
358
  );
344
359
  if (!record) {
345
360
  console.error(
346
- `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}`
347
362
  );
348
363
  return;
349
364
  }
350
365
  record.request.body = body || null;
351
366
  console.log(
352
- `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})`
353
368
  );
354
369
  }
355
370
  async recordResponse(req, proxyRes) {
356
371
  if (!this.currentSession) {
357
372
  return;
358
373
  }
359
- const key = getReqID(req);
360
- const record = this.currentSession.recordings.findLast(
361
- (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
362
383
  );
363
384
  if (!record) {
364
- 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
+ );
365
388
  return;
366
389
  }
367
390
  const chunks = [];
@@ -375,34 +398,28 @@ var ProxyServer = class {
375
398
  headers: proxyRes.headers,
376
399
  body: body || null
377
400
  };
378
- console.log(`Recorded: ${req.method} ${req.url}`);
401
+ console.log(
402
+ `Recorded: ${req.method} ${req.url} (seq: ${record.sequence}, recordingId: ${recordingId})`
403
+ );
379
404
  });
380
405
  }
381
406
  async recordResponseData(req, proxyRes, body) {
382
407
  if (!this.currentSession) {
383
408
  return false;
384
409
  }
385
- const key = getReqID(req);
386
- const record = this.currentSession.recordings.findLast(
387
- (r) => r.key === key && !r.response
388
- );
389
- if (!record) {
390
- const host = req.headers.host || "unknown";
391
- const recordsWithKey = this.currentSession.recordings.filter(
392
- (r) => r.key === key
393
- );
410
+ const recordingId = req.__recordingId;
411
+ if (recordingId === void 0) {
394
412
  console.error(
395
- `Request record not found for response: ${key} at ${req.method} ${host}${req.url}`
396
- );
397
- console.error(
398
- ` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
413
+ `recordResponseData: No recording ID found on request ${req.method} ${req.url}`
399
414
  );
415
+ return false;
416
+ }
417
+ const record = this.currentSession.recordings.find(
418
+ (r) => r.recordingId === recordingId
419
+ );
420
+ if (!record) {
400
421
  console.error(
401
- ` Records with key:`,
402
- recordsWithKey.map((r) => ({
403
- seq: r.sequence,
404
- hasResponse: !!r.response
405
- }))
422
+ `recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
406
423
  );
407
424
  return false;
408
425
  }
@@ -411,12 +428,8 @@ var ProxyServer = class {
411
428
  headers: proxyRes.headers,
412
429
  body: body || null
413
430
  };
414
- record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
415
- const currentSequence = this.requestSequenceMap.get(key) || 0;
416
- record.sequence = currentSequence;
417
- this.requestSequenceMap.set(key, currentSequence + 1);
418
431
  console.log(
419
- `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})`
420
433
  );
421
434
  return true;
422
435
  }
@@ -428,13 +441,30 @@ var ProxyServer = class {
428
441
  const host = req.headers.host || "unknown";
429
442
  const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
430
443
  if (recordsWithKey.length === 0) {
431
- throw new Error(
432
- `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`
433
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;
434
460
  }
435
461
  const usageCount = this.replaySequenceMap.get(key) || 0;
436
- const recordIndex = usageCount % recordsWithKey.length;
437
- const record = recordsWithKey[recordIndex];
462
+ let record;
463
+ if (usageCount < recordsWithKey.length) {
464
+ record = recordsWithKey[usageCount];
465
+ } else {
466
+ record = recordsWithKey[recordsWithKey.length - 1];
467
+ }
438
468
  console.log(
439
469
  `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
440
470
  );
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;
@@ -304,6 +307,11 @@ var ProxyServer = class {
304
307
  return;
305
308
  }
306
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;
307
315
  const record = {
308
316
  request: {
309
317
  method: req.method,
@@ -313,44 +321,58 @@ var ProxyServer = class {
313
321
  },
314
322
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
315
323
  key,
316
- sequence: -1
317
- // Temporary, will be set when response arrives
324
+ sequence,
325
+ recordingId
318
326
  };
319
327
  this.currentSession.recordings.push(record);
320
328
  console.log(
321
329
  // eslint-disable-next-line sonarjs/no-nested-template-literals
322
- `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})`
323
331
  );
324
332
  }
325
333
  updateRequestBodySync(req, body) {
326
334
  if (!this.currentSession) {
327
335
  return;
328
336
  }
329
- const key = getReqID(req);
330
- const record = this.currentSession.recordings.findLast(
331
- (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
332
346
  );
333
347
  if (!record) {
334
348
  console.error(
335
- `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}`
336
350
  );
337
351
  return;
338
352
  }
339
353
  record.request.body = body || null;
340
354
  console.log(
341
- `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})`
342
356
  );
343
357
  }
344
358
  async recordResponse(req, proxyRes) {
345
359
  if (!this.currentSession) {
346
360
  return;
347
361
  }
348
- const key = getReqID(req);
349
- const record = this.currentSession.recordings.findLast(
350
- (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
351
371
  );
352
372
  if (!record) {
353
- 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
+ );
354
376
  return;
355
377
  }
356
378
  const chunks = [];
@@ -364,34 +386,28 @@ var ProxyServer = class {
364
386
  headers: proxyRes.headers,
365
387
  body: body || null
366
388
  };
367
- console.log(`Recorded: ${req.method} ${req.url}`);
389
+ console.log(
390
+ `Recorded: ${req.method} ${req.url} (seq: ${record.sequence}, recordingId: ${recordingId})`
391
+ );
368
392
  });
369
393
  }
370
394
  async recordResponseData(req, proxyRes, body) {
371
395
  if (!this.currentSession) {
372
396
  return false;
373
397
  }
374
- const key = getReqID(req);
375
- const record = this.currentSession.recordings.findLast(
376
- (r) => r.key === key && !r.response
377
- );
378
- if (!record) {
379
- const host = req.headers.host || "unknown";
380
- const recordsWithKey = this.currentSession.recordings.filter(
381
- (r) => r.key === key
382
- );
398
+ const recordingId = req.__recordingId;
399
+ if (recordingId === void 0) {
383
400
  console.error(
384
- `Request record not found for response: ${key} at ${req.method} ${host}${req.url}`
385
- );
386
- console.error(
387
- ` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
401
+ `recordResponseData: No recording ID found on request ${req.method} ${req.url}`
388
402
  );
403
+ return false;
404
+ }
405
+ const record = this.currentSession.recordings.find(
406
+ (r) => r.recordingId === recordingId
407
+ );
408
+ if (!record) {
389
409
  console.error(
390
- ` Records with key:`,
391
- recordsWithKey.map((r) => ({
392
- seq: r.sequence,
393
- hasResponse: !!r.response
394
- }))
410
+ `recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
395
411
  );
396
412
  return false;
397
413
  }
@@ -400,12 +416,8 @@ var ProxyServer = class {
400
416
  headers: proxyRes.headers,
401
417
  body: body || null
402
418
  };
403
- record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
404
- const currentSequence = this.requestSequenceMap.get(key) || 0;
405
- record.sequence = currentSequence;
406
- this.requestSequenceMap.set(key, currentSequence + 1);
407
419
  console.log(
408
- `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})`
409
421
  );
410
422
  return true;
411
423
  }
@@ -417,13 +429,30 @@ var ProxyServer = class {
417
429
  const host = req.headers.host || "unknown";
418
430
  const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
419
431
  if (recordsWithKey.length === 0) {
420
- throw new Error(
421
- `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`
422
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;
423
448
  }
424
449
  const usageCount = this.replaySequenceMap.get(key) || 0;
425
- const recordIndex = usageCount % recordsWithKey.length;
426
- const record = recordsWithKey[recordIndex];
450
+ let record;
451
+ if (usageCount < recordsWithKey.length) {
452
+ record = recordsWithKey[usageCount];
453
+ } else {
454
+ record = recordsWithKey[recordsWithKey.length - 1];
455
+ }
427
456
  console.log(
428
457
  `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
429
458
  );
@@ -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;
@@ -338,6 +341,11 @@ var ProxyServer = class {
338
341
  return;
339
342
  }
340
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;
341
349
  const record = {
342
350
  request: {
343
351
  method: req.method,
@@ -347,44 +355,58 @@ var ProxyServer = class {
347
355
  },
348
356
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
349
357
  key,
350
- sequence: -1
351
- // Temporary, will be set when response arrives
358
+ sequence,
359
+ recordingId
352
360
  };
353
361
  this.currentSession.recordings.push(record);
354
362
  console.log(
355
363
  // eslint-disable-next-line sonarjs/no-nested-template-literals
356
- `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})`
357
365
  );
358
366
  }
359
367
  updateRequestBodySync(req, body) {
360
368
  if (!this.currentSession) {
361
369
  return;
362
370
  }
363
- const key = getReqID(req);
364
- const record = this.currentSession.recordings.findLast(
365
- (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
366
380
  );
367
381
  if (!record) {
368
382
  console.error(
369
- `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}`
370
384
  );
371
385
  return;
372
386
  }
373
387
  record.request.body = body || null;
374
388
  console.log(
375
- `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})`
376
390
  );
377
391
  }
378
392
  async recordResponse(req, proxyRes) {
379
393
  if (!this.currentSession) {
380
394
  return;
381
395
  }
382
- const key = getReqID(req);
383
- const record = this.currentSession.recordings.findLast(
384
- (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
385
405
  );
386
406
  if (!record) {
387
- 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
+ );
388
410
  return;
389
411
  }
390
412
  const chunks = [];
@@ -398,34 +420,28 @@ var ProxyServer = class {
398
420
  headers: proxyRes.headers,
399
421
  body: body || null
400
422
  };
401
- console.log(`Recorded: ${req.method} ${req.url}`);
423
+ console.log(
424
+ `Recorded: ${req.method} ${req.url} (seq: ${record.sequence}, recordingId: ${recordingId})`
425
+ );
402
426
  });
403
427
  }
404
428
  async recordResponseData(req, proxyRes, body) {
405
429
  if (!this.currentSession) {
406
430
  return false;
407
431
  }
408
- const key = getReqID(req);
409
- const record = this.currentSession.recordings.findLast(
410
- (r) => r.key === key && !r.response
411
- );
412
- if (!record) {
413
- const host = req.headers.host || "unknown";
414
- const recordsWithKey = this.currentSession.recordings.filter(
415
- (r) => r.key === key
416
- );
432
+ const recordingId = req.__recordingId;
433
+ if (recordingId === void 0) {
417
434
  console.error(
418
- `Request record not found for response: ${key} at ${req.method} ${host}${req.url}`
419
- );
420
- console.error(
421
- ` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
435
+ `recordResponseData: No recording ID found on request ${req.method} ${req.url}`
422
436
  );
437
+ return false;
438
+ }
439
+ const record = this.currentSession.recordings.find(
440
+ (r) => r.recordingId === recordingId
441
+ );
442
+ if (!record) {
423
443
  console.error(
424
- ` Records with key:`,
425
- recordsWithKey.map((r) => ({
426
- seq: r.sequence,
427
- hasResponse: !!r.response
428
- }))
444
+ `recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
429
445
  );
430
446
  return false;
431
447
  }
@@ -434,12 +450,8 @@ var ProxyServer = class {
434
450
  headers: proxyRes.headers,
435
451
  body: body || null
436
452
  };
437
- record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
438
- const currentSequence = this.requestSequenceMap.get(key) || 0;
439
- record.sequence = currentSequence;
440
- this.requestSequenceMap.set(key, currentSequence + 1);
441
453
  console.log(
442
- `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})`
443
455
  );
444
456
  return true;
445
457
  }
@@ -451,13 +463,30 @@ var ProxyServer = class {
451
463
  const host = req.headers.host || "unknown";
452
464
  const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
453
465
  if (recordsWithKey.length === 0) {
454
- throw new Error(
455
- `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`
456
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;
457
482
  }
458
483
  const usageCount = this.replaySequenceMap.get(key) || 0;
459
- const recordIndex = usageCount % recordsWithKey.length;
460
- const record = recordsWithKey[recordIndex];
484
+ let record;
485
+ if (usageCount < recordsWithKey.length) {
486
+ record = recordsWithKey[usageCount];
487
+ } else {
488
+ record = recordsWithKey[recordsWithKey.length - 1];
489
+ }
461
490
  console.log(
462
491
  `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
463
492
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "test-proxy-recorder",
3
- "version": "0.1.8",
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",