@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 +24 -1
- package/dist/index.js +108 -50
- package/package.json +1 -1
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
-
|
|
1074
|
+
target = obj.default;
|
|
1049
1075
|
continue;
|
|
1050
1076
|
}
|
|
1051
1077
|
break;
|
|
1052
1078
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1120
|
+
} catch {
|
|
1073
1121
|
}
|
|
1122
|
+
netH.restore();
|
|
1123
|
+
envH.restore();
|
|
1074
1124
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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) {
|