@replayio-app-building/netlify-recorder 0.37.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;
@@ -1073,6 +1083,9 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
1073
1083
  );
1074
1084
  }
1075
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;
1076
1089
  const context = { geo: {}, ip: "127.0.0.1", requestId: "replay", server: { region: "local" } };
1077
1090
  const response = await handler(req, context);
1078
1091
  if (response && typeof response === "object" && typeof response.status === "number" && typeof response.text === "function") {
@@ -1162,6 +1175,8 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
1162
1175
  console.log(` [network-replay] All ${total} recorded network call(s) were consumed.`);
1163
1176
  }
1164
1177
  globalThis.__REPLAY_RECORDING_MODE__ = false;
1178
+ delete g.__REPLAY_REQUEST_COOKIES__;
1179
+ delete g.__REPLAY_REQUEST_HEADERS__;
1165
1180
  networkHandle.restore();
1166
1181
  envHandle.restore();
1167
1182
  }
@@ -1484,6 +1499,16 @@ function formatStatus(request) {
1484
1499
  }
1485
1500
  return { status: "pending", requestId: request.id };
1486
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
+ }
1487
1512
  function createRecordingEndpoint(options) {
1488
1513
  const { sql, recorderUrl, secret, webhookUrl } = options;
1489
1514
  return async (req) => {
@@ -1507,6 +1532,44 @@ function createRecordingEndpoint(options) {
1507
1532
  return jsonResponse(formatStatus(request), 200);
1508
1533
  }
1509
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
+ }
1510
1573
  const body = await req.json();
1511
1574
  const requestId = body.requestId;
1512
1575
  if (!requestId) {
@@ -1525,10 +1588,11 @@ function createRecordingEndpoint(options) {
1525
1588
  if (request.status === "queued" || request.status === "processing") {
1526
1589
  return jsonResponse({ status: "pending", requestId }, 200);
1527
1590
  }
1591
+ const selfCallbackUrl = `${url.origin}${url.pathname}?_callback=1&requestId=${encodeURIComponent(requestId)}`;
1528
1592
  try {
1529
1593
  const recordingId = await ensureRequestRecording(sql, requestId, {
1530
1594
  recorderUrl,
1531
- webhookUrl
1595
+ webhookUrl: selfCallbackUrl
1532
1596
  });
1533
1597
  if (recordingId) {
1534
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.37.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": {