@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.
Files changed (2) hide show
  1. package/dist/index.js +168 -4
  2. 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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {