@replayio-app-building/netlify-recorder 0.36.0 → 0.38.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
@@ -351,6 +351,10 @@ interface RecordingEndpointResponse {
351
351
  * needed and returns the current status. Idempotent: re-posting the same
352
352
  * request ID will not re-queue an already-queued or recorded request.
353
353
  *
354
+ * **POST** with `?_callback=1&requestId=<uuid>` — webhook callback from the
355
+ * recorder service. Updates `backend_requests` status and optionally forwards
356
+ * to the configured `webhookUrl`.
357
+ *
354
358
  * **GET** with `?requestId=<uuid>` — returns the current recording status
355
359
  * without triggering any recording.
356
360
  *
package/dist/index.js CHANGED
@@ -890,11 +890,21 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
890
890
  const nodeCrypto = __require("crypto");
891
891
  g.crypto = {
892
892
  randomUUID: () => nodeCrypto.randomUUID(),
893
- getRandomValues: (buf) => nodeCrypto.getRandomValues(buf)
893
+ getRandomValues: (buf) => nodeCrypto.getRandomValues(buf),
894
+ subtle: nodeCrypto.webcrypto?.subtle
894
895
  };
895
896
  } catch {
896
897
  }
897
898
  }
899
+ if (g.crypto && typeof g.crypto.subtle === "undefined") {
900
+ try {
901
+ const nodeCrypto = __require("crypto");
902
+ if (nodeCrypto.webcrypto?.subtle) {
903
+ g.crypto.subtle = nodeCrypto.webcrypto.subtle;
904
+ }
905
+ } catch {
906
+ }
907
+ }
898
908
  if (typeof g.Headers === "undefined") {
899
909
  const HeadersShim = class Headers {
900
910
  _map;
@@ -1032,6 +1042,24 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
1032
1042
  handler = handlerModule;
1033
1043
  isV2 = true;
1034
1044
  }
1045
+ if (!handler && requestInfo.method) {
1046
+ const httpMethodNames = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
1047
+ let target = handlerModule;
1048
+ for (let depth = 0; depth < 6 && target && typeof target === "object"; depth++) {
1049
+ const obj = target;
1050
+ const method = requestInfo.method.toUpperCase();
1051
+ if (httpMethodNames.includes(method) && typeof obj[method] === "function") {
1052
+ handler = obj[method];
1053
+ isV2 = true;
1054
+ break;
1055
+ }
1056
+ if (obj.default && typeof obj.default === "object") {
1057
+ target = obj.default;
1058
+ continue;
1059
+ }
1060
+ break;
1061
+ }
1062
+ }
1035
1063
  if (!handler) {
1036
1064
  throw new Error(
1037
1065
  `Could not resolve handler from ${handlerPath}. Module exports: [${Object.keys(handlerModule).join(", ")}]`
@@ -1055,6 +1083,9 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
1055
1083
  );
1056
1084
  }
1057
1085
  const req = new RequestCtor(url, reqInit);
1086
+ const cookieKey = Object.keys(requestInfo.headers).find((k) => k.toLowerCase() === "cookie");
1087
+ g.__REPLAY_REQUEST_COOKIES__ = cookieKey ? requestInfo.headers[cookieKey] : "";
1088
+ g.__REPLAY_REQUEST_HEADERS__ = requestInfo.headers;
1058
1089
  const context = { geo: {}, ip: "127.0.0.1", requestId: "replay", server: { region: "local" } };
1059
1090
  const response = await handler(req, context);
1060
1091
  if (response && typeof response === "object" && typeof response.status === "number" && typeof response.text === "function") {
@@ -1144,6 +1175,8 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
1144
1175
  console.log(` [network-replay] All ${total} recorded network call(s) were consumed.`);
1145
1176
  }
1146
1177
  globalThis.__REPLAY_RECORDING_MODE__ = false;
1178
+ delete g.__REPLAY_REQUEST_COOKIES__;
1179
+ delete g.__REPLAY_REQUEST_HEADERS__;
1147
1180
  networkHandle.restore();
1148
1181
  envHandle.restore();
1149
1182
  }
@@ -1466,6 +1499,16 @@ function formatStatus(request) {
1466
1499
  }
1467
1500
  return { status: "pending", requestId: request.id };
1468
1501
  }
1502
+ async function forwardWebhook(url, payload) {
1503
+ try {
1504
+ await fetch(url, {
1505
+ method: "POST",
1506
+ headers: { "Content-Type": "application/json" },
1507
+ body: JSON.stringify(payload)
1508
+ });
1509
+ } catch {
1510
+ }
1511
+ }
1469
1512
  function createRecordingEndpoint(options) {
1470
1513
  const { sql, recorderUrl, secret, webhookUrl } = options;
1471
1514
  return async (req) => {
@@ -1489,6 +1532,44 @@ function createRecordingEndpoint(options) {
1489
1532
  return jsonResponse(formatStatus(request), 200);
1490
1533
  }
1491
1534
  if (req.method === "POST") {
1535
+ const url = new URL(req.url);
1536
+ if (url.searchParams.has("_callback")) {
1537
+ const callbackRequestId = url.searchParams.get("requestId");
1538
+ if (!callbackRequestId) {
1539
+ return jsonResponse({ status: "error", error: "Missing requestId in callback" }, 400);
1540
+ }
1541
+ const callbackBody = await req.json();
1542
+ if (callbackBody.status === "recorded" && callbackBody.recordingId) {
1543
+ await backendRequestsUpdateStatus(
1544
+ sql,
1545
+ callbackRequestId,
1546
+ "recorded",
1547
+ callbackBody.recordingId
1548
+ );
1549
+ if (webhookUrl) {
1550
+ await forwardWebhook(webhookUrl, {
1551
+ status: "recorded",
1552
+ recordingId: callbackBody.recordingId
1553
+ });
1554
+ }
1555
+ return jsonResponse(
1556
+ { status: "recorded", recordingId: callbackBody.recordingId, requestId: callbackRequestId },
1557
+ 200
1558
+ );
1559
+ }
1560
+ if (callbackBody.status === "failed") {
1561
+ const errorMsg = callbackBody.error ?? "Recording failed";
1562
+ await backendRequestsUpdateStatus(sql, callbackRequestId, "failed", void 0, errorMsg);
1563
+ if (webhookUrl) {
1564
+ await forwardWebhook(webhookUrl, { status: "failed", error: errorMsg });
1565
+ }
1566
+ return jsonResponse(
1567
+ { status: "error", requestId: callbackRequestId, error: errorMsg },
1568
+ 200
1569
+ );
1570
+ }
1571
+ return jsonResponse({ status: "error", error: "Invalid callback payload" }, 400);
1572
+ }
1492
1573
  const body = await req.json();
1493
1574
  const requestId = body.requestId;
1494
1575
  if (!requestId) {
@@ -1507,10 +1588,11 @@ function createRecordingEndpoint(options) {
1507
1588
  if (request.status === "queued" || request.status === "processing") {
1508
1589
  return jsonResponse({ status: "pending", requestId }, 200);
1509
1590
  }
1591
+ const selfCallbackUrl = `${url.origin}${url.pathname}?_callback=1&requestId=${encodeURIComponent(requestId)}`;
1510
1592
  try {
1511
1593
  const recordingId = await ensureRequestRecording(sql, requestId, {
1512
1594
  recorderUrl,
1513
- webhookUrl
1595
+ webhookUrl: selfCallbackUrl
1514
1596
  });
1515
1597
  if (recordingId) {
1516
1598
  return jsonResponse({ status: "recorded", recordingId, requestId }, 200);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {