@replayio-app-building/netlify-recorder 0.17.0 → 0.19.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.
Files changed (4) hide show
  1. package/README.md +214 -130
  2. package/dist/index.d.ts +69 -180
  3. package/dist/index.js +170 -300
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @replayio-app-building/netlify-recorder
2
2
 
3
- Capture and replay Netlify function executions as [Replay](https://replay.io) recordings. This package intercepts outbound network calls and environment variable reads during handler execution, stores the captured data via the Netlify Recorder service, and can later reproduce the exact execution as a Replay recording for debugging and analysis.
3
+ Capture and replay Netlify function executions as [Replay](https://replay.io) recordings. This package intercepts outbound network calls and environment variable reads during handler execution, stores the captured data in your app's own database, and can later reproduce the exact execution as a Replay recording for debugging and analysis.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,7 +10,19 @@ npm install @replayio-app-building/netlify-recorder
10
10
 
11
11
  ## Setup
12
12
 
13
- ### 1. Set required environment variables
13
+ ### 1. Create the backend_requests table
14
+
15
+ The package stores captured request data directly in your app's database. Call `backendRequestsEnsureTable` once during schema initialization to create the `backend_requests` table:
16
+
17
+ ```typescript
18
+ import { backendRequestsEnsureTable } from "@replayio-app-building/netlify-recorder";
19
+
20
+ await backendRequestsEnsureTable(sql);
21
+ ```
22
+
23
+ This creates a table with columns for the serialized blob data, git metadata (commit SHA, branch, repository URL), handler path, recording status, and timestamps.
24
+
25
+ ### 2. Set required environment variables
14
26
 
15
27
  `finishRequest` needs to know which repository, branch, and commit your deployed code belongs to. Set these environment variables on your Netlify site:
16
28
 
@@ -19,9 +31,8 @@ npm install @replayio-app-building/netlify-recorder
19
31
  | `REPLAY_REPOSITORY_URL` | Your app's git repository URL (e.g. `https://github.com/org/repo.git`) | Set in your deploy script or Netlify site settings |
20
32
  | `COMMIT_SHA` | The git commit hash of the deployed code | Set in your deploy script via `git rev-parse HEAD` |
21
33
  | `BRANCH_NAME` | The git branch of the deployed code | Set in your deploy script via `git rev-parse --abbrev-ref HEAD` |
22
- | `NETLIFY_RECORDER_SECRET` | Secret string for access control — restricts who can view and act on your captured requests | Set in Netlify site environment variables or via `set-branch-secret` |
23
34
 
24
- The first three are **required** — `finishRequest` will throw an error if any are missing. `NETLIFY_RECORDER_SECRET` is strongly recommended to prevent other apps from accessing your captured request data. Your deploy script should resolve the git values and set them on the Netlify site before deploying. Example:
35
+ All three are **required** — `finishRequest` will throw an error if any are missing. Your deploy script should resolve the git values and set them on the Netlify site before deploying. Example:
25
36
 
26
37
  ```typescript
27
38
  // In your deploy script:
@@ -33,19 +44,20 @@ const repositoryUrl = execSync("git remote get-url origin", { encoding: "utf-8"
33
44
  // Set these on your Netlify site via the Netlify API or CLI
34
45
  ```
35
46
 
36
- ### 2. Wrap your Netlify function
47
+ ### 3. Wrap your Netlify function
37
48
 
38
- Use `createRecordingRequestHandler` with `remoteCallbacks()` to wrap your handler with automatic request capture. Set `secret` to restrict access to captured requests only API calls providing the same secret can view or act on them.
49
+ Use `createRecordingRequestHandler` with `databaseCallbacks(sql)` to wrap your handler with automatic request capture. The captured data is stored directly in the `backend_requests` table in your database.
39
50
 
40
51
  **v1 handler** (Netlify Functions v1 — `event` with `httpMethod`, `path`, etc.):
41
52
 
42
53
  ```typescript
43
54
  import {
44
55
  createRecordingRequestHandler,
45
- remoteCallbacks,
56
+ databaseCallbacks,
46
57
  } from "@replayio-app-building/netlify-recorder";
58
+ import { neon } from "@neondatabase/serverless";
47
59
 
48
- const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
60
+ const sql = neon(process.env.DATABASE_URL!);
49
61
 
50
62
  const handler = createRecordingRequestHandler(
51
63
  async (event) => {
@@ -58,9 +70,8 @@ const handler = createRecordingRequestHandler(
58
70
  };
59
71
  },
60
72
  {
61
- callbacks: remoteCallbacks(RECORDER_URL),
73
+ callbacks: databaseCallbacks(sql),
62
74
  handlerPath: "netlify/functions/my-handler",
63
- secret: process.env.NETLIFY_RECORDER_SECRET,
64
75
  }
65
76
  );
66
77
 
@@ -72,10 +83,11 @@ export { handler };
72
83
  ```typescript
73
84
  import {
74
85
  createRecordingRequestHandler,
75
- remoteCallbacks,
86
+ databaseCallbacks,
76
87
  } from "@replayio-app-building/netlify-recorder";
88
+ import { neon } from "@neondatabase/serverless";
77
89
 
78
- const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
90
+ const sql = neon(process.env.DATABASE_URL!);
79
91
 
80
92
  // The wrapper reads the body from a clone — you can still read req.json() etc.
81
93
  export default createRecordingRequestHandler(
@@ -90,140 +102,88 @@ export default createRecordingRequestHandler(
90
102
  };
91
103
  },
92
104
  {
93
- callbacks: remoteCallbacks(RECORDER_URL),
105
+ callbacks: databaseCallbacks(sql),
94
106
  handlerPath: "netlify/functions/my-handler",
95
- secret: process.env.NETLIFY_RECORDER_SECRET,
96
107
  }
97
108
  );
98
109
  ```
99
110
 
100
- `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.
111
+ `createRecordingRequestHandler` automatically captures all outbound network calls and environment variable reads during your handler's execution, then stores the captured data in the `backend_requests` table. The response includes an `X-Replay-Request-Id` header with the ID of the stored request.
101
112
 
102
113
  > **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.
103
114
 
104
- ### 3. Create recordings
115
+ ### 4. Create recordings via the Netlify Recorder service
105
116
 
106
- When you want to turn a captured request into a Replay recording, POST to the Netlify Recorder service's `create-recording` endpoint with the request ID. If the request was created with a secret, you must include it:
117
+ Use `ensureRequestRecording` to turn a captured request into a Replay recording. It checks the `backend_requests` table first — if a recording already exists, it returns the recording ID immediately without calling the service. Otherwise it dispatches the work to the Netlify Recorder service and updates the row status to `"queued"`.
107
118
 
108
119
  ```typescript
109
- const response = await fetch(
110
- `${RECORDER_URL}/api/create-recording`,
111
- {
112
- method: "POST",
113
- headers: { "Content-Type": "application/json" },
114
- body: JSON.stringify({
115
- requestId,
116
- secret,
117
- webhookUrl: "https://your-app.netlify.app/api/on-recording-complete", // optional
118
- }),
119
- }
120
- );
121
- ```
120
+ import { ensureRequestRecording } from "@replayio-app-building/netlify-recorder";
122
121
 
123
- The service looks up the stored blob data, dispatches the work to a recording container, and creates the recording.
122
+ const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
124
123
 
125
- If `webhookUrl` is provided, the service will POST the result to that URL when the recording completes (or fails):
124
+ const recordingId = await ensureRequestRecording(sql, requestId, {
125
+ recorderUrl: RECORDER_URL,
126
+ });
126
127
 
127
- On success:
128
- ```json
129
- { "status": "recorded", "recordingId": "a1b2c3d4-..." }
130
- ```
131
-
132
- On failure:
133
- ```json
134
- { "status": "failed", "error": "Error message" }
128
+ if (recordingId) {
129
+ console.log(`Recording already exists: ${recordingId}`);
130
+ } else {
131
+ console.log("Recording queued — check back later or use a webhook");
132
+ }
135
133
  ```
136
134
 
137
- ### 4. Check recording status
135
+ The function is idempotent — calling it multiple times for the same request is safe:
136
+ - **`status: "recorded"`** — returns the `recording_id` immediately
137
+ - **`status: "queued"` or `"processing"`** — returns `null` without re-queuing
138
+ - **`status: "captured"` or `"failed"`** — calls the service, updates status to `"queued"`, returns `null`
138
139
 
139
- You can poll the recording status at any time. If the request was created with a secret, you must include it:
140
+ By default, the blob data URL points to `${recorderUrl}/api/get-backend-request-blob?requestId=${requestId}`. If your app serves blob data from a different endpoint, pass a custom `blobDataUrl`:
140
141
 
141
142
  ```typescript
142
- const res = await fetch(
143
- `${RECORDER_URL}/api/get-request?requestId=${requestId}&secret=${secret}`
144
- );
145
- const { status, recordingId } = await res.json();
146
- // status: "captured" | "processing" | "recorded" | "failed"
147
- // recordingId: string | null
143
+ const recordingId = await ensureRequestRecording(sql, requestId, {
144
+ recorderUrl: RECORDER_URL,
145
+ blobDataUrl: `https://your-app.netlify.app/api/get-blob?id=${requestId}`,
146
+ webhookUrl: "https://your-app.netlify.app/api/on-recording-complete", // optional
147
+ });
148
148
  ```
149
149
 
150
- Requests created with a secret return 403 if the secret is missing or incorrect.
151
-
152
- ### 5. Access control with secrets
153
-
154
- You can restrict access to captured requests by setting a `secret` string. When a secret is set, the request is only accessible via API calls that provide the same secret value. This lets each app isolate its requests from other apps sharing the same Netlify Recorder service.
155
-
156
- #### Setting a secret
150
+ When `webhookUrl` is provided, the service POSTs the result when the recording completes:
157
151
 
158
- Pass `secret` in the options when wrapping your handler:
159
-
160
- ```typescript
161
- export default createRecordingRequestHandler(
162
- async (req) => {
163
- const result = await myBusinessLogic();
164
- return { statusCode: 200, body: JSON.stringify(result) };
165
- },
166
- {
167
- callbacks: remoteCallbacks(RECORDER_URL),
168
- handlerPath: "netlify/functions/my-handler",
169
- secret: process.env.NETLIFY_RECORDER_SECRET,
170
- }
171
- );
152
+ On success:
153
+ ```json
154
+ { "status": "recorded", "recordingId": "a1b2c3d4-..." }
172
155
  ```
173
156
 
174
- #### Listing requests by secret
175
-
176
- Use the `requests` endpoint with a `secret` parameter to retrieve all requests associated with your secret, with optional filtering by status, handler path, and time range:
177
-
178
- ```typescript
179
- const res = await fetch(
180
- `${RECORDER_URL}/api/requests?secret=${secret}&status=recorded&handlerPath=netlify/functions/my-handler&after=2025-01-01T00:00:00Z&before=2025-12-31T23:59:59Z`
181
- );
182
- const { rows, total, page, limit } = await res.json();
157
+ On failure:
158
+ ```json
159
+ { "status": "failed", "error": "Error message" }
183
160
  ```
184
161
 
185
- Query parameters:
186
-
187
- | Parameter | Description |
188
- |---|---|
189
- | `secret` | **(required)** The secret string used when creating the requests |
190
- | `id` | Search by request ID prefix |
191
- | `status` | Filter by status: `captured`, `queued`, `processing`, `recorded`, `failed`, `all` |
192
- | `handlerPath` | Filter by handler path (exact match) |
193
- | `after` | Only include requests created at or after this ISO timestamp |
194
- | `before` | Only include requests created at or before this ISO timestamp |
195
- | `page` | Page number (default 1) |
196
- | `limit` | Page size (default 20, max 100) |
162
+ ### 5. Manage stored requests
197
163
 
198
- Without a `secret` parameter, only requests created without a secret are returned.
199
-
200
- #### Checking request status with a secret
201
-
202
- When a request was created with a secret, you must include the secret when polling its status:
164
+ Use the `backendRequests*` helpers to query and manage captured requests in your database:
203
165
 
204
166
  ```typescript
205
- const res = await fetch(
206
- `${RECORDER_URL}/api/get-request?requestId=${requestId}&secret=${secret}`
207
- );
208
- ```
209
-
210
- Requests created without a secret remain accessible without one (backward compatible).
211
-
212
- #### Managing your secret
213
-
214
- Store your secret as a `NETLIFY_RECORDER_SECRET` environment variable. To keep it secure:
215
-
216
- 1. **Netlify site environment:** Add `NETLIFY_RECORDER_SECRET` in your Netlify site's environment variables (Site settings > Environment variables). This makes it available to all deployed functions.
167
+ import {
168
+ backendRequestsGet,
169
+ backendRequestsList,
170
+ backendRequestsUpdateStatus,
171
+ } from "@replayio-app-building/netlify-recorder";
217
172
 
218
- 2. **Branch-level secret** (for CI/development): If you're using the Netlify Recorder agent infrastructure, store the secret as a branch secret so deploy scripts and background agents can access it:
173
+ // Get a single request by ID
174
+ const request = await backendRequestsGet(sql, requestId);
175
+ // request.status: "captured" | "queued" | "processing" | "recorded" | "failed"
176
+ // request.recording_id: string | null
219
177
 
220
- ```bash
221
- set-branch-secret NETLIFY_RECORDER_SECRET "your-secret-value"
222
- ```
178
+ // List requests with optional filters
179
+ const requests = await backendRequestsList(sql, { status: "captured", limit: 20 });
223
180
 
224
- 3. **Local development:** Add `NETLIFY_RECORDER_SECRET` to your `.env` file (make sure `.env` is in `.gitignore`).
181
+ // Update status after recording completes
182
+ await backendRequestsUpdateStatus(sql, requestId, "recorded", recordingId);
225
183
 
226
- Use a strong random string (e.g. `openssl rand -base64 32`) and rotate it if compromised. All requests created under the old secret remain accessible only with the old secret value — there is no migration mechanism, so plan rotations during low-traffic windows.
184
+ // Update status on failure
185
+ await backendRequestsUpdateStatus(sql, requestId, "failed", undefined, "Error message");
186
+ ```
227
187
 
