@replayio-app-building/netlify-recorder 0.48.0 → 0.50.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
@@ -331,6 +331,10 @@ interface EnsureRequestRecordingOptions {
331
331
  * with the blob data URL from the stored request, updates the row to `"queued"`,
332
332
  * and returns `null`.
333
333
  *
334
+ * For warm-start requests, registers all preceding requests from the same
335
+ * module instance with the recorder service before triggering the recording,
336
+ * so the recorder can replay them to reconstruct module-level state.
337
+ *
334
338
  * This function is idempotent — calling it multiple times for the same request
335
339
  * is safe. Once the recording completes, subsequent calls return the recording ID.
336
340
  */
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 = [];
@@ -148,7 +148,7 @@ function patchHttpModules(mode, calls, consumed) {
148
148
  https.request = origHttpsRequest;
149
149
  };
150
150
  }
151
- function replayHttpRequest(url, _method, calls, consumed, callback) {
151
+ function replayHttpRequest(url, _method, calls, consumed, callback, silent) {
152
152
  let matchIdx = -1;
153
153
  for (let i = 0; i < calls.length; i++) {
154
154
  if (consumed.has(i)) continue;
@@ -173,10 +173,12 @@ function replayHttpRequest(url, _method, calls, consumed, callback) {
173
173
  );
174
174
  }
175
175
  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
- );
176
+ 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
+ );
181
+ }
180
182
  const body = call.responseBody ?? "";
181
183
  const fakeRes = new Readable({
182
184
  read() {
@@ -267,7 +269,8 @@ function ensureCaptureInterceptor() {
267
269
  };
268
270
  globalThis.fetch = captureFetch;
269
271
  }
270
- function installNetworkInterceptor(mode, calls) {
272
+ function installNetworkInterceptor(mode, calls, options) {
273
+ const silent = options?.silent ?? false;
271
274
  if (mode === "capture") {
272
275
  const store = getRequestStore();
273
276
  if (store) {
@@ -385,10 +388,12 @@ function installNetworkInterceptor(mode, calls) {
385
388
  );
386
389
  }
387
390
  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
- );
391
+ 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
+ );
396
+ }
392
397
  const body = call.responseBody ?? "";
393
398
  const status = call.responseStatus;
394
399
  return {
@@ -420,7 +425,7 @@ function installNetworkInterceptor(mode, calls) {
420
425
  };
421
426
  };
422
427
  globalThis.fetch = replayFetch;
423
- const restoreHttp = patchHttpModules("replay", calls, consumed);
428
+ const restoreHttp = patchHttpModules("replay", calls, consumed, silent);
424
429
  return {
425
430
  restore() {
426
431
  globalThis.fetch = originalFetch;
@@ -1249,9 +1254,9 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo, p
1249
1254
  console.log(` [warm-up] Replaying ${precedingBlobs.length} preceding request(s) to populate module-level state\u2026`);
1250
1255
  for (let i = 0; i < precedingBlobs.length; i++) {
1251
1256
  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);
1257
+ const netH = installNetworkInterceptor("replay", pb.capturedData.networkCalls, { silent: true });
1254
1258
  const envH = installEnvironmentInterceptor("replay", pb.capturedData.envReads);
1259
+ let warmupError;
1255
1260
  try {
1256
1261
  if (isV2) {
1257
1262
  const url = pb.requestInfo.rawUrl ?? `https://localhost${pb.requestInfo.url}`;
@@ -1280,10 +1285,18 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo, p
1280
1285
  isBase64Encoded: pb.requestInfo.isBase64Encoded ?? false
1281
1286
  });
1282
1287
  }
1283
- } catch {
1288
+ } catch (err) {
1289
+ warmupError = err instanceof Error ? err.message : String(err);
1284
1290
  }
1291
+ const consumed = netH.consumedCount();
1292
+ const total = netH.totalCount();
1285
1293
  netH.restore();
1286
1294
  envH.restore();
1295
+ if (warmupError) {
1296
+ console.error(` [warm-up ${i + 1}/${precedingBlobs.length}] ${pb.requestInfo.method} ${pb.requestInfo.url} \u2014 ERROR: ${warmupError} (${consumed}/${total} calls)`);
1297
+ } else {
1298
+ console.log(` [warm-up ${i + 1}/${precedingBlobs.length}] ${pb.requestInfo.method} ${pb.requestInfo.url} \u2014 OK (${consumed}/${total} network calls)`);
1299
+ }
1287
1300
  }
1288
1301
  console.log(` [warm-up] Done.`);
1289
1302
  }
@@ -1587,6 +1600,19 @@ function remoteCallbacks(recorderUrl) {
1587
1600
  }
1588
1601
  };
1589
1602
  }
1603
+ async function backendRequestsListPreceding(sql, targetRequestId, originalRequestId) {
1604
+ const rows = await sql`
1605
+ SELECT id, blob_data_url, handler_path, commit_sha, branch_name, repository_url,
1606
+ original_request_id, status, recording_id, error_message, created_at, updated_at
1607
+ FROM backend_requests
1608
+ WHERE original_request_id = ${originalRequestId}
1609
+ AND id != ${targetRequestId}
1610
+ AND created_at <= (SELECT created_at FROM backend_requests WHERE id = ${targetRequestId})
1611
+ AND blob_data_url IS NOT NULL
1612
+ ORDER BY created_at ASC
1613
+ `;
1614
+ return rows;
1615
+ }
1590
1616
  async function ensureRequestRecording(sql, requestId, options) {
1591
1617
  const request = await backendRequestsGet(sql, requestId);
1592
1618
  if (!request) {
@@ -1599,6 +1625,30 @@ async function ensureRequestRecording(sql, requestId, options) {
1599
1625
  return null;
1600
1626
  }
1601
1627
  const recorderUrl = options.recorderUrl.replace(/\/+$/, "");
1628
+ const origReqId = request.original_request_id;
1629
+ if (origReqId && origReqId !== requestId) {
1630
+ const preceding = await backendRequestsListPreceding(sql, requestId, origReqId);
1631
+ if (preceding.length > 0) {
1632
+ await Promise.all(
1633
+ preceding.map(
1634
+ (req) => fetch(`${recorderUrl}/api/store-request`, {
1635
+ method: "POST",
1636
+ headers: { "Content-Type": "application/json" },
1637
+ body: JSON.stringify({
1638
+ requestId: req.id,
1639
+ blobDataUrl: req.blob_data_url,
1640
+ handlerPath: req.handler_path,
1641
+ commitSha: req.commit_sha,
1642
+ branchName: req.branch_name,
1643
+ repositoryUrl: req.repository_url ?? void 0,
1644
+ originalRequestId: req.original_request_id ?? void 0
1645
+ })
1646
+ }).catch(() => {
1647
+ })
1648
+ )
1649
+ );
1650
+ }
1651
+ }
1602
1652
  const res = await fetch(`${recorderUrl}/api/create-recording`, {
1603
1653
  method: "POST",
1604
1654
  headers: { "Content-Type": "application/json" },
@@ -1610,7 +1660,7 @@ async function ensureRequestRecording(sql, requestId, options) {
1610
1660
  branchName: request.branch_name,
1611
1661
  repositoryUrl: request.repository_url ?? void 0,
1612
1662
  webhookUrl: options.webhookUrl,
1613
- originalRequestId: request.original_request_id ?? void 0
1663
+ originalRequestId: origReqId ?? void 0
1614
1664
  })
1615
1665
  });
1616
1666
  if (!res.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {