@replayio-app-building/netlify-recorder 0.16.0 → 0.18.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 (2) hide show
  1. package/README.md +182 -298
  2. 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 as a blob, 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
 
@@ -8,32 +8,31 @@ Capture and replay Netlify function executions as [Replay](https://replay.io) re
8
8
  npm install @replayio-app-building/netlify-recorder
9
9
  ```
10
10
 
11
- ## Integration Options
11
+ ## Setup
12
12
 
13
- There are two ways to use this package:
13
+ ### 1. Create the backend_requests table
14
14
 
15
- - **Option A: Use the Netlify Recorder service (recommended)** — The service handles blob storage, request tracking, and recording creation. Your app just wraps its handlers and calls `remoteCallbacks()`. No database or container infrastructure needed.
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
16
 
17
- - **Option B: Self-hosted** — You manage your own blob storage, database tables, and recording containers. Full control, but requires more setup.
18
-
19
- ---
17
+ ```typescript
18
+ import { backendRequestsEnsureTable } from "@replayio-app-building/netlify-recorder";
20
19
 
21
- ## Option A: Using the Netlify Recorder Service
20
+ await backendRequestsEnsureTable(sql);
21
+ ```
22
22
 
23
- The Netlify Recorder app (`https://netlify-recorder-bm4wmw.netlify.app`) provides a hosted service that stores captured request data, manages a pool of recording containers, and creates Replay recordings on demand. Your app needs zero database or infrastructure setup.
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
24
 
25
- ### 1. Set required environment variables
25
+ ### 2. Set required environment variables
26
26
 
27
- `finishRequest` needs to know which repository, branch, and commit your deployed code belongs to. Set these three environment variables on your Netlify site:
27
+ `finishRequest` needs to know which repository, branch, and commit your deployed code belongs to. Set these environment variables on your Netlify site:
28
28
 
29
29
  | Variable | Description | How to set |
30
30
  |---|---|---|
31
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 |
32
32
  | `COMMIT_SHA` | The git commit hash of the deployed code | Set in your deploy script via `git rev-parse HEAD` |
33
33
  | `BRANCH_NAME` | The git branch of the deployed code | Set in your deploy script via `git rev-parse --abbrev-ref HEAD` |
34
- | `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` |
35
34
 
36
- 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:
37
36
 
38
37
  ```typescript
39
38
  // In your deploy script:
@@ -45,19 +44,20 @@ const repositoryUrl = execSync("git remote get-url origin", { encoding: "utf-8"
45
44
  // Set these on your Netlify site via the Netlify API or CLI
46
45
  ```
47
46
 
48
- ### 2. Wrap your Netlify function
47
+ ### 3. Wrap your Netlify function
49
48
 
50
- 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.
51
50
 
52
51
  **v1 handler** (Netlify Functions v1 — `event` with `httpMethod`, `path`, etc.):
53
52
 
54
53
  ```typescript
55
54
  import {
56
55
  createRecordingRequestHandler,
57
- remoteCallbacks,
56
+ databaseCallbacks,
58
57
  } from "@replayio-app-building/netlify-recorder";
58
+ import { neon } from "@neondatabase/serverless";
59
59
 
60
- const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
60
+ const sql = neon(process.env.DATABASE_URL!);
61
61
 
62
62
  const handler = createRecordingRequestHandler(
63
63
  async (event) => {
@@ -70,9 +70,8 @@ const handler = createRecordingRequestHandler(
70
70
  };
71
71
  },
72
72
  {
73
- callbacks: remoteCallbacks(RECORDER_URL),
73
+ callbacks: databaseCallbacks(sql),
74
74
  handlerPath: "netlify/functions/my-handler",
75
- secret: process.env.NETLIFY_RECORDER_SECRET,
76
75
  }
77
76
  );
78
77
 
@@ -84,10 +83,11 @@ export { handler };
84
83
  ```typescript
85
84
  import {
86
85
  createRecordingRequestHandler,
87
- remoteCallbacks,
86
+ databaseCallbacks,
88
87
  } from "@replayio-app-building/netlify-recorder";
88
+ import { neon } from "@neondatabase/serverless";
89
89
 
90
- const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
90
+ const sql = neon(process.env.DATABASE_URL!);
91
91
 
92
92
  // The wrapper reads the body from a clone — you can still read req.json() etc.
93
93
  export default createRecordingRequestHandler(
@@ -102,37 +102,57 @@ export default createRecordingRequestHandler(
102
102
  };
103
103
  },
104
104
  {
105
- callbacks: remoteCallbacks(RECORDER_URL),
105
+ callbacks: databaseCallbacks(sql),
106
106
  handlerPath: "netlify/functions/my-handler",
107
- secret: process.env.NETLIFY_RECORDER_SECRET,
108
107
  }
109
108
  );
110
109
  ```
111
110
 
112
- `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.
113
112
 
114
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.
115
114
 
116
- ### 3. Create recordings
115
+ ### 4. Create recordings via the Netlify Recorder service
116
+
117
+ When you want to turn a captured request into a Replay recording, POST to the Netlify Recorder service's `create-recording` endpoint. You need to provide a `blobDataUrl` — a publicly accessible URL where the recording container can fetch the captured blob data.
117
118
 
118
- When you want to turn a captured request into a Replay recording, POST to the service's `create-recording` endpoint with the request ID. If the request was created with a secret, you must include it:
119
+ If your app exposes the blob data via an endpoint (e.g. using `backendRequestsGetBlobData`), construct the URL and pass it:
119
120
 
120
121
  ```typescript
122
+ const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
123
+ const blobDataUrl = `https://your-app.netlify.app/api/get-backend-request-blob?requestId=${requestId}`;
124
+
121
125
  const response = await fetch(
122
126
  `${RECORDER_URL}/api/create-recording`,
123
127
  {
124
128
  method: "POST",
125
129
  headers: { "Content-Type": "application/json" },
126
130
  body: JSON.stringify({
127
- requestId,
128
- secret,
131
+ blobDataUrl,
132
+ handlerPath: "netlify/functions/my-handler",
133
+ commitSha: "abc123",
134
+ branchName: "main",
135
+ repositoryUrl: "https://github.com/org/repo.git", // optional
129
136
  webhookUrl: "https://your-app.netlify.app/api/on-recording-complete", // optional
130
137
  }),
131
138
  }
132
139
  );
140
+
141
+ const { requestId: serviceRequestId } = await response.json();
133
142
  ```
134
143
 
135
- The service looks up the stored blob data, dispatches the work to a pool container, and creates the recording.
144
+ The `create-recording` endpoint accepts:
145
+
146
+ | Parameter | Required | Description |
147
+ |---|---|---|
148
+ | `blobDataUrl` | Yes | URL where the recording container can fetch the captured blob JSON |
149
+ | `handlerPath` | Yes | Path to the handler file (e.g. `netlify/functions/my-handler`) |
150
+ | `commitSha` | Yes | Git commit SHA of the deployed code |
151
+ | `branchName` | Yes | Git branch of the deployed code |
152
+ | `repositoryUrl` | No | Git repository URL for the container to clone |
153
+ | `webhookUrl` | No | URL to POST the result to when the recording completes or fails |
154
+
155
+ The service dispatches the work to a recording container, which fetches the blob data from `blobDataUrl`, replays the handler execution under `replay-node`, and produces a Replay recording.
136
156
 
137
157
  If `webhookUrl` is provided, the service will POST the result to that URL when the recording completes (or fails):
138
158
 
@@ -146,249 +166,32 @@ On failure:
146
166
  { "status": "failed", "error": "Error message" }
147
167
  ```
148
168
 
