@replayio-app-building/netlify-recorder 0.43.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
@@ -95,6 +95,14 @@ interface BlobData {
95
95
  endTime: number;
96
96
  /** The response returned to the client, used to detect replay mismatches. */
97
97
  handlerResponse?: HandlerResponse$1;
98
+ /**
99
+ * The request ID of the first request handled by this module instance.
100
+ * When this differs from the current request's ID, the execution environment
101
+ * was reused (warm start) and module-level state from the original request
102
+ * may have affected this request's behavior (e.g. in-memory caches skipping
103
+ * network calls).
104
+ */
105
+ originalRequestId?: string;
98
106
  }
99
107
  interface FinishRequestCallbacks {
100
108
  /**
@@ -113,6 +121,8 @@ interface FinishRequestCallbacks {
113
121
  handlerPath: string;
114
122
  /** Pre-generated request ID. Use as the row ID when provided. */
115
123
  requestId?: string;
124
+ /** ID of the first request handled by this module instance (warm-start linkage). */
125
+ originalRequestId?: string;
116
126
  }) => Promise<string>;
117
127
  }
118
128
 
@@ -178,6 +188,12 @@ interface FinishRequestOptions {
178
188
  * sent to the client in the `X-Replay-Request-Id` header.
179
189
  */
180
190
  requestId?: string;
191
+ /**
192
+ * The request ID of the first request handled by this module instance.
193
+ * Included in the blob data to link warm-start requests back to the
194
+ * cold-start request that populated module-level caches.
195
+ */
196
+ originalRequestId?: string;
181
197
  }
182
198
  /**
183
199
  * Called at the end of the handler execution.
@@ -236,8 +252,13 @@ interface RecordingResult {
236
252
  *
237
253
  * Accepts either a blob URL (fetched at runtime) or pre-parsed BlobData (avoids needing
238
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.
239
260
  */
240
- 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>;
241
262
 
242
263
  type SqlFunction$2 = (...args: any[]) => Promise<any[]>;
243
264
  interface BackendRequest {
@@ -247,6 +268,7 @@ interface BackendRequest {
247
268
  commit_sha: string;
248
269
  branch_name: string;
249
270
  repository_url: string | null;
271
+ original_request_id: string | null;
250
272
  status: string;
251
273
  recording_id: string | null;
252
274
  error_message: string | null;
@@ -268,6 +290,7 @@ declare function backendRequestsInsert(sql: SqlFunction$2, data: {
268
290
  commitSha: string;
269
291
  branchName: string;
270
292
  repositoryUrl?: string | null;
293
+ originalRequestId?: string | null;
271
294
  }): Promise<string>;
272
295
  declare function backendRequestsGet(sql: SqlFunction$2, id: string): Promise<BackendRequest | null>;
273
296
  declare function backendRequestsGetBlobUrl(sql: SqlFunction$2, id: string): Promise<string | null>;
package/dist/index.js CHANGED
@@ -730,7 +730,8 @@ async function finishRequest(requestContext, callbacks, response, options) {
730
730
  handlerResponse: {
731
731
  statusCode: response.statusCode ?? response.status ?? 0,
732
732
  body: typeof response.body === "string" ? response.body : void 0
733
- }
733
+ },
734
+ originalRequestId: options?.originalRequestId
734
735
  };
735
736
  const blobData = redactBlobData(rawBlobData);
736
737
  const blobContent = JSON.stringify(blobData);
@@ -741,7 +742,8 @@ async function finishRequest(requestContext, callbacks, response, options) {
741
742
  branchName,
742
743
  repositoryUrl,
743
744
  handlerPath,
744
- requestId: options?.requestId
745
+ requestId: options?.requestId,
746
+ originalRequestId: options?.originalRequestId
745
747
  });
746
748
  const storeDuration = Date.now() - storeStart;
747
749
  const totalDuration = Date.now() - finishStart;
@@ -761,9 +763,13 @@ async function finishRequest(requestContext, callbacks, response, options) {
761
763
 
762
764
  // src/createRecordingRequestHandler.ts
763
765
  import crypto2 from "crypto";
766
+ var _originalRequestId = null;
764
767
  function createRecordingRequestHandler(handler, options) {
765
768
  return async (event, context) => {
766
769
  const requestId = crypto2.randomUUID();
770
+ if (_originalRequestId === null) {
771
+ _originalRequestId = requestId;
772
+ }
767
773
  return runInRequestContext(requestId, async () => {
768
774
  const reqContext = startRequest(event);
769
775
  let response;
@@ -786,7 +792,7 @@ function createRecordingRequestHandler(handler, options) {
786
792
  statusCode: 500,
787
793
  body: JSON.stringify({ error: errorMessage })
788
794
  };
789
- const finishOpts2 = { ...options, requestId };
795
+ const finishOpts2 = { ...options, requestId, originalRequestId: _originalRequestId };
790
796
  const ctx2 = context;
791
797
  if (ctx2 && typeof ctx2.waitUntil === "function") {
792
798
  ctx2.waitUntil(
@@ -825,7 +831,7 @@ function createRecordingRequestHandler(handler, options) {
825
831
  "X-Replay-Request-Id": requestId
826
832
  }
827
833
  };
828
- const finishOpts = { ...options, requestId };
834
+ const finishOpts = { ...options, requestId, originalRequestId: _originalRequestId };
829
835
  const ctx = context;
830
836
  if (ctx && typeof ctx.waitUntil === "function") {
831
837
  ctx.waitUntil(
@@ -854,7 +860,7 @@ function createRecordingRequestHandler(handler, options) {
854
860
  }
855
861
 
856
862
  // src/createRequestRecording.ts
857
- async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
863
+ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo, precedingBlobs) {
858
864
  let blobData;
859
865
  if (typeof blobUrlOrData === "string") {
860
866
  const response = await fetch(blobUrlOrData);
@@ -1027,56 +1033,98 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
1027
1033
  g.Response = ResponseShim;
1028
1034
  }
1029
1035
  const result = { responseMismatch: false, unconsumedNetworkCalls: false };
1030
- try {
1031
- const handlerModule = await import(handlerPath);
1032
- let handler;
1033
- let isV2 = false;
1034
- let current = handlerModule;
1035
- for (let depth = 0; depth < 6 && current && typeof current === "object"; depth++) {
1036
- const obj = current;
1037
- if (typeof obj.handler === "function") {
1038
- handler = obj.handler;
1039
- isV2 = false;
1040
- break;
1041
- }
1042
- if (typeof obj.default === "function") {
1043
- 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];
1044
1070
  isV2 = true;
1045
1071
  break;
1046
1072
  }
1047
1073
  if (obj.default && typeof obj.default === "object") {
1048
- current = obj.default;
1074
+ target = obj.default;
1049
1075
  continue;
1050
1076
  }
1051
1077
  break;
1052
1078
  }
1053
- if (!handler && typeof handlerModule === "function") {
1054
- handler = handlerModule;
1055
- isV2 = true;
1056
- }
1057
- if (!handler && requestInfo.method) {
1058
- const httpMethodNames = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
1059
- let target = handlerModule;
1060
- for (let depth = 0; depth < 6 && target && typeof target === "object"; depth++) {
1061
- const obj = target;
1062
- const method = requestInfo.method.toUpperCase();
1063
- if (httpMethodNames.includes(method) && typeof obj[method] === "function") {
1064
- handler = obj[method];
1065
- isV2 = true;
1066
- break;
1067
- }
1068
- if (obj.default && typeof obj.default === "object") {
1069
- target = obj.default;
1070
- 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
+ });
1071
1119
  }
1072
- break;
1120
+ } catch {
1073
1121
  }
1122
+ netH.restore();
1123
+ envH.restore();
1074
1124
  }
1075
- if (!handler) {
1076
- throw new Error(
1077
- `Could not resolve handler from ${handlerPath}. Module exports: [${Object.keys(handlerModule).join(", ")}]`
1078
- );
1079
- }
1125
+ console.log(` [warm-up] Done.`);
1126
+ }
1127
+ try {
1080
1128
  let rawResult;
1081
1129
  try {
1082
1130
  if (isV2) {
@@ -1220,30 +1268,38 @@ async function backendRequestsEnsureTable(sql) {
1220
1268
  await sql`
1221
1269
  CREATE INDEX IF NOT EXISTS idx_backend_requests_created_at ON backend_requests (created_at DESC)
1222
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
+ `;
1223
1277
  }
1224
1278
  async function backendRequestsInsert(sql, data) {
1225
1279
  if (data.id) {
1226
1280
  await sql`
1227
- 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)
1228
1282
  VALUES (
1229
1283
  ${data.id}::uuid,
1230
1284
  ${data.blobDataUrl},
1231
1285
  ${data.handlerPath},
1232
1286
  ${data.commitSha},
1233
1287
  ${data.branchName},
1234
- ${data.repositoryUrl ?? null}
1288
+ ${data.repositoryUrl ?? null},
1289
+ ${data.originalRequestId ?? null}
1235
1290
  )
1236
1291
  `;
1237
1292
  return data.id;
1238
1293
  }
1239
1294
  const rows = await sql`
1240
- 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)
1241
1296
  VALUES (
1242
1297
  ${data.blobDataUrl},
1243
1298
  ${data.handlerPath},
1244
1299
  ${data.commitSha},
1245
1300
  ${data.branchName},
1246
- ${data.repositoryUrl ?? null}
1301
+ ${data.repositoryUrl ?? null},
1302
+ ${data.originalRequestId ?? null}
1247
1303
  )
1248
1304
  RETURNING id
1249
1305
  `;
@@ -1334,7 +1390,8 @@ function databaseCallbacks(sql) {
1334
1390
  handlerPath: data.handlerPath,
1335
1391
  commitSha: data.commitSha,
1336
1392
  branchName: data.branchName,
1337
- repositoryUrl: data.repositoryUrl
1393
+ repositoryUrl: data.repositoryUrl,
1394
+ originalRequestId: data.originalRequestId
1338
1395
  });
1339
1396
  }
1340
1397
  };
@@ -1352,7 +1409,8 @@ function remoteCallbacks(recorderUrl) {
1352
1409
  branchName: data.branchName,
1353
1410
  repositoryUrl: data.repositoryUrl,
1354
1411
  handlerPath: data.handlerPath,
1355
- requestId: data.requestId
1412
+ requestId: data.requestId,
1413
+ originalRequestId: data.originalRequestId
1356
1414
  })
1357
1415
  });
1358
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.43.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": {