@replayio-app-building/netlify-recorder 0.50.0 → 0.52.0

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.d.ts CHANGED
@@ -74,8 +74,10 @@ interface NetworkCall {
74
74
  timestamp: number;
75
75
  /** Epoch ms when the fetch call started. */
76
76
  startTime: number;
77
- /** Epoch ms when the response was received. */
77
+ /** Epoch ms when the response was received. 0 if response was never awaited (fire-and-forget). */
78
78
  endTime: number;
79
+ /** True when the call was initiated but the response was not awaited by the handler. */
80
+ pending?: boolean;
79
81
  }
80
82
  interface EnvRead {
81
83
  key: string;
package/dist/index.js CHANGED
@@ -102,37 +102,46 @@ function patchHttpModules(mode, calls, consumed, silent) {
102
102
  return origEnd(chunk, ...rest);
103
103
  };
104
104
  const startTime = Date.now();
105
+ const requestHeaders = {};
106
+ if (options.headers) {
107
+ for (const [k, v] of Object.entries(options.headers)) {
108
+ if (v !== void 0) requestHeaders[k] = Array.isArray(v) ? v.join(", ") : String(v);
109
+ }
110
+ }
111
+ const store = getRequestStore();
112
+ const targetCalls = store?.networkCalls ?? calls;
113
+ const entry = {
114
+ url: urlStr,
115
+ method,
116
+ requestHeaders,
117
+ requestBody: bodyChunks.length > 0 ? Buffer.concat(bodyChunks).toString("utf-8") : void 0,
118
+ responseStatus: 0,
119
+ responseHeaders: {},
120
+ responseBody: void 0,
121
+ timestamp: startTime,
122
+ startTime,
123
+ endTime: 0,
124
+ pending: true
125
+ };
126
+ targetCalls.push(entry);
105
127
  req.on("response", (res) => {
106
128
  const resChunks = [];
107
129
  res.on("data", (chunk) => resChunks.push(chunk));
108
130
  res.on("end", () => {
109
131
  const endTime = Date.now();
110
- const requestHeaders = {};
111
- if (options.headers) {
112
- for (const [k, v] of Object.entries(options.headers)) {
113
- if (v !== void 0) requestHeaders[k] = Array.isArray(v) ? v.join(", ") : String(v);
114
- }
115
- }
116
132
  const responseHeaders = {};
117
133
  if (res.headers) {
118
134
  for (const [k, v] of Object.entries(res.headers)) {
119
135
  if (v !== void 0) responseHeaders[k] = Array.isArray(v) ? v.join(", ") : String(v);
120
136
  }
121
137
  }
122
- const store = getRequestStore();
123
- const targetCalls = store?.networkCalls ?? calls;
124
- targetCalls.push({
125
- url: urlStr,
126
- method,
127
- requestHeaders,
128
- requestBody: bodyChunks.length > 0 ? Buffer.concat(bodyChunks).toString("utf-8") : void 0,
129
- responseStatus: res.statusCode ?? 0,
130
- responseHeaders,
131
- responseBody: Buffer.concat(resChunks).toString("utf-8"),
132
- timestamp: endTime,
133
- startTime,
134
- endTime
135
- });
138
+ entry.requestBody = bodyChunks.length > 0 ? Buffer.concat(bodyChunks).toString("utf-8") : void 0;
139
+ entry.responseStatus = res.statusCode ?? 0;
140
+ entry.responseHeaders = responseHeaders;
141
+ entry.responseBody = Buffer.concat(resChunks).toString("utf-8");
142
+ entry.timestamp = endTime;
143
+ entry.endTime = endTime;
144
+ entry.pending = false;
136
145
  });
137
146
  });
138
147
  return req;
@@ -173,11 +182,18 @@ function replayHttpRequest(url, _method, calls, consumed, callback, silent) {
173
182
  );
174
183
  }
175
184
  consumed.add(matchIdx);
185
+ const isPending = call.pending || call.endTime === 0 && call.responseStatus === 0;
176
186
  if (!silent) {
177
- const duration = call.endTime - call.startTime;
178
- console.log(
179
- ` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus} (original: ${duration}ms)`
180
- );
187
+ if (isPending) {
188
+ console.log(
189
+ ` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => (fire-and-forget, no response captured)`
190
+ );
191
+ } else {
192
+ const duration = call.endTime - call.startTime;
193
+ console.log(
194
+ ` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus} (original: ${duration}ms)`
195
+ );
196
+ }
181
197
  }
182
198
  const body = call.responseBody ?? "";
183
199
  const fakeRes = new Readable({
@@ -186,12 +202,15 @@ function replayHttpRequest(url, _method, calls, consumed, callback, silent) {
186
202
  this.push(null);
187
203
  }
188
204
  });
189
- fakeRes.statusCode = call.responseStatus;
205
+ fakeRes.statusCode = isPending ? 200 : call.responseStatus;
190
206
  fakeRes.statusMessage = "";
191
207
  fakeRes.headers = {};
192
208
  if (call.responseHeaders) {
209
+ const skipHeaders = /* @__PURE__ */ new Set(["content-encoding", "transfer-encoding"]);
193
210
  for (const [k, v] of Object.entries(call.responseHeaders)) {
194
- fakeRes.headers[k.toLowerCase()] = v;
211
+ if (!skipHeaders.has(k.toLowerCase())) {
212
+ fakeRes.headers[k.toLowerCase()] = v;
213
+ }
195
214
  }
196
215
  }
197
216
  const fakeReq = new Writable({
@@ -246,6 +265,20 @@ function ensureCaptureInterceptor() {
246
265
  );
247
266
  }
248
267
  const startTime = Date.now();
268
+ const entry = {
269
+ url,
270
+ method,
271
+ requestHeaders,
272
+ requestBody,
273
+ responseStatus: 0,
274
+ responseHeaders: {},
275
+ responseBody: void 0,
276
+ timestamp: startTime,
277
+ startTime,
278
+ endTime: 0,
279
+ pending: true
280
+ };
281
+ calls.push(entry);
249
282
  const response = await _realOriginalFetch(input, init);
250
283
  const endTime = Date.now();
251
284
  const responseBody = await response.clone().text();
@@ -253,18 +286,12 @@ function ensureCaptureInterceptor() {
253
286
  response.headers.forEach((v, k) => {
254
287
  responseHeaders[k] = v;
255
288
  });
256
- calls.push({
257
- url,
258
- method,
259
- requestHeaders,
260
- requestBody,
261
- responseStatus: response.status,
262
- responseHeaders,
263
- responseBody,
264
- timestamp: endTime,
265
- startTime,
266
- endTime
267
- });
289
+ entry.responseStatus = response.status;
290
+ entry.responseHeaders = responseHeaders;
291
+ entry.responseBody = responseBody;
292
+ entry.timestamp = endTime;
293
+ entry.endTime = endTime;
294
+ entry.pending = false;
268
295
  return response;
269
296
  };
270
297
  globalThis.fetch = captureFetch;
@@ -320,6 +347,20 @@ function installNetworkInterceptor(mode, calls, options) {
320
347
  );
321
348
  }
322
349
  const startTime = Date.now();
350
+ const entry = {
351
+ url,
352
+ method,
353
+ requestHeaders,
354
+ requestBody,
355
+ responseStatus: 0,
356
+ responseHeaders: {},
357
+ responseBody: void 0,
358
+ timestamp: startTime,
359
+ startTime,
360
+ endTime: 0,
361
+ pending: true
362
+ };
363
+ calls.push(entry);
323
364
  const response = await originalFetch2(input, init);
324
365
  const endTime = Date.now();
325
366
  const responseBody = await response.clone().text();
@@ -327,18 +368,12 @@ function installNetworkInterceptor(mode, calls, options) {
327
368
  response.headers.forEach((v, k) => {
328
369
  responseHeaders[k] = v;
329
370
  });
330
- calls.push({
331
- url,
332
- method,
333
- requestHeaders,
334
- requestBody,
335
- responseStatus: response.status,
336
- responseHeaders,
337
- responseBody,
338
- timestamp: endTime,
339
- startTime,
340
- endTime
341
- });
371
+ entry.responseStatus = response.status;
372
+ entry.responseHeaders = responseHeaders;
373
+ entry.responseBody = responseBody;
374
+ entry.timestamp = endTime;
375
+ entry.endTime = endTime;
376
+ entry.pending = false;
342
377
  return response;
343
378
  };
344
379
  globalThis.fetch = captureFetch;
@@ -373,6 +408,26 @@ function installNetworkInterceptor(mode, calls, options) {
373
408
  break;
374
409
  }
375
410
  }
411
+ if (matchIdx === -1) {
412
+ for (let i = 0; i < calls.length; i++) {
413
+ if (consumed.has(i)) continue;
414
+ const c = calls[i];
415
+ if (c && c.url === url) {
416
+ matchIdx = i;
417
+ break;
418
+ }
419
+ }
420
+ }
421
+ if (matchIdx === -1 && requestBody) {
422
+ for (let i = 0; i < calls.length; i++) {
423
+ if (consumed.has(i)) continue;
424
+ const c = calls[i];
425
+ if (c && c.requestBody === requestBody) {
426
+ matchIdx = i;
427
+ break;
428
+ }
429
+ }
430
+ }
376
431
  if (matchIdx === -1) {
377
432
  for (let i = 0; i < calls.length; i++) {
378
433
  if (!consumed.has(i)) {
@@ -388,23 +443,37 @@ function installNetworkInterceptor(mode, calls, options) {
388
443
  );
389
444
  }
390
445
  consumed.add(matchIdx);
446
+ const isPending = call.pending || call.endTime === 0 && call.responseStatus === 0;
391
447
  if (!silent) {
392
- const duration = call.endTime - call.startTime;
393
- console.log(
394
- ` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus} (original: ${duration}ms)`
395
- );
448
+ if (isPending) {
449
+ console.log(
450
+ ` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => (fire-and-forget, no response captured)`
451
+ );
452
+ } else {
453
+ const duration = call.endTime - call.startTime;
454
+ console.log(
455
+ ` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus} (original: ${duration}ms)`
456
+ );
457
+ }
396
458
  }
397
459
  const body = call.responseBody ?? "";
398
- const status = call.responseStatus;
460
+ const status = isPending ? 200 : call.responseStatus;
461
+ const skipHeaders = /* @__PURE__ */ new Set(["content-encoding", "transfer-encoding"]);
462
+ const filteredHeaders = {};
463
+ for (const [k, v] of Object.entries(call.responseHeaders ?? {})) {
464
+ if (!skipHeaders.has(k.toLowerCase())) {
465
+ filteredHeaders[k.toLowerCase()] = v;
466
+ }
467
+ }
399
468
  return {
400
469
  ok: status >= 200 && status < 300,
401
470
  status,
402
471
  statusText: "",
403
472
  headers: {
404
- get: (name) => (call.responseHeaders ?? {})[name.toLowerCase()] ?? null,
405
- has: (name) => name.toLowerCase() in (call.responseHeaders ?? {}),
473
+ get: (name) => filteredHeaders[name.toLowerCase()] ?? null,
474
+ has: (name) => name.toLowerCase() in filteredHeaders,
406
475
  forEach: (cb) => {
407
- for (const [k, v] of Object.entries(call.responseHeaders ?? {})) cb(v, k);
476
+ for (const [k, v] of Object.entries(filteredHeaders)) cb(v, k);
408
477
  }
409
478
  },
410
479
  text: async () => body,
@@ -1050,6 +1119,14 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo, p
1050
1119
  blobData.capturedData.envReads
1051
1120
  );
1052
1121
  globalThis.__REPLAY_RECORDING_MODE__ = true;
1122
+ const origMathRandom = Math.random;
1123
+ let prngState = 305419896;
1124
+ Math.random = () => {
1125
+ prngState = prngState + 1831565813 | 0;
1126
+ let t = Math.imul(prngState ^ prngState >>> 15, 1 | prngState);
1127
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
1128
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
1129
+ };
1053
1130
  const g = globalThis;
1054
1131
  if (typeof g.Blob === "undefined") {
1055
1132
  try {
@@ -1413,6 +1490,7 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo, p
1413
1490
  globalThis.__REPLAY_RECORDING_MODE__ = false;
1414
1491
  delete g.__REPLAY_REQUEST_COOKIES__;
1415
1492
  delete g.__REPLAY_REQUEST_HEADERS__;
1493
+ Math.random = origMathRandom;
1416
1494
  networkHandle.restore();
1417
1495
  envHandle.restore();
1418
1496
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.50.0",
3
+ "version": "0.52.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {