@replayio-app-building/netlify-recorder 0.44.0 → 0.45.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
@@ -121,6 +121,8 @@ interface FinishRequestCallbacks {
121
121
  handlerPath: string;
122
122
  /** Pre-generated request ID. Use as the row ID when provided. */
123
123
  requestId?: string;
124
+ /** ID of the first request handled by this module instance (warm-start linkage). */
125
+ originalRequestId?: string;
124
126
  }) => Promise<string>;
125
127
  }
126
128
 
@@ -250,8 +252,13 @@ interface RecordingResult {
250
252
  *
251
253
  * Accepts either a blob URL (fetched at runtime) or pre-parsed BlobData (avoids needing
252
254
  * globalThis.fetch, which is missing in replay-node's Node v16 environment).
255
+ *
256
+ * When `precedingBlobs` is provided, the handler is executed once for each preceding
257
+ * blob (in order) before the target request. This replays prior requests on the same
258
+ * module instance to populate module-level state (e.g. in-memory caches) that would
259
+ * have existed on a warm Lambda container in production.
253
260
  */
254
- declare function createRequestRecording(blobUrlOrData: string | BlobData, handlerPath: string, requestInfo: RequestInfo): Promise<RecordingResult>;
261
+ declare function createRequestRecording(blobUrlOrData: string | BlobData, handlerPath: string, requestInfo: RequestInfo, precedingBlobs?: BlobData[]): Promise<RecordingResult>;
255
262
 
256
263
  type SqlFunction$2 = (...args: any[]) => Promise<any[]>;
257
264
  interface BackendRequest {
@@ -261,6 +268,7 @@ interface BackendRequest {
261
268
  commit_sha: string;
262
269
  branch_name: string;
263
270
  repository_url: string | null;
271
+ original_request_id: string | null;
264
272
  status: string;
265
273
  recording_id: string | null;
266
274
  error_message: string | null;
@@ -282,6 +290,7 @@ declare function backendRequestsInsert(sql: SqlFunction$2, data: {
282
290
  commitSha: string;
283
291
  branchName: string;
284
292
  repositoryUrl?: string | null;
293
+ originalRequestId?: string | null;
285
294
  }): Promise<string>;
286
295
  declare function backendRequestsGet(sql: SqlFunction$2, id: string): Promise<BackendRequest | null>;
287
296
  declare function backendRequestsGetBlobUrl(sql: SqlFunction$2, id: string): Promise<string | null>;
package/dist/index.js CHANGED
@@ -742,7 +742,8 @@ async function finishRequest(requestContext, callbacks, response, options) {
742
742
  branchName,
743
743
  repositoryUrl,
744
744
  handlerPath,
745
- requestId: options?.requestId
745
+ requestId: options?.requestId,
746
+ originalRequestId: options?.originalRequestId
746
747
  });
747
748
  const storeDuration = Date.now() - storeStart;
748
749
  const totalDuration = Date.now() - finishStart;
@@ -859,7 +860,7 @@ function createRecordingRequestHandler(handler, options) {
859
860
  }
860
861
 
861
862
  // src/createRequestRecording.ts
862
- async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
863
+ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo, precedingBlobs) {
863
864
  let blobData;
864
865
  if (typeof blobUrlOrData === "string") {
865
866
  const response = await fetch(blobUrlOrData);
@@ -1032,56 +1033,98 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
1032
1033
  g.Response = ResponseShim;
1033
1034
  }
1034
1035
  const result = { responseMismatch: false, unconsumedNetworkCalls: false };
1035
- try {
1036
- const handlerModule = await import(handlerPath);
1037
- let handler;
1038
- let isV2 = false;
1039
- let current = handlerModule;
1040
- for (let depth = 0; depth < 6 && current && typeof current === "object"; depth++) {
1041
- const obj = current;
1042
- if (typeof obj.handler === "function") {
1043
- handler = obj.handler;
1044
- isV2 = false;
1045
- break;
1046
- }
1047
- if (typeof obj.default === "function") {
1048
- handler = obj.default;
1036
+ const handlerModule = await import(handlerPath);
1037
+ let handler;
1038
+ let isV2 = false;
1039
+ let current = handlerModule;
1040
+ for (let depth = 0; depth < 6 && current && typeof current === "object"; depth++) {
1041
+ const obj = current;
1042
+ if (typeof obj.handler === "function") {
1043
+ handler = obj.handler;
1044
+ isV2 = false;
1045
+ break;
1046
+ }
1047
+ if (typeof obj.default === "function") {
1048
+ handler = obj.default;
1049
+ isV2 = true;
1050
+ break;
1051
+ }
1052
+ if (obj.default && typeof obj.default === "object") {
1053
+ current = obj.default;
1054
+ continue;
1055
+ }
1056
+ break;
1057
+ }
1058
+ if (!handler && typeof handlerModule === "function") {
1059
+ handler = handlerModule;
1060
+ isV2 = true;
1061
+ }
1062
+ if (!handler && requestInfo.method) {
1063
+ const httpMethodNames = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
1064
+ let target = handlerModule;
1065
+ for (let depth = 0; depth < 6 && target && typeof target === "object"; depth++) {
1066
+ const obj = target;
1067
+ const method = requestInfo.method.toUpperCase();
1068
+ if (httpMethodNames.includes(method) && typeof obj[method] === "function") {
1069
+ handler = obj[method];
1049
1070
  isV2 = true;
1050
1071
  break;
1051
1072
  }
1052
1073
  if (obj.default && typeof obj.default === "object") {
1053
- current = obj.default;
1074
+ target = obj.default;
1054
1075
  continue;
1055
1076
  }
1056
1077
  break;
1057
1078
  }
1058
- if (!handler && typeof handlerModule === "function") {
1059
- handler = handlerModule;
1060
- isV2 = true;
1061
- }
1062
- if (!handler && requestInfo.method) {
1063
- const httpMethodNames = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
1064
- let target = handlerModule;
1065
- for (let depth = 0; depth < 6 && target && typeof target === "object"; depth++) {
1066
- const obj = target;
1067
- const method = requestInfo.method.toUpperCase();
1068
- if (httpMethodNames.includes(method) && typeof obj[method] === "function") {
1069
- handler = obj[method];
1070
- isV2 = true;
1071
- break;
1072
- }
1073
- if (obj.default && typeof obj.default === "object") {
1074
- target = obj.default;
1075
- continue;
1079
+ }
1080
+ if (!handler) {
1081
+ throw new Error(
1082
+ `Could not resolve handler from ${handlerPath}. Module exports: [${Object.keys(handlerModule).join(", ")}]`
1083
+ );
1084
+ }
1085
+ if (precedingBlobs && precedingBlobs.length > 0) {
1086
+ console.log(` [warm-up] Replaying ${precedingBlobs.length} preceding request(s) to populate module-level state\u2026`);
1087
+ for (let i = 0; i < precedingBlobs.length; i++) {
1088
+ const pb = precedingBlobs[i];
1089
+ console.log(` [warm-up ${i + 1}/${precedingBlobs.length}] ${pb.requestInfo.method} ${pb.requestInfo.url}`);
1090
+ const netH = installNetworkInterceptor("replay", pb.capturedData.networkCalls);
1091
+ const envH = installEnvironmentInterceptor("replay", pb.capturedData.envReads);
1092
+ try {
1093
+ if (isV2) {
1094
+ const url = pb.requestInfo.rawUrl ?? `https://localhost${pb.requestInfo.url}`;
1095
+ const reqInit = { method: pb.requestInfo.method, headers: pb.requestInfo.headers };
1096
+ if (pb.requestInfo.body && pb.requestInfo.method !== "GET" && pb.requestInfo.method !== "HEAD") {
1097
+ reqInit.body = pb.requestInfo.body;
1098
+ }
1099
+ const RequestCtor = g.Request;
1100
+ if (RequestCtor) {
1101
+ const req = new RequestCtor(url, reqInit);
1102
+ const ctx = { geo: {}, ip: "127.0.0.1", requestId: "replay-warmup", server: { region: "local" } };
1103
+ g.__REPLAY_REQUEST_COOKIES__ = Object.keys(pb.requestInfo.headers).find((k) => k.toLowerCase() === "cookie") ? pb.requestInfo.headers[Object.keys(pb.requestInfo.headers).find((k) => k.toLowerCase() === "cookie")] : "";
1104
+ g.__REPLAY_REQUEST_HEADERS__ = pb.requestInfo.headers;
1105
+ await handler(req, ctx);
1106
+ }
1107
+ } else {
1108
+ await handler({
1109
+ httpMethod: pb.requestInfo.method,
1110
+ path: pb.requestInfo.url,
1111
+ headers: pb.requestInfo.headers,
1112
+ body: pb.requestInfo.body ?? null,
1113
+ queryStringParameters: pb.requestInfo.queryStringParameters ?? null,
1114
+ multiValueQueryStringParameters: pb.requestInfo.multiValueQueryStringParameters ?? null,
1115
+ rawUrl: pb.requestInfo.rawUrl ?? pb.requestInfo.url,
1116
+ rawQuery: pb.requestInfo.rawQuery ?? "",
1117
+ isBase64Encoded: pb.requestInfo.isBase64Encoded ?? false
1118
+ });
1076
1119
  }
1077
- break;
1120
+ } catch {
1078
1121
  }
1122
+ netH.restore();
1123
+ envH.restore();
1079
1124
  }
1080
- if (!handler) {
1081
- throw new Error(
1082
- `Could not resolve handler from ${handlerPath}. Module exports: [${Object.keys(handlerModule).join(", ")}]`
1083
- );
1084
- }
1125
+ console.log(` [warm-up] Done.`);
1126
+ }
1127
+ try {
1085
1128
  let rawResult;
1086
1129
  try {
1087
1130
  if (isV2) {
@@ -1225,30 +1268,38 @@ async function backendRequestsEnsureTable(sql) {
1225
1268
  await sql`
1226
1269
  CREATE INDEX IF NOT EXISTS idx_backend_requests_created_at ON backend_requests (created_at DESC)
1227
1270
  `;
1271
+ await sql`
1272
+ ALTER TABLE backend_requests ADD COLUMN IF NOT EXISTS original_request_id TEXT
1273
+ `;
1274
+ await sql`
1275
+ CREATE INDEX IF NOT EXISTS idx_backend_requests_original_request_id ON backend_requests (original_request_id)
1276
+ `;
1228
1277
  }
1229
1278
  async function backendRequestsInsert(sql, data) {
1230
1279
  if (data.id) {
1231
1280
  await sql`
1232
- INSERT INTO backend_requests (id, blob_data_url, handler_path, commit_sha, branch_name, repository_url)
1281
+ INSERT INTO backend_requests (id, blob_data_url, handler_path, commit_sha, branch_name, repository_url, original_request_id)
1233
1282
  VALUES (
1234
1283
  ${data.id}::uuid,
1235
1284
  ${data.blobDataUrl},
1236
1285
  ${data.handlerPath},
1237
1286
  ${data.commitSha},
1238
1287
  ${data.branchName},
1239
- ${data.repositoryUrl ?? null}
1288
+ ${data.repositoryUrl ?? null},
1289
+ ${data.originalRequestId ?? null}
1240
1290
  )
1241
1291
  `;
1242
1292
  return data.id;
1243
1293
  }
1244
1294
  const rows = await sql`
1245
- INSERT INTO backend_requests (blob_data_url, handler_path, commit_sha, branch_name, repository_url)
1295
+ INSERT INTO backend_requests (blob_data_url, handler_path, commit_sha, branch_name, repository_url, original_request_id)
1246
1296
  VALUES (
1247
1297
  ${data.blobDataUrl},
1248
1298
  ${data.handlerPath},
1249
1299
  ${data.commitSha},
1250
1300
  ${data.branchName},
1251
- ${data.repositoryUrl ?? null}
1301
+ ${data.repositoryUrl ?? null},
1302
+ ${data.originalRequestId ?? null}
1252
1303
  )
1253
1304
  RETURNING id
1254
1305
  `;
@@ -1339,7 +1390,8 @@ function databaseCallbacks(sql) {
1339
1390
  handlerPath: data.handlerPath,
1340
1391
  commitSha: data.commitSha,
1341
1392
  branchName: data.branchName,
1342
- repositoryUrl: data.repositoryUrl
1393
+ repositoryUrl: data.repositoryUrl,
1394
+ originalRequestId: data.originalRequestId
1343
1395
  });
1344
1396
  }
1345
1397
  };
@@ -1357,7 +1409,8 @@ function remoteCallbacks(recorderUrl) {
1357
1409
  branchName: data.branchName,
1358
1410
  repositoryUrl: data.repositoryUrl,
1359
1411
  handlerPath: data.handlerPath,
1360
- requestId: data.requestId
1412
+ requestId: data.requestId,
1413
+ originalRequestId: data.originalRequestId
1361
1414
  })
1362
1415
  });
1363
1416
  if (!res.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {