@replayio-app-building/netlify-recorder 0.8.0 → 0.10.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
@@ -46,44 +46,69 @@ const repositoryUrl = execSync("git remote get-url origin", { encoding: "utf-8"
46
46
 
47
47
  ### 2. Wrap your Netlify function
48
48
 
49
- Use `startRequest` / `finishRequest` with `remoteCallbacks()` to capture request data and send it to the service:
49
+ Use `createRecordingRequestHandler` with `remoteCallbacks()` to wrap your handler with automatic request capture.
50
+
51
+ **v1 handler** (Netlify Functions v1 — `event` with `httpMethod`, `path`, etc.):
50
52
 
51
53
  ```typescript
52
54
  import {
53
- startRequest,
54
- finishRequest,
55
+ createRecordingRequestHandler,
55
56
  remoteCallbacks,
56
57
  } from "@replayio-app-building/netlify-recorder";
57
- import type { Handler } from "@netlify/functions";
58
58
 
59
59
  const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
60
60
 
61
- const handler: Handler = async (event) => {
62
- const reqContext = startRequest(event);
63
-
64
- try {
61
+ const handler = createRecordingRequestHandler(
62
+ async (event) => {
65
63
  const result = await myBusinessLogic();
66
64
 
67
- return await finishRequest(
68
- reqContext,
69
- remoteCallbacks(RECORDER_URL),
70
- {
71
- statusCode: 200,
72
- headers: { "Content-Type": "application/json" },
73
- body: JSON.stringify(result),
74
- },
75
- { handlerPath: "netlify/functions/my-handler" }
76
- );
77
- } catch (err) {
78
- reqContext.cleanup();
79
- throw err;
65
+ return {
66
+ statusCode: 200,
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify(result),
69
+ };
70
+ },
71
+ {
72
+ callbacks: remoteCallbacks(RECORDER_URL),
73
+ handlerPath: "netlify/functions/my-handler",
80
74
  }
81
- };
75
+ );
82
76
 
83
77
  export { handler };
84
78
  ```
85
79
 
86
- The `remoteCallbacks(url)` helper sends the captured blob data including the repository URL, branch, and commit — to the Netlify Recorder service's `/api/store-request` endpoint, which uploads it to blob storage and stores the request metadata. The response includes an `X-Replay-Request-Id` header with the request ID managed by the service.
80
+ **v2 handler** (Netlify Functions v2Web API `Request`):
81
+
82
+ ```typescript
83
+ import {
84
+ createRecordingRequestHandler,
85
+ remoteCallbacks,
86
+ } from "@replayio-app-building/netlify-recorder";
87
+
88
+ const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
89
+
90
+ // The wrapper reads the body from a clone — you can still read req.json() etc.
91
+ export default createRecordingRequestHandler(
92
+ async (req) => {
93
+ const body = await (req as Request).json();
94
+ const result = await myBusinessLogic(body);
95
+
96
+ return {
97
+ statusCode: 200,
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify(result),
100
+ };
101
+ },
102
+ {
103
+ callbacks: remoteCallbacks(RECORDER_URL),
104
+ handlerPath: "netlify/functions/my-handler",
105
+ }
106
+ );
107
+ ```
108
+
109
+ `createRecordingRequestHandler` automatically captures all outbound network calls and environment variable reads during your handler's execution, then sends the captured data to the Netlify Recorder service. The response includes an `X-Replay-Request-Id` header with the request ID managed by the service.
110
+
111
+ > **Note:** Always use the response returned by the wrapper (or `finishRequest`), not your original response object. The wrapper adds the `X-Replay-Request-Id` header to the response it returns.
87
112
 
88
113
  ### 2. Create recordings
89
114
 
@@ -142,50 +167,43 @@ Same as Option A — you must set `REPLAY_REPOSITORY_URL`, `COMMIT_SHA`, and `BR
142
167
 
143
168
  ### 2. Wrap your Netlify function
144
169
 
145
- Use `startRequest` / `finishRequest` with custom callbacks that write to your own storage and database:
170
+ Use `createRecordingRequestHandler` with custom callbacks that write to your own storage and database:
146
171
 
147
172
  ```typescript
148
- import { startRequest, finishRequest } from "@replayio-app-building/netlify-recorder";
149
- import type { Handler } from "@netlify/functions";
150
-
151
- const handler: Handler = async (event) => {
152
- const reqContext = startRequest(event);
173
+ import { createRecordingRequestHandler } from "@replayio-app-building/netlify-recorder";
153
174
 
154
- try {
175
+ const handler = createRecordingRequestHandler(
176
+ async (event) => {
155
177
  const result = await myBusinessLogic();
156
178
 
157
- return await finishRequest(
158
- reqContext,
159
- {
160
- uploadBlob: async (data) => {
161
- // Upload the JSON string to your blob storage (S3, R2, etc.)
162
- const res = await originalFetch("https://storage.example.com/upload", {
163
- method: "PUT",
164
- body: data,
165
- });
166
- const { url } = await res.json();
167
- return url;
168
- },
169
- storeRequestData: async ({ blobUrl, commitSha, branchName, repositoryUrl, handlerPath }) => {
170
- const [row] = await sql`
171
- INSERT INTO requests (blob_url, commit_sha, branch_name, repository_url, handler_path, status)
172
- VALUES (${blobUrl}, ${commitSha}, ${branchName}, ${repositoryUrl}, ${handlerPath}, 'captured')
173
- RETURNING id
174
- `;
175
- return row.id;
176
- },
179
+ return {
180
+ statusCode: 200,
181
+ headers: { "Content-Type": "application/json" },
182
+ body: JSON.stringify(result),
183
+ };
184
+ },
185
+ {
186
+ callbacks: {
187
+ uploadBlob: async (data) => {
188
+ // Upload the JSON string to your blob storage (S3, R2, etc.)
189
+ const res = await fetch("https://storage.example.com/upload", {
190
+ method: "PUT",
191
+ body: data,
192
+ });
193
+ const { url } = await res.json();
194
+ return url;
195
+ },
196
+ storeRequestData: async ({ blobUrl, commitSha, branchName, repositoryUrl, handlerPath }) => {
197
+ const [row] = await sql`
198
+ INSERT INTO requests (blob_url, commit_sha, branch_name, repository_url, handler_path, status)
199
+ VALUES (${blobUrl}, ${commitSha}, ${branchName}, ${repositoryUrl}, ${handlerPath}, 'captured')
200
+ RETURNING id
201
+ `;
202
+ return row.id;
177
203
  },
178
- {
179
- statusCode: 200,
180
- headers: { "Content-Type": "application/json" },
181
- body: JSON.stringify(result),
182
- }
183
- );
184
- } catch (err) {
185
- reqContext.cleanup();
186
- throw err;
204
+ },
187
205
  }
188
- };
206
+ );
189
207
 
190
208
  export { handler };
191
209
  ```
@@ -290,18 +308,40 @@ Self-hosted recording requires these environment variables:
290
308
 
291
309
  ## API Reference
292
310
 
311
+ ### `createRecordingRequestHandler(handler, options): Handler`
312
+
313
+ Wraps a Netlify handler function with automatic request recording. This is the recommended way to integrate — it handles `startRequest`/`finishRequest` and error cleanup internally.
314
+
315
+ **Parameters:**
316
+ - `handler` — Your async handler function `(event, context?) => Promise<{ statusCode, headers?, body? }>`
317
+ - `options.callbacks` — Either `remoteCallbacks(serviceUrl)` or custom `{ uploadBlob, storeRequestData }` callbacks
318
+ - `options.handlerPath` — Path to the handler file (used for recording metadata)
319
+ - `options.commitSha` — Override `COMMIT_SHA` env var
320
+ - `options.branchName` — Override `BRANCH_NAME` env var
321
+ - `options.repositoryUrl` — Override `REPLAY_REPOSITORY_URL` env var
322
+
323
+ **Returns:** A wrapped handler function with the same signature.
324
+
293
325
  ### `startRequest(event): RequestContext`
294
326
 
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.
327
+ Lower-level API. Begins capturing a Netlify handler execution. Patches `globalThis.fetch` and `process.env` to record all outbound network calls and environment variable reads. Use this with `finishRequest` when you need more control than `createRecordingRequestHandler` provides.
328
+
329
+ Accepts either a **v1** `NetlifyEvent` (has `httpMethod`) or a **v2** Web API `Request` (has `method` + `url`). The version is detected automatically.
330
+
331
+ For v2 Request inputs, the body is read from a **clone** — the original request body remains consumable by your handler.
296
332
 
297
333
  **Parameters:**
298
- - `event` — The Netlify handler event object (passed directly)
334
+ - `event` — A v1 `NetlifyEvent` or v2 `Request` object (passed directly — no need to clone)
299
335
 
300
336
  **Returns:** A `RequestContext` object to pass to `finishRequest`.
301
337
 
302
338
  ### `finishRequest(requestContext, callbacks, response, options?): Promise<HandlerResponse>`
303
339
 
304
- Finalizes the request capture. Restores original `fetch` and `process.env`, serializes the captured data, uploads it via the callbacks, and returns the response with `X-Replay-Request-Id` header set.
340
+ Lower-level API. Finalizes the request capture. Restores original `fetch` and `process.env`, serializes the captured data, uploads it via the callbacks, and returns the response with `X-Replay-Request-Id` header set.
341
+
342
+ **Important:** You must send the returned response to the client — it contains the `X-Replay-Request-Id` header.
343
+
344
+ Logs a `console.warn` when the total duration exceeds 2 seconds or when individual callback steps (upload, store) exceed 1 second, to help diagnose slow operations.
305
345
 
306
346
  **Requires** the following environment variables (or equivalent options overrides): `COMMIT_SHA`, `BRANCH_NAME`, `REPLAY_REPOSITORY_URL`. Throws if any are missing.
307
347
 
@@ -362,6 +402,6 @@ These must be set on your Netlify site. Your deploy script should resolve them f
362
402
 
363
403
  ## How It Works
364
404
 
365
- 1. **Capture phase** (`startRequest` / `finishRequest`): When a Netlify function handles a request, `startRequest` patches `globalThis.fetch` and `process.env` with Proxies that record every outbound network call and environment variable read. When the handler completes, `finishRequest` restores the originals, serializes the captured data to JSON, and sends it to either the remote service (via `remoteCallbacks`) or your own storage (via custom callbacks).
405
+ 1. **Capture phase** (`createRecordingRequestHandler` or `startRequest` / `finishRequest`): When a Netlify function handles a request, the recording layer patches `globalThis.fetch` and `process.env` with Proxies that record every outbound network call and environment variable read. When the handler completes, the originals are restored, the captured data is serialized to JSON, and sent to either the remote service (via `remoteCallbacks`) or your own storage (via custom callbacks).
366
406
 
367
407
  2. **Recording phase**: The captured blob is sent to a recording container (either via the Netlify Recorder service or self-hosted). Inside the container, `createRequestRecording` downloads the blob, installs replay-mode interceptors that return the pre-recorded responses, and re-executes the handler under `replay-node`. Since replay-node records all execution, this produces a Replay recording of the exact same handler execution.
package/dist/index.d.ts CHANGED
@@ -28,6 +28,27 @@ interface NetlifyEvent {
28
28
  rawQuery?: string;
29
29
  isBase64Encoded?: boolean;
30
30
  }
31
+ /**
32
+ * Minimal interface for a Netlify Functions v2 Web API Request.
33
+ *
34
+ * This is a subset of the standard `Request` interface — only the properties
35
+ * that `startRequest` actually reads are required. Any spec-compliant
36
+ * `Request` object (including `new Request(...)`) satisfies this interface.
37
+ *
38
+ * `startRequest` reads the body from a **clone**, so the original request
39
+ * remains consumable by your handler. You do **not** need to clone the
40
+ * request before passing it in.
41
+ */
42
+ interface NetlifyV2Request {
43
+ readonly method: string;
44
+ readonly url: string;
45
+ readonly headers: Headers | {
46
+ forEach(cb: (value: string, key: string) => void): void;
47
+ };
48
+ readonly body: ReadableStream<Uint8Array> | null;
49
+ clone(): NetlifyV2Request;
50
+ text(): Promise<string>;
51
+ }
31
52
  interface RequestContext {
32
53
  requestInfo: RequestInfo;
33
54
  capturedData: CapturedData;
@@ -57,7 +78,7 @@ interface EnvRead {
57
78
  value: string | undefined;
58
79
  timestamp: number;
59
80
  }
60
- interface HandlerResponse {
81
+ interface HandlerResponse$1 {
61
82
  statusCode: number;
62
83
  body?: string;
63
84
  }
@@ -69,7 +90,7 @@ interface BlobData {
69
90
  startTime: number;
70
91
  endTime: number;
71
92
  /** The response returned to the client, used to detect replay mismatches. */
72
- handlerResponse?: HandlerResponse;
93
+ handlerResponse?: HandlerResponse$1;
73
94
  }
74
95
  interface FinishRequestCallbacks {
75
96
  /** Uploads serialized captured data and returns the blob URL. */
@@ -125,18 +146,46 @@ interface EnsureRecordingOptions {
125
146
 
126
147
  /**
127
148
  * Called at the beginning of a Netlify handler execution.
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.
133
149
  *
134
- * When running inside createRequestRecording (replay mode), the replay
150
+ * Accepts either a **Netlify Functions v1** event (`NetlifyEvent` has
151
+ * `httpMethod`, `path`, etc.) or a **v2 Web API `Request`** object
152
+ * (`NetlifyV2Request`). The version is detected automatically: v1 events
153
+ * have an `httpMethod` property while v2 Request objects have `method` +
154
+ * `url`.
155
+ *
156
+ * **v2 body handling:** For v2 Request inputs the body is read from a
157
+ * **clone** of the request, so the original `Request` body remains
158
+ * consumable by your handler. You do **not** need to call `req.clone()`
159
+ * before passing it in. The body text is resolved asynchronously and
160
+ * captured when `finishRequest` is called.
161
+ *
162
+ * Installs interceptors on `globalThis.fetch` and `process.env` to capture
163
+ * outbound network calls and environment variable reads, and returns a
164
+ * request context that must be passed to `finishRequest`.
165
+ *
166
+ * When running inside `createRequestRecording` (replay mode), the replay
135
167
  * interceptors are already installed. In that case we skip installing
136
- * capture interceptors to avoid overwriting the replay layer (which would
137
- * also crash on Node v16 where Headers/Response don't exist).
168
+ * capture interceptors to avoid overwriting the replay layer.
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * // v1 handler
173
+ * export async function handler(event: NetlifyEvent) {
174
+ * const ctx = startRequest(event);
175
+ * // ... your logic ...
176
+ * return finishRequest(ctx, callbacks, response);
177
+ * }
178
+ *
179
+ * // v2 handler — no need to clone the request
180
+ * export default async function handler(req: Request) {
181
+ * const ctx = startRequest(req);
182
+ * const body = await req.json(); // still works — body was NOT consumed
183
+ * // ... your logic ...
184
+ * return finishRequest(ctx, callbacks, response);
185
+ * }
186
+ * ```
138
187
  */
139
- declare function startRequest(event: NetlifyEvent | /* v2 Request */ Record<string, unknown>): RequestContext;
188
+ declare function startRequest(event: NetlifyEvent | NetlifyV2Request): RequestContext;
140
189
 
141
190
  interface FullHandlerResponse {
142
191
  statusCode: number;
@@ -157,9 +206,85 @@ interface FinishRequestOptions {
157
206
  * Restores original globals, serializes all captured data,
158
207
  * uploads it as a JSON blob via the provided callback,
159
208
  * stores the request metadata, and sets the X-Replay-Request-Id header.
209
+ *
210
+ * **Important:** The returned response includes the `X-Replay-Request-Id`
211
+ * header. You must send the returned response to the client — not the
212
+ * original response object you passed in.
213
+ *
214
+ * Logs a warning to `console.warn` when the total finishRequest time or
215
+ * individual callback steps exceed their thresholds, to help diagnose
216
+ * slow blob uploads or database writes.
160
217
  */
161
218
  declare function finishRequest(requestContext: RequestContext, callbacks: FinishRequestCallbacks, response: FullHandlerResponse, options?: FinishRequestOptions): Promise<FullHandlerResponse>;
162
219
 
220
+ interface HandlerResponse {
221
+ statusCode: number;
222
+ headers?: Record<string, string>;
223
+ body?: string;
224
+ }
225
+ interface CreateRecordingRequestHandlerOptions extends FinishRequestOptions {
226
+ callbacks: FinishRequestCallbacks;
227
+ }
228
+ /**
229
+ * Wraps a Netlify handler function with request recording.
230
+ *
231
+ * Automatically calls `startRequest` before the handler and `finishRequest`
232
+ * after, capturing all outbound network calls and environment variable reads.
233
+ * On error, interceptors are cleaned up and the error is re-thrown.
234
+ *
235
+ * **Important:** You must use the response returned by the wrapped handler
236
+ * (not your original response object), because it includes the
237
+ * `X-Replay-Request-Id` header set by `finishRequest`.
238
+ *
239
+ * For v2 handlers the request body is read from a clone internally — your
240
+ * handler still receives the original request with an unconsumed body.
241
+ *
242
+ * @example v1 handler (NetlifyEvent)
243
+ * ```typescript
244
+ * import { createRecordingRequestHandler, remoteCallbacks } from "@replayio-app-building/netlify-recorder";
245
+ *
246
+ * const handler = createRecordingRequestHandler(
247
+ * async (event) => {
248
+ * const result = await myBusinessLogic(event.body);
249
+ * return {
250
+ * statusCode: 200,
251
+ * headers: { "Content-Type": "application/json" },
252
+ * body: JSON.stringify(result),
253
+ * };
254
+ * },
255
+ * {
256
+ * callbacks: remoteCallbacks("https://netlify-recorder-bm4wmw.netlify.app"),
257
+ * handlerPath: "netlify/functions/my-handler",
258
+ * }
259
+ * );
260
+ *
261
+ * export { handler };
262
+ * ```
263
+ *
264
+ * @example v2 handler (Web API Request)
265
+ * ```typescript
266
+ * import { createRecordingRequestHandler, remoteCallbacks } from "@replayio-app-building/netlify-recorder";
267
+ *
268
+ * export default createRecordingRequestHandler(
269
+ * async (req) => {
270
+ * // Body is still available — startRequest reads from a clone
271
+ * const body = await (req as Request).json();
272
+ * const result = await myBusinessLogic(body);
273
+ * return {
274
+ * statusCode: 200,
275
+ * headers: { "Content-Type": "application/json" },
276
+ * body: JSON.stringify(result),
277
+ * };
278
+ * },
279
+ * {
280
+ * callbacks: remoteCallbacks("https://netlify-recorder-bm4wmw.netlify.app"),
281
+ * handlerPath: "netlify/functions/my-handler",
282
+ * }
283
+ * );
284
+ * ```
285
+ */
286
+ declare function createRecordingRequestHandler(handler: (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>, options: CreateRecordingRequestHandlerOptions): (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>;
287
+
163
288
  /**
164
289
  * Creates `FinishRequestCallbacks` that send captured data to a remote
165
290
  * Netlify Recorder service. This removes the need for the consuming app
@@ -222,9 +347,9 @@ interface RecordingResult {
222
347
  /** Details about the mismatch, if any. */
223
348
  mismatchDetails?: string;
224
349
  /** The response produced during replay. */
225
- replayResponse?: HandlerResponse;
350
+ replayResponse?: HandlerResponse$1;
226
351
  /** The original response captured in the blob. */
227
- capturedResponse?: HandlerResponse;
352
+ capturedResponse?: HandlerResponse$1;
228
353
  /** Whether some recorded network calls were not consumed during replay. */
229
354
  unconsumedNetworkCalls: boolean;
230
355
  /** Details about unconsumed network calls, if any. */
@@ -276,4 +401,4 @@ interface SpawnRecordingContainerOptions {
276
401
  */
277
402
  declare function spawnRecordingContainer(options: SpawnRecordingContainerOptions): Promise<string>;
278
403
 
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 };
404
+ export { type BlobData, type CapturedData, type ContainerInfraConfig, type CreateRecordingRequestHandlerOptions, type EnsureRecordingOptions, type EnvRead, type FinishRequestCallbacks, type FinishRequestOptions, type HandlerResponse$1 as HandlerResponse, type NetlifyEvent, type NetlifyV2Request, type NetworkCall, type RecordingResult, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRecordingRequestHandler, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
package/dist/index.js CHANGED
@@ -367,7 +367,10 @@ function redactBlobData(blobData) {
367
367
  }
368
368
 
369
369
  // src/finishRequest.ts
370
+ var SLOW_THRESHOLD_MS = 2e3;
371
+ var SLOW_STEP_THRESHOLD_MS = 1e3;
370
372
  async function finishRequest(requestContext, callbacks, response, options) {
373
+ const finishStart = Date.now();
371
374
  requestContext.cleanup();
372
375
  if (globalThis.__REPLAY_RECORDING_MODE__ === true) {
373
376
  return response;
@@ -387,6 +390,7 @@ async function finishRequest(requestContext, callbacks, response, options) {
387
390
  const commitSha = rawCommitSha;
388
391
  const branchName = rawBranchName;
389
392
  const repositoryUrl = rawRepositoryUrl;
393
+ const handlerPath = options?.handlerPath ?? "unknown";
390
394
  if (requestContext._v2BodyPromise && !requestContext.requestInfo.body) {
391
395
  try {
392
396
  const bodyText = await requestContext._v2BodyPromise;
@@ -409,14 +413,34 @@ async function finishRequest(requestContext, callbacks, response, options) {
409
413
  };
410
414
  const blobData = redactBlobData(rawBlobData);
411
415
  const blobContent = JSON.stringify(blobData);
416
+ const uploadStart = Date.now();
412
417
  const blobUrl = await callbacks.uploadBlob(blobContent);
418
+ const uploadDuration = Date.now() - uploadStart;
419
+ if (uploadDuration > SLOW_STEP_THRESHOLD_MS) {
420
+ console.warn(
421
+ `netlify-recorder: uploadBlob took ${uploadDuration}ms (handler: ${handlerPath})`
422
+ );
423
+ }
424
+ const storeStart = Date.now();
413
425
  const storedRequestId = await callbacks.storeRequestData({
414
426
  blobUrl,
415
427
  commitSha,
416
428
  branchName,
417
429
  repositoryUrl,
418
- handlerPath: options?.handlerPath ?? "unknown"
430
+ handlerPath
419
431
  });
432
+ const storeDuration = Date.now() - storeStart;
433
+ if (storeDuration > SLOW_STEP_THRESHOLD_MS) {
434
+ console.warn(
435
+ `netlify-recorder: storeRequestData took ${storeDuration}ms (handler: ${handlerPath})`
436
+ );
437
+ }
438
+ const totalDuration = Date.now() - finishStart;
439
+ if (totalDuration > SLOW_THRESHOLD_MS) {
440
+ console.warn(
441
+ `netlify-recorder: finishRequest took ${totalDuration}ms total (upload: ${uploadDuration}ms, store: ${storeDuration}ms, handler: ${handlerPath})`
442
+ );
443
+ }
420
444
  return {
421
445
  ...response,
422
446
  headers: {
@@ -426,6 +450,20 @@ async function finishRequest(requestContext, callbacks, response, options) {
426
450
  };
427
451
  }
428
452
 
453
+ // src/createRecordingRequestHandler.ts
454
+ function createRecordingRequestHandler(handler, options) {
455
+ return async (event, context) => {
456
+ const reqContext = startRequest(event);
457
+ try {
458
+ const response = await handler(event, context);
459
+ return await finishRequest(reqContext, options.callbacks, response, options);
460
+ } catch (err) {
461
+ reqContext.cleanup();
462
+ throw err;
463
+ }
464
+ };
465
+ }
466
+
429
467
  // src/remoteCallbacks.ts
430
468
  function remoteCallbacks(serviceUrl) {
431
469
  const base = serviceUrl.replace(/\/+$/, "");
@@ -994,6 +1032,7 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
994
1032
  return result;
995
1033
  }
996
1034
  export {
1035
+ createRecordingRequestHandler,
997
1036
  createRequestRecording,
998
1037
  ensureRequestRecording,
999
1038
  finishRequest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {