@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.
- package/README.md +214 -130
- package/dist/index.d.ts +69 -180
- package/dist/index.js +170 -300
- 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
|
|
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.
|
|
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
|
-
|
|
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
|
-
###
|
|
47
|
+
### 3. Wrap your Netlify function
|
|
37
48
|
|
|
38
|
-
Use `createRecordingRequestHandler` with `
|
|
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
|
-
|
|
56
|
+
databaseCallbacks,
|
|
46
57
|
} from "@replayio-app-building/netlify-recorder";
|
|
58
|
+
import { neon } from "@neondatabase/serverless";
|
|
47
59
|
|
|
48
|
-
const
|
|
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:
|
|
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
|
-
|
|
86
|
+
databaseCallbacks,
|
|
76
87
|
} from "@replayio-app-building/netlify-recorder";
|
|
88
|
+
import { neon } from "@neondatabase/serverless";
|
|
77
89
|
|
|
78
|
-
const
|
|
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:
|
|
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
|
|
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
|
-
###
|
|
115
|
+
### 4. Create recordings via the Netlify Recorder service
|
|
105
116
|
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
|
|
124
123
|
|
|
125
|
-
|
|
124
|
+
const recordingId = await ensureRequestRecording(sql, requestId, {
|
|
125
|
+
recorderUrl: RECORDER_URL,
|
|
126
|
+
});
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
```
|
|
178
|
+
// List requests with optional filters
|
|
179
|
+
const requests = await backendRequestsList(sql, { status: "captured", limit: 20 });
|
|
223
180
|
|
|
224
|
-
|
|
181
|
+
// Update status after recording completes
|
|
182
|
+
await backendRequestsUpdateStatus(sql, requestId, "recorded", recordingId);
|
|
225
183
|
|
|
226
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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` — `
|
|
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, `
|
|
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,
|
|
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
|
|
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` — `
|
|
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
|
-
### `
|
|
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
|
-
|
|
397
|
+
Updates the status of a request. Optionally sets `recording_id` (on success) or `error_message` (on failure).
|
|
371
398
|
|
|
372
399
|
**Parameters:**
|
|
373
|
-
- `
|
|
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
|
|
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**:
|
|
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.
|