228
188
  ---
229
189
 
@@ -261,7 +221,7 @@ No special SQL wrapper is needed. Any Neon SQL query inside a handler wrapped wi
261
221
  ```typescript
262
222
  import {
263
223
  createRecordingRequestHandler,
264
- remoteCallbacks,
224
+ databaseCallbacks,
265
225
  } from "@replayio-app-building/netlify-recorder";
266
226
 
267
227
  export default createRecordingRequestHandler(
@@ -271,9 +231,8 @@ export default createRecordingRequestHandler(
271
231
  return { statusCode: 200, body: "OK" };
272
232
  },
273
233
  {
274
- callbacks: remoteCallbacks(RECORDER_URL),
234
+ callbacks: databaseCallbacks(sql),
275
235
  handlerPath: "netlify/functions/create-order",
276
- secret: process.env.NETLIFY_RECORDER_SECRET,
277
236
  }
278
237
  );
279
238
  ```
@@ -314,22 +273,21 @@ The network interceptor detects Neon SQL HTTP requests (which use `fetch` intern
314
273
 
315
274
  Wraps a Netlify handler function with automatic request recording. This is the recommended way to integrate — it handles `startRequest`/`finishRequest` and error cleanup internally.
316
275
 
317
- **Response timing:** When the Netlify Functions v2 `context` object is available (with `waitUntil`), the response is returned to the client **immediately** with a pre-generated `X-Replay-Request-Id` header. The blob upload and metadata storage continue in the background via `context.waitUntil()`, adding zero latency to the client response.
276
+ **Response timing:** When the Netlify Functions v2 `context` object is available (with `waitUntil`), the response is returned to the client **immediately** with a pre-generated `X-Replay-Request-Id` header. The data storage continues in the background via `context.waitUntil()`, adding zero latency to the client response.
318
277
 
319
278
  When `context.waitUntil` is not available (v1 handlers or missing context), the wrapper falls back to awaiting `finishRequest` before returning.
320
279
 
321
280
  **Parameters:**
322
281
  - `handler` — Your async handler function `(event, context?) => Promise<{ statusCode, headers?, body? }>`
323
- - `options.callbacks` — `remoteCallbacks(serviceUrl)` for sending captured data to the Netlify Recorder service
282
+ - `options.callbacks` — `databaseCallbacks(sql)` to store captured data in the `backend_requests` table
324
283
  - `options.handlerPath` — Path to the handler file (used for recording metadata)
325
284
  - `options.commitSha` — Override `COMMIT_SHA` env var
326
285
  - `options.branchName` — Override `BRANCH_NAME` env var
327
286
  - `options.repositoryUrl` — Override `REPLAY_REPOSITORY_URL` env var
328
- - `options.secret` — Optional secret string. When set, the stored request is only accessible via API calls that provide the same secret value.
329
287
 
330
288
  **Returns:** A wrapped handler function with the same signature.
331
289
 
332
- **Callbacks note:** When using the `waitUntil` flow, `storeRequestData` receives a `requestId` field in its data parameter. Callbacks should use this as the row ID so the stored record matches the ID already sent to the client.
290
+ **Callbacks note:** When using the `waitUntil` flow, `storeRequest` receives a `requestId` field in its data parameter. Callbacks should use this as the row ID so the stored record matches the ID already sent to the client.
333
291
 
334
292
  ### `startRequest(event): RequestContext`
335
293
 
@@ -346,31 +304,158 @@ For v2 Request inputs, the body is read from a **clone** — the original reques
346
304
 
347
305
  ### `finishRequest(requestContext, callbacks, response, options?): Promise<HandlerResponse>`
348
306
 
349
- 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.
307
+ Lower-level API. Finalizes the request capture. Restores original `fetch` and `process.env`, serializes the captured data, stores it via the callback, and returns the response with `X-Replay-Request-Id` header set.
350
308
 
351
309
  **Important:** You must send the returned response to the client — it contains the `X-Replay-Request-Id` header.
352
310
 
353
- 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.
311
+ Logs a `console.warn` when the total duration exceeds 2 seconds to help diagnose slow operations.
354
312
 
355
313
  **Requires** the following environment variables (or equivalent options overrides): `COMMIT_SHA`, `BRANCH_NAME`, `REPLAY_REPOSITORY_URL`. Throws if any are missing.
356
314
 
357
315
  **Parameters:**
358
316
  - `requestContext` — The context returned by `startRequest`
359
- - `callbacks` — `remoteCallbacks(serviceUrl)` for sending captured data to the Netlify Recorder service
317
+ - `callbacks` — `databaseCallbacks(sql)` or a custom `{ storeRequest }` callback
360
318
  - `response` — The handler's response object (`{ statusCode, headers?, body? }`)
361
319
  - `options.handlerPath` — Path to the handler file (used for recording metadata)
362
320
  - `options.commitSha` — Override `COMMIT_SHA` env var
363
321
  - `options.branchName` — Override `BRANCH_NAME` env var
364
322
  - `options.repositoryUrl` — Override `REPLAY_REPOSITORY_URL` env var
365
323
  - `options.requestId` — Pre-generated request ID (used by `createRecordingRequestHandler` in the `waitUntil` flow)
366
- - `options.secret` — Optional secret string. When set, the stored request is only accessible via API calls that provide the same secret value.
367
324
 
368
- ### `remoteCallbacks(serviceUrl): FinishRequestCallbacks`
325
+ ### `databaseCallbacks(sql): FinishRequestCallbacks`
326
+
327
+ Creates a `FinishRequestCallbacks` object that stores captured request data directly in the `backend_requests` table. This is the standard way to provide callbacks to `createRecordingRequestHandler` or `finishRequest`.
328
+
329
+ **Parameters:**
330
+ - `sql` — A Neon SQL tagged-template function
331
+
332
+ **Returns:** An object with a `storeRequest` method that inserts rows into `backend_requests`.
333
+
334
+ ### `backendRequestsEnsureTable(sql): Promise<void>`
335
+
336
+ Creates the `backend_requests` table and its indexes. Call once during schema initialization.
337
+
338
+ The table schema:
339
+
340
+ | Column | Type | Description |
341
+ |---|---|---|
342
+ | `id` | UUID (PK) | Auto-generated request ID |
343
+ | `blob_data` | TEXT | Serialized JSON of captured request data |
344
+ | `handler_path` | TEXT | Path to the handler file |
345
+ | `commit_sha` | TEXT | Git commit SHA |
346
+ | `branch_name` | TEXT | Git branch name (default: `'main'`) |
347
+ | `repository_url` | TEXT | Git repository URL (nullable) |
348
+ | `status` | TEXT | `'captured'`, `'queued'`, `'processing'`, `'recorded'`, or `'failed'` |
349
+ | `recording_id` | TEXT | Replay recording ID (set when status is `'recorded'`) |
350
+ | `error_message` | TEXT | Error details (set when status is `'failed'`) |
351
+ | `created_at` | TIMESTAMPTZ | Row creation time |
352
+ | `updated_at` | TIMESTAMPTZ | Last update time |
353
+
354
+ **Parameters:**
355
+ - `sql` — A Neon SQL tagged-template function
356
+
357
+ ### `backendRequestsInsert(sql, data): Promise<string>`
358
+
359
+ Inserts a new row into `backend_requests` and returns the generated (or provided) ID.
360
+
361
+ **Parameters:**
362
+ - `sql` — A Neon SQL tagged-template function
363
+ - `data.blobData` — Serialized blob JSON string
364
+ - `data.handlerPath` — Handler file path
365
+ - `data.commitSha` — Git commit SHA
366
+ - `data.branchName` — Git branch name
367
+ - `data.repositoryUrl` — Git repository URL (optional)
368
+ - `data.id` — Pre-generated UUID (optional; auto-generated if omitted)
369
+
370
+ ### `backendRequestsGet(sql, id): Promise<BackendRequest | null>`
371
+
372
+ Retrieves a single request by ID, or `null` if not found.
373
+
374
+ **Parameters:**
375
+ - `sql` — A Neon SQL tagged-template function
376
+ - `id` — The request UUID
377
+
378
+ ### `backendRequestsGetBlobData(sql, id): Promise<string | null>`
379
+
380
+ Retrieves only the `blob_data` column for a request, or `null` if not found. Use this to serve blob data to the recording container without fetching the full row.
381
+
382
+ **Parameters:**
383
+ - `sql` — A Neon SQL tagged-template function
384
+ - `id` — The request UUID
385
+
386
+ ### `backendRequestsList(sql, filters?): Promise<BackendRequest[]>`
387
+
388
+ Lists requests ordered by `created_at` DESC, with optional filters.
389
+
390
+ **Parameters:**
391
+ - `sql` — A Neon SQL tagged-template function
392
+ - `filters.status` — Filter by status (e.g. `"captured"`, `"recorded"`)
393
+ - `filters.limit` — Maximum rows to return (default: 50)
394
+
395
+ ### `backendRequestsUpdateStatus(sql, id, status, recordingId?, errorMessage?): Promise<void>`
369
396
 
370
- Creates callbacks for `finishRequest` that send captured data to the Netlify Recorder service. The service handles blob storage and request tracking.
397
+ Updates the status of a request. Optionally sets `recording_id` (on success) or `error_message` (on failure).
371
398
 
372
399
  **Parameters:**
373
- - `serviceUrl` — Base URL of the Netlify Recorder service (e.g. `"https://netlify-recorder-bm4wmw.netlify.app"`)
400
+ - `sql` — A Neon SQL tagged-template function
401
+ - `id` — The request UUID
402
+ - `status` — New status string
403
+ - `recordingId` — Replay recording ID (optional, for `"recorded"` status)
404
+ - `errorMessage` — Error details (optional, for `"failed"` status)
405
+
406
+ ### `ensureRequestRecording(sql, requestId, options): Promise<string | null>`
407
+
408
+ Ensures a Replay recording exists (or is being created) for a backend request. Looks up the request in `backend_requests`, returns the `recording_id` if already recorded, or calls the Netlify Recorder service to queue a recording. Idempotent — safe to call multiple times for the same request.
409
+
410
+ **Parameters:**
411
+ - `sql` — A Neon SQL tagged-template function
412
+ - `requestId` — The backend request UUID
413
+ - `options.recorderUrl` — Base URL of the Netlify Recorder service
414
+ - `options.blobDataUrl` — Override the URL where the recording container fetches the blob JSON (defaults to `${recorderUrl}/api/get-backend-request-blob?requestId=${requestId}`)
415
+ - `options.webhookUrl` — URL to POST the result to when the recording completes or fails
416
+
417
+ **Returns:** The recording ID (`string`) if the request is already recorded, or `null` if the recording was queued or is in progress.
418
+
419
+ **Throws:** If the request ID is not found in `backend_requests`, or if the service call fails.
420
+
421
+ ### `createRequestRecording(blobUrlOrData, handlerPath, requestInfo): Promise<RecordingResult>`
422
+
423
+ Called inside a recording container running under `replay-node`. Downloads the captured data blob (or accepts pre-parsed `BlobData`), installs replay-mode interceptors that return pre-recorded responses instead of making real calls, and executes the original handler so `replay-node` can record the execution.
424
+
425
+ Returns a `RecordingResult` with mismatch detection:
426
+
427
+ | Field | Type | Description |
428
+ |---|---|---|
429
+ | `responseMismatch` | boolean | Whether the replay response differs from the captured response |
430
+ | `mismatchDetails` | string? | Description of the mismatch |
431
+ | `replayResponse` | HandlerResponse? | The response produced during replay |
432
+ | `capturedResponse` | HandlerResponse? | The original captured response |
433
+ | `unconsumedNetworkCalls` | boolean | Whether some recorded network calls were not replayed |
434
+ | `unconsumedNetworkDetails` | string? | Details about unconsumed calls |
435
+
436
+ **Parameters:**
437
+ - `blobUrlOrData` — URL to the captured data blob, or pre-parsed `BlobData` object
438
+ - `handlerPath` — Path to the handler module to execute
439
+ - `requestInfo` — The original request info to replay
440
+
441
+ ### `getCurrentRequestId(): string | null`
442
+
443
+ Returns the request ID for the currently executing handler, or `null` if no handler is active. Useful for correlating logs or database operations with the current request.
444
+
445
+ ### `redactBlobData(data): BlobData`
446
+
447
+ Redacts sensitive environment variable values from captured blob data before storage. Applied automatically by `finishRequest` — you only need to call this directly if using the lower-level APIs.
448
+
449
+ Redaction rules:
450
+ - Allow-listed keys (standard system/runtime variables like `NODE_ENV`, `PATH`, `COMMIT_SHA`) are never redacted
451
+ - Values that are `undefined` or 8 characters or shorter are kept as-is
452
+ - All other values are replaced with `*` repeated to the same length
453
+ - Redacted values are also scrubbed from all other string fields in the blob (request headers, network call bodies, etc.) to prevent leakage through embedded values
454
+
455
+ **Parameters:**
456
+ - `data` — A `BlobData` object
457
+
458
+ **Returns:** A new `BlobData` object with sensitive values masked.
374
459
 
375
460
  ### `databaseAuditEnsureLogTable(sql): Promise<void>`
376
461
 
@@ -403,10 +488,9 @@ These must be set on your Netlify site. Your deploy script should resolve them f
403
488
  | `COMMIT_SHA` | Git commit hash of the deployed code | `git rev-parse HEAD` |
404
489
  | `BRANCH_NAME` | Git branch of the deployed code | `git rev-parse --abbrev-ref HEAD` |
405
490
  | `REPLAY_REPOSITORY_URL` | Git repository URL (no embedded credentials) | `git remote get-url origin` (strip tokens) |
406
- | `NETLIFY_RECORDER_SECRET` | Secret for access control (strongly recommended) | `openssl rand -base64 32` — store in Netlify site env vars |
407
491
 
408
492
  ## How It Works
409
493
 
410
- 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 the Netlify Recorder service via `remoteCallbacks`.
494
+ 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. Sensitive environment variable values are automatically redacted. When the handler completes, the originals are restored, the captured data is serialized to JSON, and stored in the `backend_requests` table in your database via `databaseCallbacks`.
411
495
 
412
- 2. **Recording phase**: The Netlify Recorder service dispatches the captured data to a recording container. Inside the container, the captured blob is downloaded, replay-mode interceptors return the pre-recorded responses instead of making real calls, and the original handler is re-executed under `replay-node`. Since replay-node records all execution, this produces a Replay recording of the exact same handler execution.
496
+ 2. **Recording phase**: To create a Replay recording, POST to the Netlify Recorder service's `create-recording` endpoint with a `blobDataUrl` pointing to the captured data. The service dispatches the work to a recording container, which fetches the blob data from the URL, installs replay-mode interceptors that return pre-recorded responses instead of making real calls, 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. The recording result includes mismatch detection — the service compares the replay response against the originally captured response to flag any divergence.