@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 +4 -0
- package/dist/index.js +67 -17
- package/package.json +1 -1
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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:
|
|
1663
|
+
originalRequestId: origReqId ?? void 0
|
|
1614
1664
|
})
|
|
1615
1665
|
});
|
|
1616
1666
|
if (!res.ok) {
|