@replayio-app-building/netlify-recorder 0.49.0 → 0.51.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
@@ -55,7 +55,7 @@ function buildSetConfigQueries(requestId, callIndex) {
55
55
  { query: "SELECT set_config('app.replay_call_index', $1, true)", params: [String(callIndex)] }
56
56
  ];
57
57
  }
58
- function patchHttpModules(mode, calls, consumed) {
58
+ function patchHttpModules(mode, calls, consumed, silent) {
59
59
  const origHttpRequest = http.request;
60
60
  const origHttpsRequest = https.request;
61
61
  function makeInterceptedRequest(mod, origRequest, protocol, args) {
@@ -87,7 +87,7 @@ function patchHttpModules(mode, calls, consumed) {
87
87
  const urlStr = `${protocol}//${host}${port}${path}`;
88
88
  const method = (options.method || "GET").toUpperCase();
89
89
  if (mode === "replay") {
90
- return replayHttpRequest(urlStr, method, calls, consumed, callback);
90
+ return replayHttpRequest(urlStr, method, calls, consumed, callback, silent);
91
91
  }
92
92
  const req = origRequest.call(mod, ...args);
93
93
  const bodyChunks = [];
@@ -102,37 +102,46 @@ function patchHttpModules(mode, calls, consumed) {
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;
@@ -148,7 +157,7 @@ function patchHttpModules(mode, calls, consumed) {
148
157
  https.request = origHttpsRequest;
149
158
  };
150
159
  }
151
- function replayHttpRequest(url, _method, calls, consumed, callback) {
160
+ function replayHttpRequest(url, _method, calls, consumed, callback, silent) {
152
161
  let matchIdx = -1;
153
162
  for (let i = 0; i < calls.length; i++) {
154
163
  if (consumed.has(i)) continue;
@@ -173,10 +182,19 @@ function replayHttpRequest(url, _method, calls, consumed, callback) {
173
182
  );
174
183
  }
175
184
  consumed.add(matchIdx);
176
- const duration = call.endTime - call.startTime;
177
- console.log(
178
- ` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus} (original: ${duration}ms)`
179
- );
185
+ const isPending = call.pending || call.endTime === 0 && call.responseStatus === 0;
186
+ if (!silent) {
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
+ }
197
+ }
180
198
  const body = call.responseBody ?? "";
181
199
  const fakeRes = new Readable({
182
200
  read() {
@@ -184,7 +202,7 @@ function replayHttpRequest(url, _method, calls, consumed, callback) {
184
202
  this.push(null);
185
203
  }
186
204
  });
187
- fakeRes.statusCode = call.responseStatus;
205
+ fakeRes.statusCode = isPending ? 200 : call.responseStatus;
188
206
  fakeRes.statusMessage = "";
189
207
  fakeRes.headers = {};
190
208
  if (call.responseHeaders) {
@@ -244,6 +262,20 @@ function ensureCaptureInterceptor() {
244
262
  );
245
263
  }
246
264
  const startTime = Date.now();
265
+ const entry = {
266
+ url,
267
+ method,
268
+ requestHeaders,
269
+ requestBody,
270
+ responseStatus: 0,
271
+ responseHeaders: {},
272
+ responseBody: void 0,
273
+ timestamp: startTime,
274
+ startTime,
275
+ endTime: 0,
276
+ pending: true
277
+ };
278
+ calls.push(entry);
247
279
  const response = await _realOriginalFetch(input, init);
248
280
  const endTime = Date.now();
249
281
  const responseBody = await response.clone().text();
@@ -251,23 +283,18 @@ function ensureCaptureInterceptor() {
251
283
  response.headers.forEach((v, k) => {
252
284
  responseHeaders[k] = v;
253
285
  });
254
- calls.push({
255
- url,
256
- method,
257
- requestHeaders,
258
- requestBody,
259
- responseStatus: response.status,
260
- responseHeaders,
261
- responseBody,
262
- timestamp: endTime,
263
- startTime,
264
- endTime
265
- });
286
+ entry.responseStatus = response.status;
287
+ entry.responseHeaders = responseHeaders;
288
+ entry.responseBody = responseBody;
289
+ entry.timestamp = endTime;
290
+ entry.endTime = endTime;
291
+ entry.pending = false;
266
292
  return response;
267
293
  };
268
294
  globalThis.fetch = captureFetch;
269
295
  }
270
- function installNetworkInterceptor(mode, calls) {
296
+ function installNetworkInterceptor(mode, calls, options) {
297
+ const silent = options?.silent ?? false;
271
298
  if (mode === "capture") {
272
299
  const store = getRequestStore();
273
300
  if (store) {
@@ -317,6 +344,20 @@ function installNetworkInterceptor(mode, calls) {
317
344
  );
318
345
  }
319
346
  const startTime = Date.now();
347
+ const entry = {
348
+ url,
349
+ method,
350
+ requestHeaders,
351
+ requestBody,
352
+ responseStatus: 0,
353
+ responseHeaders: {},
354
+ responseBody: void 0,
355
+ timestamp: startTime,
356
+ startTime,
357
+ endTime: 0,
358
+ pending: true
359
+ };
360
+ calls.push(entry);
320
361
  const response = await originalFetch2(input, init);
321
362
  const endTime = Date.now();
322
363
  const responseBody = await response.clone().text();
@@ -324,18 +365,12 @@ function installNetworkInterceptor(mode, calls) {
324
365
  response.headers.forEach((v, k) => {
325
366
  responseHeaders[k] = v;
326
367
  });
327
- calls.push({
328
- url,
329
- method,
330
- requestHeaders,
331
- requestBody,
332
- responseStatus: response.status,
333
- responseHeaders,
334
- responseBody,
335
- timestamp: endTime,
336
- startTime,
337
- endTime
338
- });
368
+ entry.responseStatus = response.status;
369
+ entry.responseHeaders = responseHeaders;
370
+ entry.responseBody = responseBody;
371
+ entry.timestamp = endTime;
372
+ entry.endTime = endTime;
373
+ entry.pending = false;
339
374
  return response;
340
375
  };
341
376
  globalThis.fetch = captureFetch;
@@ -385,12 +420,21 @@ function installNetworkInterceptor(mode, calls) {
385
420
  );
386
421
  }
387
422
  consumed.add(matchIdx);
388
- const duration = call.endTime - call.startTime;
389
- console.log(
390
- ` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus} (original: ${duration}ms)`
391
- );
423
+ const isPending = call.pending || call.endTime === 0 && call.responseStatus === 0;
424
+ if (!silent) {
425
+ if (isPending) {
426
+ console.log(
427
+ ` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => (fire-and-forget, no response captured)`
428
+ );
429
+ } else {
430
+ const duration = call.endTime - call.startTime;
431
+ console.log(
432
+ ` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus} (original: ${duration}ms)`
433
+ );
434
+ }
435
+ }
392
436
  const body = call.responseBody ?? "";
393
- const status = call.responseStatus;
437
+ const status = isPending ? 200 : call.responseStatus;
394
438
  return {
395
439
  ok: status >= 200 && status < 300,
396
440
  status,
@@ -420,7 +464,7 @@ function installNetworkInterceptor(mode, calls) {
420
464
  };
421
465
  };
422
466
  globalThis.fetch = replayFetch;
423
- const restoreHttp = patchHttpModules("replay", calls, consumed);
467
+ const restoreHttp = patchHttpModules("replay", calls, consumed, silent);
424
468
  return {
425
469
  restore() {
426
470
  globalThis.fetch = originalFetch;
@@ -1249,9 +1293,9 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo, p
1249
1293
  console.log(` [warm-up] Replaying ${precedingBlobs.length} preceding request(s) to populate module-level state\u2026`);
1250
1294
  for (let i = 0; i < precedingBlobs.length; i++) {
1251
1295
  const pb = precedingBlobs[i];
1252
- console.log(` [warm-up ${i + 1}/${precedingBlobs.length}] ${pb.requestInfo.method} ${pb.requestInfo.url}`);
1253
- const netH = installNetworkInterceptor("replay", pb.capturedData.networkCalls);
1296
+ const netH = installNetworkInterceptor("replay", pb.capturedData.networkCalls, { silent: true });
1254
1297
  const envH = installEnvironmentInterceptor("replay", pb.capturedData.envReads);
1298
+ let warmupError;
1255
1299
  try {
1256
1300
  if (isV2) {
1257
1301
  const url = pb.requestInfo.rawUrl ?? `https://localhost${pb.requestInfo.url}`;
@@ -1280,10 +1324,18 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo, p
1280
1324
  isBase64Encoded: pb.requestInfo.isBase64Encoded ?? false
1281
1325
  });
1282
1326
  }
1283
- } catch {
1327
+ } catch (err) {
1328
+ warmupError = err instanceof Error ? err.message : String(err);
1284
1329
  }
1330
+ const consumed = netH.consumedCount();
1331
+ const total = netH.totalCount();
1285
1332
  netH.restore();
1286
1333
  envH.restore();
1334
+ if (warmupError) {
1335
+ console.error(` [warm-up ${i + 1}/${precedingBlobs.length}] ${pb.requestInfo.method} ${pb.requestInfo.url} \u2014 ERROR: ${warmupError} (${consumed}/${total} calls)`);
1336
+ } else {
1337
+ console.log(` [warm-up ${i + 1}/${precedingBlobs.length}] ${pb.requestInfo.method} ${pb.requestInfo.url} \u2014 OK (${consumed}/${total} network calls)`);
1338
+ }
1287
1339
  }
1288
1340
  console.log(` [warm-up] Done.`);
1289
1341
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {