@replayio-app-building/netlify-recorder 0.46.0 → 0.48.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.js +168 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,6 +32,9 @@ function incrementCallIndex() {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// src/interceptors/network.ts
|
|
35
|
+
import http from "http";
|
|
36
|
+
import https from "https";
|
|
37
|
+
import { Readable, Writable } from "stream";
|
|
35
38
|
function isNeonQuery(obj) {
|
|
36
39
|
return typeof obj === "object" && obj !== null && "query" in obj && "params" in obj;
|
|
37
40
|
}
|
|
@@ -52,6 +55,159 @@ function buildSetConfigQueries(requestId, callIndex) {
|
|
|
52
55
|
{ query: "SELECT set_config('app.replay_call_index', $1, true)", params: [String(callIndex)] }
|
|
53
56
|
];
|
|
54
57
|
}
|
|
58
|
+
function patchHttpModules(mode, calls, consumed) {
|
|
59
|
+
const origHttpRequest = http.request;
|
|
60
|
+
const origHttpsRequest = https.request;
|
|
61
|
+
function makeInterceptedRequest(mod, origRequest, protocol, args) {
|
|
62
|
+
let options;
|
|
63
|
+
let callback;
|
|
64
|
+
if (typeof args[0] === "string" || args[0] instanceof URL) {
|
|
65
|
+
const urlStr2 = typeof args[0] === "string" ? args[0] : args[0].href;
|
|
66
|
+
options = typeof args[1] === "object" && args[1] !== null ? { ...args[1] } : {};
|
|
67
|
+
try {
|
|
68
|
+
const parsed = new URL(urlStr2);
|
|
69
|
+
options.hostname = options.hostname ?? parsed.hostname;
|
|
70
|
+
options.port = options.port ?? parsed.port;
|
|
71
|
+
options.path = options.path ?? parsed.pathname + parsed.search;
|
|
72
|
+
options.protocol = options.protocol ?? parsed.protocol;
|
|
73
|
+
} catch {
|
|
74
|
+
options.path = options.path ?? urlStr2;
|
|
75
|
+
}
|
|
76
|
+
callback = typeof args[2] === "function" ? args[2] : void 0;
|
|
77
|
+
if (!callback && typeof args[1] === "function") {
|
|
78
|
+
callback = args[1];
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
options = typeof args[0] === "object" && args[0] !== null ? { ...args[0] } : {};
|
|
82
|
+
callback = typeof args[1] === "function" ? args[1] : void 0;
|
|
83
|
+
}
|
|
84
|
+
const host = options.hostname || options.host || "localhost";
|
|
85
|
+
const port = options.port ? `:${options.port}` : "";
|
|
86
|
+
const path = options.path || "/";
|
|
87
|
+
const urlStr = `${protocol}//${host}${port}${path}`;
|
|
88
|
+
const method = (options.method || "GET").toUpperCase();
|
|
89
|
+
if (mode === "replay") {
|
|
90
|
+
return replayHttpRequest(urlStr, method, calls, consumed, callback);
|
|
91
|
+
}
|
|
92
|
+
const req = origRequest.call(mod, ...args);
|
|
93
|
+
const bodyChunks = [];
|
|
94
|
+
const origWrite = req.write.bind(req);
|
|
95
|
+
const origEnd = req.end.bind(req);
|
|
96
|
+
req.write = function(chunk, ...rest) {
|
|
97
|
+
if (chunk) bodyChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
98
|
+
return origWrite(chunk, ...rest);
|
|
99
|
+
};
|
|
100
|
+
req.end = function(chunk, ...rest) {
|
|
101
|
+
if (chunk && typeof chunk !== "function") bodyChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
102
|
+
return origEnd(chunk, ...rest);
|
|
103
|
+
};
|
|
104
|
+
const startTime = Date.now();
|
|
105
|
+
req.on("response", (res) => {
|
|
106
|
+
const resChunks = [];
|
|
107
|
+
res.on("data", (chunk) => resChunks.push(chunk));
|
|
108
|
+
res.on("end", () => {
|
|
109
|
+
const endTime = Date.now();
|
|
110
|
+
const requestHeaders = {};
|
|
111
|
+
if (options.headers) {
|
|
112
|
+
for (const [k, v] of Object.entries(options.headers)) {
|
|
113
|
+
if (v !== void 0) requestHeaders[k] = Array.isArray(v) ? v.join(", ") : String(v);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const responseHeaders = {};
|
|
117
|
+
if (res.headers) {
|
|
118
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
119
|
+
if (v !== void 0) responseHeaders[k] = Array.isArray(v) ? v.join(", ") : String(v);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const store = getRequestStore();
|
|
123
|
+
const targetCalls = store?.networkCalls ?? calls;
|
|
124
|
+
targetCalls.push({
|
|
125
|
+
url: urlStr,
|
|
126
|
+
method,
|
|
127
|
+
requestHeaders,
|
|
128
|
+
requestBody: bodyChunks.length > 0 ? Buffer.concat(bodyChunks).toString("utf-8") : void 0,
|
|
129
|
+
responseStatus: res.statusCode ?? 0,
|
|
130
|
+
responseHeaders,
|
|
131
|
+
responseBody: Buffer.concat(resChunks).toString("utf-8"),
|
|
132
|
+
timestamp: endTime,
|
|
133
|
+
startTime,
|
|
134
|
+
endTime
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
return req;
|
|
139
|
+
}
|
|
140
|
+
http.request = function(...args) {
|
|
141
|
+
return makeInterceptedRequest(http, origHttpRequest, "http:", args);
|
|
142
|
+
};
|
|
143
|
+
https.request = function(...args) {
|
|
144
|
+
return makeInterceptedRequest(https, origHttpsRequest, "https:", args);
|
|
145
|
+
};
|
|
146
|
+
return () => {
|
|
147
|
+
http.request = origHttpRequest;
|
|
148
|
+
https.request = origHttpsRequest;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function replayHttpRequest(url, _method, calls, consumed, callback) {
|
|
152
|
+
let matchIdx = -1;
|
|
153
|
+
for (let i = 0; i < calls.length; i++) {
|
|
154
|
+
if (consumed.has(i)) continue;
|
|
155
|
+
const c = calls[i];
|
|
156
|
+
if (c && c.url === url) {
|
|
157
|
+
matchIdx = i;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (matchIdx === -1) {
|
|
162
|
+
for (let i = 0; i < calls.length; i++) {
|
|
163
|
+
if (!consumed.has(i)) {
|
|
164
|
+
matchIdx = i;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const call = calls[matchIdx];
|
|
170
|
+
if (matchIdx === -1 || !call) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`No more recorded network calls to replay for http/https request (exhausted ${calls.length} calls)`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
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
|
+
);
|
|
180
|
+
const body = call.responseBody ?? "";
|
|
181
|
+
const fakeRes = new Readable({
|
|
182
|
+
read() {
|
|
183
|
+
this.push(body);
|
|
184
|
+
this.push(null);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
fakeRes.statusCode = call.responseStatus;
|
|
188
|
+
fakeRes.statusMessage = "";
|
|
189
|
+
fakeRes.headers = {};
|
|
190
|
+
if (call.responseHeaders) {
|
|
191
|
+
for (const [k, v] of Object.entries(call.responseHeaders)) {
|
|
192
|
+
fakeRes.headers[k.toLowerCase()] = v;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const fakeReq = new Writable({
|
|
196
|
+
write(_chunk, _encoding, cb) {
|
|
197
|
+
cb();
|
|
198
|
+
},
|
|
199
|
+
final(cb) {
|
|
200
|
+
cb();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
fakeReq.aborted = false;
|
|
204
|
+
fakeReq.reusedSocket = false;
|
|
205
|
+
process.nextTick(() => {
|
|
206
|
+
if (callback) callback(fakeRes);
|
|
207
|
+
fakeReq.emit("response", fakeRes);
|
|
208
|
+
});
|
|
209
|
+
return fakeReq;
|
|
210
|
+
}
|
|
55
211
|
var _realOriginalFetch = null;
|
|
56
212
|
var _captureInterceptorInstalled = false;
|
|
57
213
|
function ensureCaptureInterceptor() {
|
|
@@ -117,10 +273,12 @@ function installNetworkInterceptor(mode, calls) {
|
|
|
117
273
|
if (store) {
|
|
118
274
|
ensureCaptureInterceptor();
|
|
119
275
|
store.networkCalls = calls;
|
|
276
|
+
const restoreHttp3 = patchHttpModules("capture", calls, null);
|
|
120
277
|
return {
|
|
121
278
|
restore() {
|
|
122
279
|
const s = getRequestStore();
|
|
123
280
|
if (s) s.networkCalls = null;
|
|
281
|
+
restoreHttp3();
|
|
124
282
|
},
|
|
125
283
|
consumedCount() {
|
|
126
284
|
return 0;
|
|
@@ -181,9 +339,11 @@ function installNetworkInterceptor(mode, calls) {
|
|
|
181
339
|
return response;
|
|
182
340
|
};
|
|
183
341
|
globalThis.fetch = captureFetch;
|
|
342
|
+
const restoreHttp2 = patchHttpModules("capture", calls, null);
|
|
184
343
|
return {
|
|
185
344
|
restore() {
|
|
186
345
|
globalThis.fetch = originalFetch2;
|
|
346
|
+
restoreHttp2();
|
|
187
347
|
},
|
|
188
348
|
consumedCount() {
|
|
189
349
|
return 0;
|
|
@@ -225,8 +385,9 @@ function installNetworkInterceptor(mode, calls) {
|
|
|
225
385
|
);
|
|
226
386
|
}
|
|
227
387
|
consumed.add(matchIdx);
|
|
388
|
+
const duration = call.endTime - call.startTime;
|
|
228
389
|
console.log(
|
|
229
|
-
` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus}`
|
|
390
|
+
` [network-replay] Consumed call ${consumed.size}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus} (original: ${duration}ms)`
|
|
230
391
|
);
|
|
231
392
|
const body = call.responseBody ?? "";
|
|
232
393
|
const status = call.responseStatus;
|
|
@@ -259,9 +420,11 @@ function installNetworkInterceptor(mode, calls) {
|
|
|
259
420
|
};
|
|
260
421
|
};
|
|
261
422
|
globalThis.fetch = replayFetch;
|
|
423
|
+
const restoreHttp = patchHttpModules("replay", calls, consumed);
|
|
262
424
|
return {
|
|
263
425
|
restore() {
|
|
264
426
|
globalThis.fetch = originalFetch;
|
|
427
|
+
restoreHttp();
|
|
265
428
|
},
|
|
266
429
|
consumedCount() {
|
|
267
430
|
return consumed.size;
|
|
@@ -1308,7 +1471,7 @@ async function backendRequestsInsert(sql, data) {
|
|
|
1308
1471
|
async function backendRequestsGet(sql, id) {
|
|
1309
1472
|
const rows = await sql`
|
|
1310
1473
|
SELECT id, blob_data_url, handler_path, commit_sha, branch_name, repository_url,
|
|
1311
|
-
status, recording_id, error_message, created_at, updated_at
|
|
1474
|
+
original_request_id, status, recording_id, error_message, created_at, updated_at
|
|
1312
1475
|
FROM backend_requests WHERE id = ${id}
|
|
1313
1476
|
`;
|
|
1314
1477
|
return rows[0] ?? null;
|
|
@@ -1324,7 +1487,7 @@ async function backendRequestsList(sql, filters) {
|
|
|
1324
1487
|
if (filters?.status) {
|
|
1325
1488
|
const rows2 = await sql`
|
|
1326
1489
|
SELECT id, blob_data_url, handler_path, commit_sha, branch_name, repository_url,
|
|
1327
|
-
status, recording_id, error_message, created_at, updated_at
|
|
1490
|
+
original_request_id, status, recording_id, error_message, created_at, updated_at
|
|
1328
1491
|
FROM backend_requests
|
|
1329
1492
|
WHERE status = ${filters.status}
|
|
1330
1493
|
ORDER BY created_at DESC
|
|
@@ -1446,7 +1609,8 @@ async function ensureRequestRecording(sql, requestId, options) {
|
|
|
1446
1609
|
commitSha: request.commit_sha,
|
|
1447
1610
|
branchName: request.branch_name,
|
|
1448
1611
|
repositoryUrl: request.repository_url ?? void 0,
|
|
1449
|
-
webhookUrl: options.webhookUrl
|
|
1612
|
+
webhookUrl: options.webhookUrl,
|
|
1613
|
+
originalRequestId: request.original_request_id ?? void 0
|
|
1450
1614
|
})
|
|
1451
1615
|
});
|
|
1452
1616
|
if (!res.ok) {
|