149
- ### 4. Check recording status
150
-
151
- You can poll the recording status at any time. If the request was created with a secret, you must include it:
152
-
153
- ```typescript
154
- const res = await fetch(
155
- `${RECORDER_URL}/api/get-request?requestId=${requestId}&secret=${secret}`
156
- );
157
- const { status, recordingId } = await res.json();
158
- // status: "captured" | "processing" | "recorded" | "failed"
159
- // recordingId: string | null
160
- ```
161
-
162
- Requests created with a secret return 403 if the secret is missing or incorrect.
163
-
164
- ### 5. Access control with secrets
165
-
166
- 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.
167
-
168
- #### Setting a secret
169
-
170
- Pass `secret` in the options when wrapping your handler:
171
-
172
- ```typescript
173
- export default createRecordingRequestHandler(
174
- async (req) => {
175
- const result = await myBusinessLogic();
176
- return { statusCode: 200, body: JSON.stringify(result) };
177
- },
178
- {
179
- callbacks: remoteCallbacks(RECORDER_URL),
180
- handlerPath: "netlify/functions/my-handler",
181
- secret: process.env.NETLIFY_RECORDER_SECRET,
182
- }
183
- );
184
- ```
185
-
186
- #### Listing requests by secret
187
-
188
- 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:
189
-
190
- ```typescript
191
- const res = await fetch(
192
- `${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`
193
- );
194
- const { rows, total, page, limit } = await res.json();
195
- ```
196
-
197
- Query parameters:
198
-
199
- | Parameter | Description |
200
- |---|---|
201
- | `secret` | **(required)** The secret string used when creating the requests |
202
- | `id` | Search by request ID prefix |
203
- | `status` | Filter by status: `captured`, `queued`, `processing`, `recorded`, `failed`, `all` |
204
- | `handlerPath` | Filter by handler path (exact match) |
205
- | `after` | Only include requests created at or after this ISO timestamp |
206
- | `before` | Only include requests created at or before this ISO timestamp |
207
- | `page` | Page number (default 1) |
208
- | `limit` | Page size (default 20, max 100) |
209
-
210
- Without a `secret` parameter, only requests created without a secret are returned.
211
-
212
- #### Checking request status with a secret
213
-
214
- When a request was created with a secret, you must include the secret when polling its status:
215
-
216
- ```typescript
217
- const res = await fetch(
218
- `${RECORDER_URL}/api/get-request?requestId=${requestId}&secret=${secret}`
219
- );
220
- ```
221
-
222
- Requests created without a secret remain accessible without one (backward compatible).
223
-
224
- #### Managing your secret
225
-
226
- Store your secret as a `NETLIFY_RECORDER_SECRET` environment variable. To keep it secure:
227
-
228
- 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.
229
-
230
- 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:
231
-
232
- ```bash
233
- set-branch-secret NETLIFY_RECORDER_SECRET "your-secret-value"
234
- ```
235
-
236
- 3. **Local development:** Add `NETLIFY_RECORDER_SECRET` to your `.env` file (make sure `.env` is in `.gitignore`).
237
-
238
- 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.
239
-
240
- ---
241
-
242
- ## Option B: Self-Hosted
243
-
244
- If you need full control, you can manage your own blob storage, database, and recording containers. This requires more setup but gives you complete ownership of the data pipeline.
245
-
246
- ### 1. Set required environment variables
247
-
248
- Same as Option A — you must set `REPLAY_REPOSITORY_URL`, `COMMIT_SHA`, and `BRANCH_NAME` on your Netlify site. See the table in Option A, Step 1 above.
249
-
250
- ### 2. Wrap your Netlify function
251
-
252
- Use `createRecordingRequestHandler` with custom callbacks that write to your own storage and database:
253
-
254
- ```typescript
255
- import { createRecordingRequestHandler } from "@replayio-app-building/netlify-recorder";
256
-
257
- const handler = createRecordingRequestHandler(
258
- async (event) => {
259
- const result = await myBusinessLogic();
260
-
261
- return {
262
- statusCode: 200,
263
- headers: { "Content-Type": "application/json" },
264
- body: JSON.stringify(result),
265
- };
266
- },
267
- {
268
- secret: process.env.NETLIFY_RECORDER_SECRET,
269
- callbacks: {
270
- uploadBlob: async (data) => {
271
- // Upload the JSON string to your blob storage (S3, R2, etc.)
272
- const res = await fetch("https://storage.example.com/upload", {
273
- method: "PUT",
274
- body: data,
275
- });
276
- const { url } = await res.json();
277
- return url;
278
- },
279
- storeRequestData: async ({ blobUrl, commitSha, branchName, repositoryUrl, handlerPath, secret }) => {
280
- const [row] = await sql`
281
- INSERT INTO requests (blob_url, commit_sha, branch_name, repository_url, handler_path, secret, status)
282
- VALUES (${blobUrl}, ${commitSha}, ${branchName}, ${repositoryUrl}, ${handlerPath}, ${secret}, 'captured')
283
- RETURNING id
284
- `;
285
- return row.id;
286
- },
287
- },
288
- }
289
- );
290
-
291
- export { handler };
292
- ```
293
-
294
- ### 3. Create a requests database table
295
-
296
- ```sql
297
- CREATE TABLE IF NOT EXISTS requests (
298
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
299
- blob_url TEXT,
300
- commit_sha TEXT,
301
- branch_name TEXT,
302
- repository_url TEXT,
303
- handler_path TEXT,
304
- secret TEXT,
305
- recording_id TEXT,
306
- status TEXT NOT NULL DEFAULT 'captured'
307
- CHECK (status IN ('captured', 'processing', 'recorded', 'failed')),
308
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
309
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
310
- );
311
-
312
- CREATE INDEX IF NOT EXISTS idx_requests_secret ON requests (secret) WHERE secret IS NOT NULL;
313
- ```
169
+ ### 5. Manage stored requests
314
170
 
315
- ### 4. Create a background function to produce recordings
171
+ Use the `backendRequests*` helpers to query and manage captured requests in your database:
316
172
 
317
173
  ```typescript
318
- import { ensureRequestRecording } from "@replayio-app-building/netlify-recorder";
319
- import type { Handler } from "@netlify/functions";
320
-
321
- const handler: Handler = async (event) => {
322
- const { requestId } = JSON.parse(event.body ?? "{}");
323
-
324
- const recordingId = await ensureRequestRecording(requestId, {
325
- repositoryUrl: process.env.APP_REPOSITORY_URL!,
326
- lookupRequest: async (id) => {
327
- const [row] = await sql`
328
- SELECT blob_url, commit_sha, branch_name, handler_path
329
- FROM requests WHERE id = ${id}
330
- `;
331
- return {
332
- blobUrl: row.blob_url,
333
- commitSha: row.commit_sha,
334
- branchName: row.branch_name ?? "main",
335
- handlerPath: row.handler_path,
336
- };
337
- },
338
- updateStatus: async (id, status, recordingId) => {
339
- await sql`
340
- UPDATE requests
341
- SET status = ${status},
342
- recording_id = ${recordingId ?? null},
343
- updated_at = NOW()
344
- WHERE id = ${id}
345
- `;
346
- },
347
- });
348
-
349
- return {
350
- statusCode: 200,
351
- body: JSON.stringify({ recordingId }),
352
- };
353
- };
174
+ import {
175
+ backendRequestsGet,
176
+ backendRequestsList,
177
+ backendRequestsUpdateStatus,
178
+ } from "@replayio-app-building/netlify-recorder";
354
179
 
355
- export { handler };
356
- ```
180
+ // Get a single request by ID
181
+ const request = await backendRequestsGet(sql, requestId);
182
+ // request.status: "captured" | "queued" | "processing" | "recorded" | "failed"
183
+ // request.recording_id: string | null
357
184
 
358
- ### 5. Create a container script
185
+ // List requests with optional filters
186
+ const requests = await backendRequestsList(sql, { status: "captured", limit: 20 });
359
187
 
360
- This script runs inside the recording container under `replay-node`:
188
+ // Update status after recording completes
189
+ await backendRequestsUpdateStatus(sql, requestId, "recorded", recordingId);
361
190
 
362
- ```typescript
363
- // scripts/create-request-recording.ts
364
- import { createRequestRecording } from "@replayio-app-building/netlify-recorder";
365
-
366
- const args = process.argv.slice(2);
367
- const blobUrl = args[args.indexOf("--blob-url") + 1]!;
368
- const handlerPath = args[args.indexOf("--handler-path") + 1]!;
369
-
370
- await createRequestRecording(blobUrl, handlerPath, {
371
- method: "POST",
372
- url: handlerPath,
373
- headers: {},
374
- });
191
+ // Update status on failure
192
+ await backendRequestsUpdateStatus(sql, requestId, "failed", undefined, "Error message");
375
193
  ```
376
194
 
377
- ### Required infrastructure
378
-
379
- Self-hosted recording requires these environment variables:
380
-
381
- | Variable | Description |
382
- |---|---|
383
- | `INFISICAL_CLIENT_ID` | Infisical service account client ID |
384
- | `INFISICAL_CLIENT_SECRET` | Infisical service account client secret |
385
- | `INFISICAL_PROJECT_ID` | Infisical project ID |
386
- | `INFISICAL_ENVIRONMENT` | Infisical environment (e.g. `production`) |
387
- | `FLY_API_TOKEN` | Fly.io API token for container management |
388
- | `FLY_APP_NAME` | Fly.io app name for container deployment |
389
- | `APP_REPOSITORY_URL` | Git repository URL for container cloning |
390
- | `RECORD_REPLAY_API_KEY` | Replay API key for recording upload |
391
-
392
195
  ---
393
196
 
394
197
  ## Audit Log Support
@@ -425,7 +228,7 @@ No special SQL wrapper is needed. Any Neon SQL query inside a handler wrapped wi
425
228
  ```typescript
426
229
  import {
427
230
  createRecordingRequestHandler,
428
- remoteCallbacks,
231
+ databaseCallbacks,
429
232
  } from "@replayio-app-building/netlify-recorder";
430
233
 
431
234
  export default createRecordingRequestHandler(
@@ -435,9 +238,8 @@ export default createRecordingRequestHandler(
435
238
  return { statusCode: 200, body: "OK" };
436
239
  },
437
240
  {
438
- callbacks: remoteCallbacks(RECORDER_URL),
241
+ callbacks: databaseCallbacks(sql),
439
242
  handlerPath: "netlify/functions/create-order",
440
- secret: process.env.NETLIFY_RECORDER_SECRET,
441
243
  }
442
244
  );
443
245
  ```
@@ -478,22 +280,21 @@ The network interceptor detects Neon SQL HTTP requests (which use `fetch` intern
478
280
 
479
281
  Wraps a Netlify handler function with automatic request recording. This is the recommended way to integrate — it handles `startRequest`/`finishRequest` and error cleanup internally.
480
282
 
481
- **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.
283
+ **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.
482
284
 
483
285
  When `context.waitUntil` is not available (v1 handlers or missing context), the wrapper falls back to awaiting `finishRequest` before returning.
484
286
 
485
287
  **Parameters:**
486
288
  - `handler` — Your async handler function `(event, context?) => Promise<{ statusCode, headers?, body? }>`
487
- - `options.callbacks` — Either `remoteCallbacks(serviceUrl)` or custom `{ uploadBlob, storeRequestData }` callbacks
289
+ - `options.callbacks` — `databaseCallbacks(sql)` to store captured data in the `backend_requests` table
488
290
  - `options.handlerPath` — Path to the handler file (used for recording metadata)
489
291
  - `options.commitSha` — Override `COMMIT_SHA` env var
490
292
  - `options.branchName` — Override `BRANCH_NAME` env var
491
293
  - `options.repositoryUrl` — Override `REPLAY_REPOSITORY_URL` env var
492
- - `options.secret` — Optional secret string. When set, the stored request is only accessible via API calls that provide the same secret value.
493
294
 
494
295
  **Returns:** A wrapped handler function with the same signature.
495
296
 
496
- **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.
297
+ **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.
497
298
 
498
299
  ### `startRequest(event): RequestContext`
499
300
 
@@ -510,51 +311,144 @@ For v2 Request inputs, the body is read from a **clone** — the original reques
510
311
 
511
312
  ### `finishRequest(requestContext, callbacks, response, options?): Promise<HandlerResponse>`
512
313
 
513
- 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.
314
+ 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.
514
315
 
515
316
  **Important:** You must send the returned response to the client — it contains the `X-Replay-Request-Id` header.
516
317
 
517
- 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.
318
+ Logs a `console.warn` when the total duration exceeds 2 seconds to help diagnose slow operations.
518
319
 
519
320
  **Requires** the following environment variables (or equivalent options overrides): `COMMIT_SHA`, `BRANCH_NAME`, `REPLAY_REPOSITORY_URL`. Throws if any are missing.
520
321
 
521
322
  **Parameters:**
522
323
  - `requestContext` — The context returned by `startRequest`
523
- - `callbacks` — Either `remoteCallbacks(serviceUrl)` or custom `{ uploadBlob, storeRequestData }` callbacks
324
+ - `callbacks` — `databaseCallbacks(sql)` or a custom `{ storeRequest }` callback
524
325
  - `response` — The handler's response object (`{ statusCode, headers?, body? }`)
525
326
  - `options.handlerPath` — Path to the handler file (used for recording metadata)
526
327
  - `options.commitSha` — Override `COMMIT_SHA` env var
527
328
  - `options.branchName` — Override `BRANCH_NAME` env var
528
329
  - `options.repositoryUrl` — Override `REPLAY_REPOSITORY_URL` env var
529
330
  - `options.requestId` — Pre-generated request ID (used by `createRecordingRequestHandler` in the `waitUntil` flow)
530
- - `options.secret` — Optional secret string. When set, the stored request is only accessible via API calls that provide the same secret value.
531
331
 
532
- ### `remoteCallbacks(serviceUrl): FinishRequestCallbacks`
332
+ ### `databaseCallbacks(sql): FinishRequestCallbacks`
333
+
334
+ 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`.
335
+
336
+ **Parameters:**
337
+ - `sql` — A Neon SQL tagged-template function
338
+
339
+ **Returns:** An object with a `storeRequest` method that inserts rows into `backend_requests`.
340
+
341
+ ### `backendRequestsEnsureTable(sql): Promise<void>`
342
+
343
+ Creates the `backend_requests` table and its indexes. Call once during schema initialization.
344
+
345
+ The table schema:
346
+
347
+ | Column | Type | Description |
348
+ |---|---|---|
349
+ | `id` | UUID (PK) | Auto-generated request ID |
350
+ | `blob_data` | TEXT | Serialized JSON of captured request data |
351
+ | `handler_path` | TEXT | Path to the handler file |
352
+ | `commit_sha` | TEXT | Git commit SHA |
353
+ | `branch_name` | TEXT | Git branch name (default: `'main'`) |
354
+ | `repository_url` | TEXT | Git repository URL (nullable) |
355
+ | `status` | TEXT | `'captured'`, `'queued'`, `'processing'`, `'recorded'`, or `'failed'` |
356
+ | `recording_id` | TEXT | Replay recording ID (set when status is `'recorded'`) |
357
+ | `error_message` | TEXT | Error details (set when status is `'failed'`) |
358
+ | `created_at` | TIMESTAMPTZ | Row creation time |
359
+ | `updated_at` | TIMESTAMPTZ | Last update time |
360
+
361
+ **Parameters:**
362
+ - `sql` — A Neon SQL tagged-template function
363
+
364
+ ### `backendRequestsInsert(sql, data): Promise<string>`
365
+
366
+ Inserts a new row into `backend_requests` and returns the generated (or provided) ID.
367
+
368
+ **Parameters:**
369
+ - `sql` — A Neon SQL tagged-template function
370
+ - `data.blobData` — Serialized blob JSON string
371
+ - `data.handlerPath` — Handler file path
372
+ - `data.commitSha` — Git commit SHA
373
+ - `data.branchName` — Git branch name
374
+ - `data.repositoryUrl` — Git repository URL (optional)
375
+ - `data.id` — Pre-generated UUID (optional; auto-generated if omitted)
376
+
377
+ ### `backendRequestsGet(sql, id): Promise<BackendRequest | null>`
378
+
379
+ Retrieves a single request by ID, or `null` if not found.
380
+
381
+ **Parameters:**
382
+ - `sql` — A Neon SQL tagged-template function
383
+ - `id` — The request UUID
384
+
385
+ ### `backendRequestsGetBlobData(sql, id): Promise<string | null>`
386
+
387
+ 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.
388
+
389
+ **Parameters:**
390
+ - `sql` — A Neon SQL tagged-template function
391
+ - `id` — The request UUID
392
+
393
+ ### `backendRequestsList(sql, filters?): Promise<BackendRequest[]>`
533
394
 
534
- Creates callbacks for `finishRequest` that send captured data to the Netlify Recorder service. The service handles blob storage and request tracking — no local database needed.
395
+ Lists requests ordered by `created_at` DESC, with optional filters.
535
396
 
536
397
  **Parameters:**
537
- - `serviceUrl` — Base URL of the Netlify Recorder service (e.g. `"https://netlify-recorder-bm4wmw.netlify.app"`)
398
+ - `sql` — A Neon SQL tagged-template function
399
+ - `filters.status` — Filter by status (e.g. `"captured"`, `"recorded"`)
400
+ - `filters.limit` — Maximum rows to return (default: 50)
538
401
 
539
- ### `ensureRequestRecording(requestId, options): Promise<string>`
402
+ ### `backendRequestsUpdateStatus(sql, id, status, recordingId?, errorMessage?): Promise<void>`
540
403
 
541
- Spawns a container via `@replayio/app-building` to create a Replay recording from captured request data. Returns the recording ID. Only needed for self-hosted setups (Option B).
404
+ Updates the status of a request. Optionally sets `recording_id` (on success) or `error_message` (on failure).
542
405
 
543
406
  **Parameters:**
544
- - `requestId` — The request to create a recording for
545
- - `options.repositoryUrl` — Git repository URL for the container to clone
546
- - `options.lookupRequest(id)` — Fetches `{ blobUrl, commitSha, branchName, handlerPath }` from the database
547
- - `options.updateStatus(id, status, recordingId?)` — Updates the request status in the database
407
+ - `sql` — A Neon SQL tagged-template function
408
+ - `id` — The request UUID
409
+ - `status` — New status string
410
+ - `recordingId` — Replay recording ID (optional, for `"recorded"` status)
411
+ - `errorMessage` — Error details (optional, for `"failed"` status)
548
412
 
549
- ### `createRequestRecording(blobUrl, handlerPath, requestInfo): Promise<void>`
413
+ ### `createRequestRecording(blobUrlOrData, handlerPath, requestInfo): Promise<RecordingResult>`
550
414
 
551
- Called inside a container running under `replay-node`. Downloads the captured data blob, installs replay-mode interceptors (which return pre-recorded responses instead of making real calls), and executes the original handler so replay-node can record the execution. Only needed for self-hosted setups (Option B).
415
+ 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.
416
+
417
+ Returns a `RecordingResult` with mismatch detection:
418
+
419
+ | Field | Type | Description |
420
+ |---|---|---|
421
+ | `responseMismatch` | boolean | Whether the replay response differs from the captured response |
422
+ | `mismatchDetails` | string? | Description of the mismatch |
423
+ | `replayResponse` | HandlerResponse? | The response produced during replay |
424
+ | `capturedResponse` | HandlerResponse? | The original captured response |
425
+ | `unconsumedNetworkCalls` | boolean | Whether some recorded network calls were not replayed |
426
+ | `unconsumedNetworkDetails` | string? | Details about unconsumed calls |
552
427
 
553
428
  **Parameters:**
554
- - `blobUrl` — URL to the captured data blob
429
+ - `blobUrlOrData` — URL to the captured data blob, or pre-parsed `BlobData` object
555
430
  - `handlerPath` — Path to the handler module to execute
556
431
  - `requestInfo` — The original request info to replay
557
432
 
433
+ ### `getCurrentRequestId(): string | null`
434
+
435
+ 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.
436
+
437
+ ### `redactBlobData(data): BlobData`
438
+
439
+ 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.
440
+
441
+ Redaction rules:
442
+ - Allow-listed keys (standard system/runtime variables like `NODE_ENV`, `PATH`, `COMMIT_SHA`) are never redacted
443
+ - Values that are `undefined` or 8 characters or shorter are kept as-is
444
+ - All other values are replaced with `*` repeated to the same length
445
+ - 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
446
+
447
+ **Parameters:**
448
+ - `data` — A `BlobData` object
449
+
450
+ **Returns:** A new `BlobData` object with sensitive values masked.
451
+
558
452
  ### `databaseAuditEnsureLogTable(sql): Promise<void>`
559
453
 
560
454
  Creates the `audit_log` table, its indexes, and a reusable PL/pgSQL trigger function (`audit_trigger_function`). Call once during schema initialization.
@@ -579,8 +473,6 @@ Returns all rows from the `audit_log` table, ordered by `performed_at` DESC.
579
473
 
580
474
  ## Environment Variables
581
475
 
582
- ### Required for all setups (read by `finishRequest`)
583
-
584
476
  These must be set on your Netlify site. Your deploy script should resolve them from git and push them to the Netlify environment before deploying. `finishRequest` will throw an error if any are missing.
585
477
 
586
478
  | Variable | Description | How to resolve |
@@ -588,17 +480,9 @@ These must be set on your Netlify site. Your deploy script should resolve them f
588
480
  | `COMMIT_SHA` | Git commit hash of the deployed code | `git rev-parse HEAD` |
589
481
  | `BRANCH_NAME` | Git branch of the deployed code | `git rev-parse --abbrev-ref HEAD` |
590
482
  | `REPLAY_REPOSITORY_URL` | Git repository URL (no embedded credentials) | `git remote get-url origin` (strip tokens) |
591
- | `NETLIFY_RECORDER_SECRET` | Secret for access control (strongly recommended) | `openssl rand -base64 32` — store in Netlify site env vars |
592
-
593
- ### Required for self-hosted recording (Option B)
594
-
595
- | Variable | Description |
596
- |---|---|
597
- | `RECORD_REPLAY_API_KEY` | Replay API key for uploading recordings |
598
- | `APP_REPOSITORY_URL` | Git repository URL for container cloning |
599
483
 
600
484
  ## How It Works
601
485
 
602
- 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).
486
+ 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`.
603
487
 
604
- 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.
488
+ 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {