@replayio-app-building/netlify-recorder 0.7.0 → 0.9.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,36 @@ 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
50
 
51
51
  ```typescript
52
52
  import {
53
- startRequest,
54
- finishRequest,
53
+ createRecordingRequestHandler,
55
54
  remoteCallbacks,
56
55
  } from "@replayio-app-building/netlify-recorder";
57
- import type { Handler } from "@netlify/functions";
58
56
 
59
57
  const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
60
58
 
61
- const handler: Handler = async (event) => {
62
- const reqContext = startRequest(event);
63
-
64
- try {
59
+ const handler = createRecordingRequestHandler(
60
+ async (event) => {
65
61
  const result = await myBusinessLogic();
66
62
 
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;
63
+ return {
64
+ statusCode: 200,
65
+ headers: { "Content-Type": "application/json" },
66
+ body: JSON.stringify(result),
67
+ };
68
+ },
69
+ {
70
+ callbacks: remoteCallbacks(RECORDER_URL),
71
+ handlerPath: "netlify/functions/my-handler",
80
72
  }
81
- };
73
+ );
82
74
 
83
75
  export { handler };
84
76
  ```
85
77
 
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.
78
+ `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.
87
79
 
88
80
  ### 2. Create recordings
89
81
 
@@ -142,50 +134,43 @@ Same as Option A — you must set `REPLAY_REPOSITORY_URL`, `COMMIT_SHA`, and `BR
142
134
 
143
135
  ### 2. Wrap your Netlify function
144
136
 
145
- Use `startRequest` / `finishRequest` with custom callbacks that write to your own storage and database:
137
+ Use `createRecordingRequestHandler` with custom callbacks that write to your own storage and database:
146
138
 
147
139
  ```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);
140
+ import { createRecordingRequestHandler } from "@replayio-app-building/netlify-recorder";
153
141
 
154
- try {
142
+ const handler = createRecordingRequestHandler(
143
+ async (event) => {
155
144
  const result = await myBusinessLogic();
156
145
 
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
- },
146
+ return {
147
+ statusCode: 200,
148
+ headers: { "Content-Type": "application/json" },
149
+ body: JSON.stringify(result),
150
+ };
151
+ },
152
+ {
153
+ callbacks: {
154
+ uploadBlob: async (data) => {
155
+ // Upload the JSON string to your blob storage (S3, R2, etc.)
156
+ const res = await fetch("https://storage.example.com/upload", {
157
+ method: "PUT",
158
+ body: data,
159
+ });
160
+ const { url } = await res.json();
161
+ return url;
162
+ },
163
+ storeRequestData: async ({ blobUrl, commitSha, branchName, repositoryUrl, handlerPath }) => {
164
+ const [row] = await sql`
165
+ INSERT INTO requests (blob_url, commit_sha, branch_name, repository_url, handler_path, status)
166
+ VALUES (${blobUrl}, ${commitSha}, ${branchName}, ${repositoryUrl}, ${handlerPath}, 'captured')
167
+ RETURNING id
168
+ `;
169
+ return row.id;
177
170
  },
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;
171
+ },
187
172
  }
188
- };
173
+ );
189
174
 
190
175
  export { handler };
191
176
  ```
@@ -290,9 +275,23 @@ Self-hosted recording requires these environment variables:
290
275
 
291
276
  ## API Reference
292
277
 
278
+ ### `createRecordingRequestHandler(handler, options): Handler`
279
+
280
+ Wraps a Netlify handler function with automatic request recording. This is the recommended way to integrate — it handles `startRequest`/`finishRequest` and error cleanup internally.
281
+
282
+ **Parameters:**
283
+ - `handler` — Your async handler function `(event, context?) => Promise<{ statusCode, headers?, body? }>`
284
+ - `options.callbacks` — Either `remoteCallbacks(serviceUrl)` or custom `{ uploadBlob, storeRequestData }` callbacks
285
+ - `options.handlerPath` — Path to the handler file (used for recording metadata)
286
+ - `options.commitSha` — Override `COMMIT_SHA` env var
287
+ - `options.branchName` — Override `BRANCH_NAME` env var
288
+ - `options.repositoryUrl` — Override `REPLAY_REPOSITORY_URL` env var
289
+
290
+ **Returns:** A wrapped handler function with the same signature.
291
+
293
292
  ### `startRequest(event): RequestContext`
294
293
 
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.
294
+ 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.
296
295
 
297
296
  **Parameters:**
298
297
  - `event` — The Netlify handler event object (passed directly)
@@ -301,7 +300,7 @@ Begins capturing a Netlify handler execution. Accepts the raw Netlify event obje
301
300
 
302
301
  ### `finishRequest(requestContext, callbacks, response, options?): Promise<HandlerResponse>`
303
302
 
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.
303
+ 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.
305
304
 
306
305
  **Requires** the following environment variables (or equivalent options overrides): `COMMIT_SHA`, `BRANCH_NAME`, `REPLAY_REPOSITORY_URL`. Throws if any are missing.
307
306
 
@@ -362,6 +361,6 @@ These must be set on your Netlify site. Your deploy script should resolve them f
362
361
 
363
362
  ## How It Works
364
363
 
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).
364
+ 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
365
 
367
366
  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
@@ -57,7 +57,7 @@ interface EnvRead {
57
57
  value: string | undefined;
58
58
  timestamp: number;
59
59
  }
60
- interface HandlerResponse {
60
+ interface HandlerResponse$1 {
61
61
  statusCode: number;
62
62
  body?: string;
63
63
  }
@@ -69,7 +69,7 @@ interface BlobData {
69
69
  startTime: number;
70
70
  endTime: number;
71
71
  /** The response returned to the client, used to detect replay mismatches. */
72
- handlerResponse?: HandlerResponse;
72
+ handlerResponse?: HandlerResponse$1;
73
73
  }
74
74
  interface FinishRequestCallbacks {
75
75
  /** Uploads serialized captured data and returns the blob URL. */
@@ -160,6 +160,45 @@ interface FinishRequestOptions {
160
160
  */
161
161
  declare function finishRequest(requestContext: RequestContext, callbacks: FinishRequestCallbacks, response: FullHandlerResponse, options?: FinishRequestOptions): Promise<FullHandlerResponse>;
162
162
 
163
+ interface HandlerResponse {
164
+ statusCode: number;
165
+ headers?: Record<string, string>;
166
+ body?: string;
167
+ }
168
+ interface CreateRecordingRequestHandlerOptions extends FinishRequestOptions {
169
+ callbacks: FinishRequestCallbacks;
170
+ }
171
+ /**
172
+ * Wraps a Netlify handler function with request recording.
173
+ *
174
+ * Automatically calls `startRequest` before the handler and `finishRequest`
175
+ * after, capturing all outbound network calls and environment variable reads.
176
+ * On error, interceptors are cleaned up and the error is re-thrown.
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * import { createRecordingRequestHandler, remoteCallbacks } from "@replayio-app-building/netlify-recorder";
181
+ *
182
+ * const handler = createRecordingRequestHandler(
183
+ * async (event) => {
184
+ * const result = await myBusinessLogic();
185
+ * return {
186
+ * statusCode: 200,
187
+ * headers: { "Content-Type": "application/json" },
188
+ * body: JSON.stringify(result),
189
+ * };
190
+ * },
191
+ * {
192
+ * callbacks: remoteCallbacks("https://netlify-recorder-bm4wmw.netlify.app"),
193
+ * handlerPath: "netlify/functions/my-handler",
194
+ * }
195
+ * );
196
+ *
197
+ * export { handler };
198
+ * ```
199
+ */
200
+ declare function createRecordingRequestHandler(handler: (event: NetlifyEvent | Record<string, unknown>, context?: unknown) => Promise<HandlerResponse>, options: CreateRecordingRequestHandlerOptions): (event: NetlifyEvent | Record<string, unknown>, context?: unknown) => Promise<HandlerResponse>;
201
+
163
202
  /**
164
203
  * Creates `FinishRequestCallbacks` that send captured data to a remote
165
204
  * Netlify Recorder service. This removes the need for the consuming app
@@ -222,9 +261,9 @@ interface RecordingResult {
222
261
  /** Details about the mismatch, if any. */
223
262
  mismatchDetails?: string;
224
263
  /** The response produced during replay. */
225
- replayResponse?: HandlerResponse;
264
+ replayResponse?: HandlerResponse$1;
226
265
  /** The original response captured in the blob. */
227
- capturedResponse?: HandlerResponse;
266
+ capturedResponse?: HandlerResponse$1;
228
267
  /** Whether some recorded network calls were not consumed during replay. */
229
268
  unconsumedNetworkCalls: boolean;
230
269
  /** Details about unconsumed network calls, if any. */
@@ -276,4 +315,4 @@ interface SpawnRecordingContainerOptions {
276
315
  */
277
316
  declare function spawnRecordingContainer(options: SpawnRecordingContainerOptions): Promise<string>;
278
317
 
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 };
318
+ 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 NetworkCall, type RecordingResult, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRecordingRequestHandler, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
package/dist/index.js CHANGED
@@ -426,6 +426,20 @@ async function finishRequest(requestContext, callbacks, response, options) {
426
426
  };
427
427
  }
428
428
 
429
+ // src/createRecordingRequestHandler.ts
430
+ function createRecordingRequestHandler(handler, options) {
431
+ return async (event, context) => {
432
+ const reqContext = startRequest(event);
433
+ try {
434
+ const response = await handler(event, context);
435
+ return await finishRequest(reqContext, options.callbacks, response, options);
436
+ } catch (err) {
437
+ reqContext.cleanup();
438
+ throw err;
439
+ }
440
+ };
441
+ }
442
+
429
443
  // src/remoteCallbacks.ts
430
444
  function remoteCallbacks(serviceUrl) {
431
445
  const base = serviceUrl.replace(/\/+$/, "");
@@ -994,6 +1008,7 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
994
1008
  return result;
995
1009
  }
996
1010
  export {
1011
+ createRecordingRequestHandler,
997
1012
  createRequestRecording,
998
1013
  ensureRequestRecording,
999
1014
  finishRequest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {