@replayio-app-building/netlify-recorder 0.5.0 → 0.7.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/README.md CHANGED
@@ -59,12 +59,7 @@ import type { Handler } from "@netlify/functions";
59
59
  const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
60
60
 
61
61
  const handler: Handler = async (event) => {
62
- const reqContext = startRequest({
63
- method: event.httpMethod,
64
- url: event.path,
65
- headers: event.headers as Record<string, string>,
66
- body: event.body ?? undefined,
67
- });
62
+ const reqContext = startRequest(event);
68
63
 
69
64
  try {
70
65
  const result = await myBusinessLogic();
@@ -154,12 +149,7 @@ import { startRequest, finishRequest } from "@replayio-app-building/netlify-reco
154
149
  import type { Handler } from "@netlify/functions";
155
150
 
156
151
  const handler: Handler = async (event) => {
157
- const reqContext = startRequest({
158
- method: event.httpMethod,
159
- url: event.path,
160
- headers: event.headers as Record<string, string>,
161
- body: event.body ?? undefined,
162
- });
152
+ const reqContext = startRequest(event);
163
153
 
164
154
  try {
165
155
  const result = await myBusinessLogic();
@@ -300,15 +290,12 @@ Self-hosted recording requires these environment variables:
300
290
 
301
291
  ## API Reference
302
292
 
303
- ### `startRequest(requestInfo): RequestContext`
293
+ ### `startRequest(event): RequestContext`
304
294
 
305
- Begins capturing a Netlify handler execution. Patches `globalThis.fetch` and `process.env` to record all outbound network calls and environment variable reads.
295
+ Begins capturing a Netlify handler execution. Accepts the raw Netlify event object and extracts all request metadata internally (method, path, headers, body, query string parameters, etc.). Patches `globalThis.fetch` and `process.env` to record all outbound network calls and environment variable reads.
306
296
 
307
297
  **Parameters:**
308
- - `requestInfo.method` — HTTP method of the incoming request
309
- - `requestInfo.url` — Request URL/path
310
- - `requestInfo.headers` — Request headers
311
- - `requestInfo.body` — Optional request body
298
+ - `event` — The Netlify handler event object (passed directly)
312
299
 
313
300
  **Returns:** A `RequestContext` object to pass to `finishRequest`.
314
301
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,32 @@
1
+ /**
2
+ * Normalized request metadata stored in the blob.
3
+ * Populated internally by startRequest from the Netlify event.
4
+ */
1
5
  interface RequestInfo {
2
6
  method: string;
3
7
  url: string;
4
8
  headers: Record<string, string>;
5
9
  body?: string;
10
+ queryStringParameters?: Record<string, string | undefined> | null;
11
+ multiValueQueryStringParameters?: Record<string, string[] | undefined> | null;
12
+ rawUrl?: string;
13
+ rawQuery?: string;
14
+ isBase64Encoded?: boolean;
15
+ }
16
+ /**
17
+ * Input type for startRequest. Mirrors the Netlify HandlerEvent shape so
18
+ * handlers can simply pass the event object directly.
19
+ */
20
+ interface NetlifyEvent {
21
+ httpMethod: string;
22
+ path: string;
23
+ headers: Record<string, string | undefined>;
24
+ body?: string | null;
25
+ queryStringParameters?: Record<string, string | undefined> | null;
26
+ multiValueQueryStringParameters?: Record<string, string[] | undefined> | null;
27
+ rawUrl?: string;
28
+ rawQuery?: string;
29
+ isBase64Encoded?: boolean;
6
30
  }
7
31
  interface RequestContext {
8
32
  requestInfo: RequestInfo;
@@ -10,6 +34,9 @@ interface RequestContext {
10
34
  startTime: number;
11
35
  /** Restores original globals (fetch, process.env). Called automatically by finishRequest. */
12
36
  cleanup: () => void;
37
+ /** For v2 Request inputs, a promise that resolves to the request body text.
38
+ * finishRequest awaits this before building the blob so POST bodies are captured. */
39
+ _v2BodyPromise?: Promise<string | undefined>;
13
40
  }
14
41
  interface CapturedData {
15
42
  networkCalls: NetworkCall[];
@@ -30,6 +57,10 @@ interface EnvRead {
30
57
  value: string | undefined;
31
58
  timestamp: number;
32
59
  }
60
+ interface HandlerResponse {
61
+ statusCode: number;
62
+ body?: string;
63
+ }
33
64
  interface BlobData {
34
65
  requestInfo: RequestInfo;
35
66
  capturedData: CapturedData;
@@ -37,6 +68,8 @@ interface BlobData {
37
68
  commitSha: string;
38
69
  startTime: number;
39
70
  endTime: number;
71
+ /** The response returned to the client, used to detect replay mismatches. */
72
+ handlerResponse?: HandlerResponse;
40
73
  }
41
74
  interface FinishRequestCallbacks {
42
75
  /** Uploads serialized captured data and returns the blob URL. */
@@ -92,18 +125,20 @@ interface EnsureRecordingOptions {
92
125
 
93
126
  /**
94
127
  * Called at the beginning of a Netlify handler execution.
95
- * Installs interceptors on globalThis.fetch and process.env to capture
96
- * outbound network calls and environment variable reads made by the handler.
97
- * Returns a request context used by finishRequest.
128
+ * Accepts either a Netlify Functions v1 event (HandlerEvent with httpMethod,
129
+ * path, etc.) or a v2 Web API Request. Extracts all request metadata
130
+ * internally, installs interceptors on globalThis.fetch and process.env to
131
+ * capture outbound network calls and environment variable reads, and returns
132
+ * a request context used by finishRequest.
98
133
  *
99
134
  * When running inside createRequestRecording (replay mode), the replay
100
135
  * interceptors are already installed. In that case we skip installing
101
136
  * capture interceptors to avoid overwriting the replay layer (which would
102
137
  * also crash on Node v16 where Headers/Response don't exist).
103
138
  */
104
- declare function startRequest(requestInfo: RequestInfo): RequestContext;
139
+ declare function startRequest(event: NetlifyEvent | /* v2 Request */ Record<string, unknown>): RequestContext;
105
140
 
106
- interface HandlerResponse {
141
+ interface FullHandlerResponse {
107
142
  statusCode: number;
108
143
  headers?: Record<string, string>;
109
144
  body?: string;
@@ -123,7 +158,7 @@ interface FinishRequestOptions {
123
158
  * uploads it as a JSON blob via the provided callback,
124
159
  * stores the request metadata, and sets the X-Replay-Request-Id header.
125
160
  */
126
- declare function finishRequest(requestContext: RequestContext, callbacks: FinishRequestCallbacks, response: HandlerResponse, options?: FinishRequestOptions): Promise<HandlerResponse>;
161
+ declare function finishRequest(requestContext: RequestContext, callbacks: FinishRequestCallbacks, response: FullHandlerResponse, options?: FinishRequestOptions): Promise<FullHandlerResponse>;
127
162
 
128
163
  /**
129
164
  * Creates `FinishRequestCallbacks` that send captured data to a remote
@@ -181,6 +216,20 @@ declare function ensureRequestRecording(requestId: string, options: EnsureRecord
181
216
  */
182
217
  declare function readInfraConfigFromEnv(): ContainerInfraConfig | undefined;
183
218
 
219
+ interface RecordingResult {
220
+ /** Whether a response mismatch was detected between capture and replay. */
221
+ responseMismatch: boolean;
222
+ /** Details about the mismatch, if any. */
223
+ mismatchDetails?: string;
224
+ /** The response produced during replay. */
225
+ replayResponse?: HandlerResponse;
226
+ /** The original response captured in the blob. */
227
+ capturedResponse?: HandlerResponse;
228
+ /** Whether some recorded network calls were not consumed during replay. */
229
+ unconsumedNetworkCalls: boolean;
230
+ /** Details about unconsumed network calls, if any. */
231
+ unconsumedNetworkDetails?: string;
232
+ }
184
233
  /**
185
234
  * Called from within a container (running under replay-node) to create a Replay recording.
186
235
  * Installs replay-mode interceptors that return pre-recorded responses, then executes
@@ -189,7 +238,7 @@ declare function readInfraConfigFromEnv(): ContainerInfraConfig | undefined;
189
238
  * Accepts either a blob URL (fetched at runtime) or pre-parsed BlobData (avoids needing
190
239
  * globalThis.fetch, which is missing in replay-node's Node v16 environment).
191
240
  */
192
- declare function createRequestRecording(blobUrlOrData: string | BlobData, handlerPath: string, requestInfo: RequestInfo): Promise<void>;
241
+ declare function createRequestRecording(blobUrlOrData: string | BlobData, handlerPath: string, requestInfo: RequestInfo): Promise<RecordingResult>;
193
242
 
194
243
  /**
195
244
  * Options for spawning a recording container from a blob URL.
@@ -227,4 +276,4 @@ interface SpawnRecordingContainerOptions {
227
276
  */
228
277
  declare function spawnRecordingContainer(options: SpawnRecordingContainerOptions): Promise<string>;
229
278
 
230
- export { type BlobData, type CapturedData, type ContainerInfraConfig, type EnsureRecordingOptions, type EnvRead, type FinishRequestCallbacks, type FinishRequestOptions, type NetworkCall, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
279
+ export { type BlobData, type CapturedData, type ContainerInfraConfig, type EnsureRecordingOptions, type EnvRead, type FinishRequestCallbacks, type FinishRequestOptions, type HandlerResponse, type NetlifyEvent, type NetworkCall, type RecordingResult, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
  // src/interceptors/network.ts
9
9
  function installNetworkInterceptor(mode, calls) {
10
10
  const originalFetch = globalThis.fetch;
11
+ let replayCallIndex = 0;
11
12
  if (mode === "capture") {
12
13
  const captureFetch = async (input, init) => {
13
14
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
@@ -39,14 +40,17 @@ function installNetworkInterceptor(mode, calls) {
39
40
  };
40
41
  globalThis.fetch = captureFetch;
41
42
  } else {
42
- let callIndex = 0;
43
43
  const replayFetch = async () => {
44
- const call = calls[callIndex++];
44
+ const idx = replayCallIndex++;
45
+ const call = calls[idx];
45
46
  if (!call) {
46
47
  throw new Error(
47
48
  `No more recorded network calls to replay (exhausted ${calls.length} calls)`
48
49
  );
49
50
  }
51
+ console.log(
52
+ ` [network-replay] Consumed call ${idx + 1}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus}`
53
+ );
50
54
  const body = call.responseBody ?? "";
51
55
  const status = call.responseStatus;
52
56
  return {
@@ -82,6 +86,12 @@ function installNetworkInterceptor(mode, calls) {
82
86
  return {
83
87
  restore() {
84
88
  globalThis.fetch = originalFetch;
89
+ },
90
+ consumedCount() {
91
+ return replayCallIndex;
92
+ },
93
+ totalCount() {
94
+ return calls.length;
85
95
  }
86
96
  };
87
97
  }
@@ -121,7 +131,92 @@ function installEnvironmentInterceptor(mode, reads) {
121
131
  }
122
132
 
123
133
  // src/startRequest.ts
124
- function startRequest(requestInfo) {
134
+ function isWebApiRequest(event) {
135
+ if (!event || typeof event !== "object") return false;
136
+ const obj = event;
137
+ return typeof obj.httpMethod === "undefined" && typeof obj.method === "string" && typeof obj.url === "string";
138
+ }
139
+ function extractFromWebRequest(req) {
140
+ const r = req;
141
+ const headers = {};
142
+ if (r.headers && typeof r.headers.forEach === "function") {
143
+ r.headers.forEach((value, key) => {
144
+ headers[key] = value;
145
+ });
146
+ }
147
+ let pathname = "/";
148
+ let rawQuery = "";
149
+ let queryStringParameters = null;
150
+ try {
151
+ const parsed = new URL(r.url);
152
+ pathname = parsed.pathname;
153
+ rawQuery = parsed.search ? parsed.search.slice(1) : "";
154
+ if (parsed.searchParams) {
155
+ const params = {};
156
+ parsed.searchParams.forEach((v, k) => {
157
+ params[k] = v;
158
+ });
159
+ if (Object.keys(params).length > 0) queryStringParameters = params;
160
+ }
161
+ } catch {
162
+ pathname = r.url;
163
+ }
164
+ const requestInfo = {
165
+ method: r.method,
166
+ url: pathname,
167
+ headers,
168
+ body: void 0,
169
+ // filled asynchronously below
170
+ queryStringParameters,
171
+ multiValueQueryStringParameters: null,
172
+ rawUrl: r.url,
173
+ rawQuery,
174
+ isBase64Encoded: false
175
+ };
176
+ let bodyPromise = Promise.resolve(void 0);
177
+ if (r.body !== null && r.body !== void 0 && typeof r.clone === "function") {
178
+ try {
179
+ const clone = r.clone();
180
+ if (typeof clone.text === "function") {
181
+ bodyPromise = clone.text().then(
182
+ (text) => text || void 0,
183
+ () => void 0
184
+ );
185
+ }
186
+ } catch {
187
+ }
188
+ }
189
+ return { requestInfo, bodyPromise };
190
+ }
191
+ function extractFromV1Event(event) {
192
+ const headers = {};
193
+ for (const [key, value] of Object.entries(event.headers)) {
194
+ if (value !== void 0) {
195
+ headers[key] = value;
196
+ }
197
+ }
198
+ return {
199
+ method: event.httpMethod,
200
+ url: event.path,
201
+ headers,
202
+ body: event.body ?? void 0,
203
+ queryStringParameters: event.queryStringParameters ?? null,
204
+ multiValueQueryStringParameters: event.multiValueQueryStringParameters ?? null,
205
+ rawUrl: event.rawUrl,
206
+ rawQuery: event.rawQuery,
207
+ isBase64Encoded: event.isBase64Encoded
208
+ };
209
+ }
210
+ function startRequest(event) {
211
+ let requestInfo;
212
+ let bodyPromise;
213
+ if (isWebApiRequest(event)) {
214
+ const extracted = extractFromWebRequest(event);
215
+ requestInfo = extracted.requestInfo;
216
+ bodyPromise = extracted.bodyPromise;
217
+ } else {
218
+ requestInfo = extractFromV1Event(event);
219
+ }
125
220
  const capturedData = { networkCalls: [], envReads: [] };
126
221
  const isReplay = globalThis.__REPLAY_RECORDING_MODE__ === true;
127
222
  let cleanup;
@@ -140,7 +235,8 @@ function startRequest(requestInfo) {
140
235
  requestInfo,
141
236
  capturedData,
142
237
  startTime: Date.now(),
143
- cleanup
238
+ cleanup,
239
+ _v2BodyPromise: bodyPromise
144
240
  };
145
241
  }
146
242
 
@@ -291,12 +387,25 @@ async function finishRequest(requestContext, callbacks, response, options) {
291
387
  const commitSha = rawCommitSha;
292
388
  const branchName = rawBranchName;
293
389
  const repositoryUrl = rawRepositoryUrl;
390
+ if (requestContext._v2BodyPromise && !requestContext.requestInfo.body) {
391
+ try {
392
+ const bodyText = await requestContext._v2BodyPromise;
393
+ if (bodyText) {
394
+ requestContext.requestInfo.body = bodyText;
395
+ }
396
+ } catch {
397
+ }
398
+ }
294
399
  const rawBlobData = {
295
400
  requestInfo: requestContext.requestInfo,
296
401
  capturedData: requestContext.capturedData,
297
402
  commitSha,
298
403
  startTime: requestContext.startTime,
299
- endTime: Date.now()
404
+ endTime: Date.now(),
405
+ handlerResponse: {
406
+ statusCode: response.statusCode,
407
+ body: response.body
408
+ }
300
409
  };
301
410
  const blobData = redactBlobData(rawBlobData);
302
411
  const blobContent = JSON.stringify(blobData);
@@ -648,19 +757,241 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
648
757
  };
649
758
  g.File = FileShim;
650
759
  }
760
+ if (typeof g.Headers === "undefined") {
761
+ const HeadersShim = class Headers {
762
+ _map;
763
+ constructor(init) {
764
+ this._map = /* @__PURE__ */ new Map();
765
+ if (init) {
766
+ const entries = Array.isArray(init) ? init : Object.entries(init);
767
+ for (const [k, v] of entries) this._map.set(k.toLowerCase(), String(v));
768
+ }
769
+ }
770
+ get(name) {
771
+ return this._map.get(name.toLowerCase()) ?? null;
772
+ }
773
+ has(name) {
774
+ return this._map.has(name.toLowerCase());
775
+ }
776
+ set(name, value) {
777
+ this._map.set(name.toLowerCase(), String(value));
778
+ }
779
+ delete(name) {
780
+ this._map.delete(name.toLowerCase());
781
+ }
782
+ forEach(cb) {
783
+ this._map.forEach((v, k) => cb(v, k, this));
784
+ }
785
+ entries() {
786
+ return this._map.entries();
787
+ }
788
+ keys() {
789
+ return this._map.keys();
790
+ }
791
+ values() {
792
+ return this._map.values();
793
+ }
794
+ [Symbol.iterator]() {
795
+ return this._map.entries();
796
+ }
797
+ };
798
+ g.Headers = HeadersShim;
799
+ }
800
+ if (typeof g.Request === "undefined") {
801
+ const H = g.Headers;
802
+ const RequestShim = class Request {
803
+ method;
804
+ url;
805
+ headers;
806
+ _body;
807
+ constructor(input, init) {
808
+ this.url = input;
809
+ this.method = String(init?.method ?? "GET");
810
+ this.headers = new H(init?.headers);
811
+ this._body = init?.body ?? null;
812
+ }
813
+ async text() {
814
+ return this._body ?? "";
815
+ }
816
+ async json() {
817
+ return JSON.parse(this._body ?? "null");
818
+ }
819
+ clone() {
820
+ return new RequestShim(this.url, {
821
+ method: this.method,
822
+ headers: Object.fromEntries(this.headers.entries()),
823
+ body: this._body ?? void 0
824
+ });
825
+ }
826
+ };
827
+ g.Request = RequestShim;
828
+ }
829
+ if (typeof g.Response === "undefined") {
830
+ const H = g.Headers;
831
+ const ResponseShim = class Response {
832
+ status;
833
+ statusText;
834
+ headers;
835
+ ok;
836
+ _body;
837
+ constructor(body, init) {
838
+ this._body = body ?? null;
839
+ this.status = init?.status ?? 200;
840
+ this.statusText = init?.statusText ?? "";
841
+ this.headers = new H(init?.headers);
842
+ this.ok = this.status >= 200 && this.status < 300;
843
+ }
844
+ async text() {
845
+ return this._body ?? "";
846
+ }
847
+ async json() {
848
+ return JSON.parse(this._body ?? "null");
849
+ }
850
+ clone() {
851
+ return new ResponseShim(this._body, {
852
+ status: this.status,
853
+ statusText: this.statusText,
854
+ headers: Object.fromEntries(this.headers.entries())
855
+ });
856
+ }
857
+ };
858
+ g.Response = ResponseShim;
859
+ }
860
+ const result = { responseMismatch: false, unconsumedNetworkCalls: false };
651
861
  try {
652
862
  const handlerModule = await import(handlerPath);
653
- await handlerModule.handler({
654
- httpMethod: requestInfo.method,
655
- path: requestInfo.url,
656
- headers: requestInfo.headers,
657
- body: requestInfo.body ?? null
658
- });
863
+ let handler;
864
+ let isV2 = false;
865
+ let current = handlerModule;
866
+ for (let depth = 0; depth < 6 && current && typeof current === "object"; depth++) {
867
+ const obj = current;
868
+ if (typeof obj.handler === "function") {
869
+ handler = obj.handler;
870
+ isV2 = false;
871
+ break;
872
+ }
873
+ if (typeof obj.default === "function") {
874
+ handler = obj.default;
875
+ isV2 = true;
876
+ break;
877
+ }
878
+ if (obj.default && typeof obj.default === "object") {
879
+ current = obj.default;
880
+ continue;
881
+ }
882
+ break;
883
+ }
884
+ if (!handler && typeof handlerModule === "function") {
885
+ handler = handlerModule;
886
+ isV2 = true;
887
+ }
888
+ if (!handler) {
889
+ throw new Error(
890
+ `Could not resolve handler from ${handlerPath}. Module exports: [${Object.keys(handlerModule).join(", ")}]`
891
+ );
892
+ }
893
+ let rawResult;
894
+ if (isV2) {
895
+ const url = requestInfo.rawUrl ?? `https://localhost${requestInfo.url}`;
896
+ const reqInit = {
897
+ method: requestInfo.method,
898
+ headers: requestInfo.headers
899
+ };
900
+ if (requestInfo.body && requestInfo.method !== "GET" && requestInfo.method !== "HEAD") {
901
+ reqInit.body = requestInfo.body;
902
+ }
903
+ const RequestCtor = globalThis.Request;
904
+ if (!RequestCtor) {
905
+ throw new Error(
906
+ "Handler uses Netlify Functions v2 signature but Request is not available in this environment. Ensure undici or a Request polyfill is installed."
907
+ );
908
+ }
909
+ const req = new RequestCtor(url, reqInit);
910
+ const context = { geo: {}, ip: "127.0.0.1", requestId: "replay", server: { region: "local" } };
911
+ const response = await handler(req, context);
912
+ if (response && typeof response === "object" && typeof response.status === "number" && typeof response.text === "function") {
913
+ const res = response;
914
+ rawResult = {
915
+ statusCode: res.status,
916
+ body: await res.text()
917
+ };
918
+ } else {
919
+ rawResult = response;
920
+ }
921
+ } else {
922
+ rawResult = await handler({
923
+ httpMethod: requestInfo.method,
924
+ path: requestInfo.url,
925
+ headers: requestInfo.headers,
926
+ body: requestInfo.body ?? null,
927
+ queryStringParameters: requestInfo.queryStringParameters ?? null,
928
+ multiValueQueryStringParameters: requestInfo.multiValueQueryStringParameters ?? null,
929
+ rawUrl: requestInfo.rawUrl ?? requestInfo.url,
930
+ rawQuery: requestInfo.rawQuery ?? "",
931
+ isBase64Encoded: requestInfo.isBase64Encoded ?? false
932
+ });
933
+ }
934
+ if (blobData.handlerResponse) {
935
+ const replayRes = rawResult;
936
+ const replayResponse = {
937
+ statusCode: replayRes?.statusCode ?? 0,
938
+ body: replayRes?.body
939
+ };
940
+ result.replayResponse = replayResponse;
941
+ result.capturedResponse = blobData.handlerResponse;
942
+ const statusMismatch = replayResponse.statusCode !== blobData.handlerResponse.statusCode;
943
+ const bodyMismatch = replayResponse.body !== blobData.handlerResponse.body;
944
+ if (statusMismatch || bodyMismatch) {
945
+ result.responseMismatch = true;
946
+ const details = [];
947
+ if (statusMismatch) {
948
+ details.push(
949
+ `Status code: captured=${blobData.handlerResponse.statusCode}, replay=${replayResponse.statusCode}`
950
+ );
951
+ }
952
+ if (bodyMismatch) {
953
+ const capturedPreview = (blobData.handlerResponse.body ?? "").slice(0, 200);
954
+ const replayPreview = (replayResponse.body ?? "").slice(0, 200);
955
+ details.push(
956
+ `Body differs: captured=${JSON.stringify(capturedPreview)}, replay=${JSON.stringify(replayPreview)}`
957
+ );
958
+ }
959
+ result.mismatchDetails = details.join("; ");
960
+ console.error(`RESPONSE_MISMATCH: The replay response differs from the captured response.`);
961
+ console.error(` ${result.mismatchDetails}`);
962
+ } else {
963
+ console.log(` [response-check] Replay response matches captured response.`);
964
+ }
965
+ }
659
966
  } finally {
967
+ const consumed = networkHandle.consumedCount();
968
+ const total = networkHandle.totalCount();
969
+ if (consumed < total) {
970
+ result.unconsumedNetworkCalls = true;
971
+ const details = [
972
+ `Consumed ${consumed} of ${total} calls. ${total - consumed} call(s) were never replayed.`
973
+ ];
974
+ console.error(
975
+ `ERROR: Not all recorded network calls were consumed during replay. Consumed ${consumed} of ${total} calls. ${total - consumed} call(s) were never replayed.`
976
+ );
977
+ for (let i = consumed; i < total; i++) {
978
+ const call = blobData.capturedData.networkCalls[i];
979
+ if (call) {
980
+ details.push(`Unconsumed: ${call.method} ${call.url} => ${call.responseStatus}`);
981
+ console.error(
982
+ ` Unconsumed call ${i + 1}/${total}: ${call.method} ${call.url} => ${call.responseStatus}`
983
+ );
984
+ }
985
+ }
986
+ result.unconsumedNetworkDetails = details.join("; ");
987
+ } else {
988
+ console.log(` [network-replay] All ${total} recorded network call(s) were consumed.`);
989
+ }
660
990
  globalThis.__REPLAY_RECORDING_MODE__ = false;
661
991
  networkHandle.restore();
662
992
  envHandle.restore();
663
993
  }
994
+ return result;
664
995
  }
665
996
  export {
666
997
  createRequestRecording,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {