@replayio-app-building/netlify-recorder 0.17.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.
- package/README.md +195 -119
- package/dist/index.d.ts +47 -182
- package/dist/index.js +136 -301
- 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,37 +102,57 @@ 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
|
-
When you want to turn a captured request into a Replay recording, POST to the Netlify Recorder service's `create-recording` endpoint
|
|
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.
|
|
118
|
+
|
|
119
|
+
If your app exposes the blob data via an endpoint (e.g. using `backendRequestsGetBlobData`), construct the URL and pass it:
|
|
107
120
|
|
|
108
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
|
+
|
|
109
125
|
const response = await fetch(
|
|
110
126
|
`${RECORDER_URL}/api/create-recording`,
|
|
111
127
|
{
|
|
112
128
|
method: "POST",
|
|
113
129
|
headers: { "Content-Type": "application/json" },
|
|
114
130
|
body: JSON.stringify({
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
blobDataUrl,
|
|
132
|
+
handlerPath: "netlify/functions/my-handler",
|
|
133
|
+
commitSha: "abc123",
|
|
134
|
+
branchName: "main",
|
|
135
|
+
repositoryUrl: "https://github.com/org/repo.git", // optional
|
|
117
136
|
webhookUrl: "https://your-app.netlify.app/api/on-recording-complete", // optional
|
|
118
137
|
}),
|
|
119
138
|
}
|
|
120
139
|
);
|
|
140
|
+
|
|
141
|
+
const { requestId: serviceRequestId } = await response.json();
|
|
121
142
|
```
|
|
122
143
|
|
|
123
|
-
The
|
|
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.
|
|
124
156
|
|
|
125
157
|
If `webhookUrl` is provided, the service will POST the result to that URL when the recording completes (or fails):
|
|
126
158
|
|
|
@@ -134,97 +166,32 @@ On failure:
|
|
|
134
166
|
{ "status": "failed", "error": "Error message" }
|
|
135
167
|
```
|
|
136
168
|
|
|
137
|
-
###
|
|
169
|
+
### 5. Manage stored requests
|
|
138
170
|
|
|
139
|
-
|
|
171
|
+
Use the `backendRequests*` helpers to query and manage captured requests in your database:
|
|
140
172
|
|
|
141
173
|
```typescript
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// recordingId: string | null
|
|
148
|
-
```
|
|
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
|
|
157
|
-
|
|
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
|
-
);
|
|
172
|
-
```
|
|
173
|
-
|
|
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();
|
|
183
|
-
```
|
|
184
|
-
|
|
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) |
|
|
174
|
+
import {
|
|
175
|
+
backendRequestsGet,
|
|
176
|
+
backendRequestsList,
|
|
177
|
+
backendRequestsUpdateStatus,
|
|
178
|
+
} from "@replayio-app-building/netlify-recorder";
|
|
197
179
|
|
|
198
|
-
|
|
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
|
|
199
184
|
|
|
200
|
-
|
|
185
|
+
// List requests with optional filters
|
|
186
|
+
const requests = await backendRequestsList(sql, { status: "captured", limit: 20 });
|
|
201
187
|
|
|
202
|
-
|
|
188
|
+
// Update status after recording completes
|
|
189
|
+
await backendRequestsUpdateStatus(sql, requestId, "recorded", recordingId);
|
|
203
190
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
`${RECORDER_URL}/api/get-request?requestId=${requestId}&secret=${secret}`
|
|
207
|
-
);
|
|
191
|
+
// Update status on failure
|
|
192
|
+
await backendRequestsUpdateStatus(sql, requestId, "failed", undefined, "Error message");
|
|
208
193
|
```
|
|
209
194
|
|
|
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.
|
|
217
|
-
|
|
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:
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
set-branch-secret NETLIFY_RECORDER_SECRET "your-secret-value"
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
3. **Local development:** Add `NETLIFY_RECORDER_SECRET` to your `.env` file (make sure `.env` is in `.gitignore`).
|
|
225
|
-
|
|
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.
|
|
227
|
-
|
|
228
195
|
---
|
|
229
196
|
|
|
230
197
|
## Audit Log Support
|
|
@@ -261,7 +228,7 @@ No special SQL wrapper is needed. Any Neon SQL query inside a handler wrapped wi
|
|
|
261
228
|
```typescript
|
|
262
229
|
import {
|
|
263
230
|
createRecordingRequestHandler,
|
|
264
|
-
|
|
231
|
+
databaseCallbacks,
|
|
265
232
|
} from "@replayio-app-building/netlify-recorder";
|
|
266
233
|
|
|
267
234
|
export default createRecordingRequestHandler(
|
|
@@ -271,9 +238,8 @@ export default createRecordingRequestHandler(
|
|
|
271
238
|
return { statusCode: 200, body: "OK" };
|
|
272
239
|
},
|
|
273
240
|
{
|
|
274
|
-
callbacks:
|
|
241
|
+
callbacks: databaseCallbacks(sql),
|
|
275
242
|
handlerPath: "netlify/functions/create-order",
|
|
276
|
-
secret: process.env.NETLIFY_RECORDER_SECRET,
|
|
277
243
|
}
|
|
278
244
|
);
|
|
279
245
|
```
|
|
@@ -314,22 +280,21 @@ The network interceptor detects Neon SQL HTTP requests (which use `fetch` intern
|
|
|
314
280
|
|
|
315
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.
|
|
316
282
|
|
|
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
|
|
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.
|
|
318
284
|
|
|
319
285
|
When `context.waitUntil` is not available (v1 handlers or missing context), the wrapper falls back to awaiting `finishRequest` before returning.
|
|
320
286
|
|
|
321
287
|
**Parameters:**
|
|
322
288
|
- `handler` — Your async handler function `(event, context?) => Promise<{ statusCode, headers?, body? }>`
|
|
323
|
-
- `options.callbacks` — `
|
|
289
|
+
- `options.callbacks` — `databaseCallbacks(sql)` to store captured data in the `backend_requests` table
|
|
324
290
|
- `options.handlerPath` — Path to the handler file (used for recording metadata)
|
|
325
291
|
- `options.commitSha` — Override `COMMIT_SHA` env var
|
|
326
292
|
- `options.branchName` — Override `BRANCH_NAME` env var
|
|
327
293
|
- `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
294
|
|
|
330
295
|
**Returns:** A wrapped handler function with the same signature.
|
|
331
296
|
|
|
332
|
-
**Callbacks note:** When using the `waitUntil` flow, `
|
|
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.
|
|
333
298
|
|
|
334
299
|
### `startRequest(event): RequestContext`
|
|
335
300
|
|
|
@@ -346,31 +311,143 @@ For v2 Request inputs, the body is read from a **clone** — the original reques
|
|
|
346
311
|
|
|
347
312
|
### `finishRequest(requestContext, callbacks, response, options?): Promise<HandlerResponse>`
|
|
348
313
|
|
|
349
|
-
Lower-level API. Finalizes the request capture. Restores original `fetch` and `process.env`, serializes the captured data,
|
|
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.
|
|
350
315
|
|
|
351
316
|
**Important:** You must send the returned response to the client — it contains the `X-Replay-Request-Id` header.
|
|
352
317
|
|
|
353
|
-
Logs a `console.warn` when the total duration exceeds 2 seconds
|
|
318
|
+
Logs a `console.warn` when the total duration exceeds 2 seconds to help diagnose slow operations.
|
|
354
319
|
|
|
355
320
|
**Requires** the following environment variables (or equivalent options overrides): `COMMIT_SHA`, `BRANCH_NAME`, `REPLAY_REPOSITORY_URL`. Throws if any are missing.
|
|
356
321
|
|
|
357
322
|
**Parameters:**
|
|
358
323
|
- `requestContext` — The context returned by `startRequest`
|
|
359
|
-
- `callbacks` — `
|
|
324
|
+
- `callbacks` — `databaseCallbacks(sql)` or a custom `{ storeRequest }` callback
|
|
360
325
|
- `response` — The handler's response object (`{ statusCode, headers?, body? }`)
|
|
361
326
|
- `options.handlerPath` — Path to the handler file (used for recording metadata)
|
|
362
327
|
- `options.commitSha` — Override `COMMIT_SHA` env var
|
|
363
328
|
- `options.branchName` — Override `BRANCH_NAME` env var
|
|
364
329
|
- `options.repositoryUrl` — Override `REPLAY_REPOSITORY_URL` env var
|
|
365
330
|
- `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
331
|
|
|
368
|
-
### `
|
|
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>`
|
|
369
386
|
|
|
370
|
-
|
|
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.
|
|
371
388
|
|
|
372
389
|
**Parameters:**
|
|
373
|
-
- `
|
|
390
|
+
- `sql` — A Neon SQL tagged-template function
|
|
391
|
+
- `id` — The request UUID
|
|
392
|
+
|
|
393
|
+
### `backendRequestsList(sql, filters?): Promise<BackendRequest[]>`
|
|
394
|
+
|
|
395
|
+
Lists requests ordered by `created_at` DESC, with optional filters.
|
|
396
|
+
|
|
397
|
+
**Parameters:**
|
|
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)
|
|
401
|
+
|
|
402
|
+
### `backendRequestsUpdateStatus(sql, id, status, recordingId?, errorMessage?): Promise<void>`
|
|
403
|
+
|
|
404
|
+
Updates the status of a request. Optionally sets `recording_id` (on success) or `error_message` (on failure).
|
|
405
|
+
|
|
406
|
+
**Parameters:**
|
|
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)
|
|
412
|
+
|
|
413
|
+
### `createRequestRecording(blobUrlOrData, handlerPath, requestInfo): Promise<RecordingResult>`
|
|
414
|
+
|
|
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 |
|
|
427
|
+
|
|
428
|
+
**Parameters:**
|
|
429
|
+
- `blobUrlOrData` — URL to the captured data blob, or pre-parsed `BlobData` object
|
|
430
|
+
- `handlerPath` — Path to the handler module to execute
|
|
431
|
+
- `requestInfo` — The original request info to replay
|
|
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.
|
|
374
451
|
|
|
375
452
|
### `databaseAuditEnsureLogTable(sql): Promise<void>`
|
|
376
453
|
|
|
@@ -403,10 +480,9 @@ These must be set on your Netlify site. Your deploy script should resolve them f
|
|
|
403
480
|
| `COMMIT_SHA` | Git commit hash of the deployed code | `git rev-parse HEAD` |
|
|
404
481
|
| `BRANCH_NAME` | Git branch of the deployed code | `git rev-parse --abbrev-ref HEAD` |
|
|
405
482
|
| `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
483
|
|
|
408
484
|
## How It Works
|
|
409
485
|
|
|
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
|
|
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`.
|
|
411
487
|
|
|
412
|
-
2. **Recording phase**:
|
|
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/dist/index.d.ts
CHANGED
|
@@ -93,66 +93,24 @@ interface BlobData {
|
|
|
93
93
|
handlerResponse?: HandlerResponse$1;
|
|
94
94
|
}
|
|
95
95
|
interface FinishRequestCallbacks {
|
|
96
|
-
/** Uploads serialized captured data and returns the blob URL. */
|
|
97
|
-
uploadBlob: (data: string) => Promise<string>;
|
|
98
96
|
/**
|
|
99
|
-
* Stores
|
|
97
|
+
* Stores the captured request data and returns the request ID.
|
|
100
98
|
*
|
|
101
99
|
* When `requestId` is provided (from `createRecordingRequestHandler`'s
|
|
102
100
|
* `waitUntil` flow), the callback should use it as the row ID so the
|
|
103
|
-
* client-facing header and the stored record match.
|
|
104
|
-
* callback generates its own ID
|
|
101
|
+
* client-facing header and the stored record match. When omitted, the
|
|
102
|
+
* callback generates its own ID.
|
|
105
103
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
storeRequest: (data: {
|
|
105
|
+
blobData: string;
|
|
108
106
|
commitSha: string;
|
|
109
107
|
branchName: string;
|
|
110
108
|
repositoryUrl: string;
|
|
111
109
|
handlerPath: string;
|
|
112
110
|
/** Pre-generated request ID. Use as the row ID when provided. */
|
|
113
111
|
requestId?: string;
|
|
114
|
-
/** Optional secret that restricts access to this request. */
|
|
115
|
-
secret?: string;
|
|
116
112
|
}) => Promise<string>;
|
|
117
113
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Infrastructure credentials required to start a recording container.
|
|
120
|
-
*
|
|
121
|
-
* The container is started on Fly.io via the `@replayio/app-building` package.
|
|
122
|
-
* It requires Infisical credentials (for secrets management inside the
|
|
123
|
-
* container) and a Fly.io token + app name.
|
|
124
|
-
*
|
|
125
|
-
* These must be set as environment variables on the Netlify site:
|
|
126
|
-
* INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET,
|
|
127
|
-
* INFISICAL_PROJECT_ID, INFISICAL_ENVIRONMENT,
|
|
128
|
-
* FLY_API_TOKEN, FLY_APP_NAME
|
|
129
|
-
*/
|
|
130
|
-
interface ContainerInfraConfig {
|
|
131
|
-
infisicalClientId: string;
|
|
132
|
-
infisicalClientSecret: string;
|
|
133
|
-
infisicalProjectId: string;
|
|
134
|
-
infisicalEnvironment: string;
|
|
135
|
-
flyToken: string;
|
|
136
|
-
flyApp: string;
|
|
137
|
-
}
|
|
138
|
-
interface EnsureRecordingOptions {
|
|
139
|
-
repositoryUrl: string;
|
|
140
|
-
/** Infrastructure credentials for starting the recording container. */
|
|
141
|
-
infraConfig?: ContainerInfraConfig;
|
|
142
|
-
/** Webhook URL the container can POST log entries to (optional). */
|
|
143
|
-
webhookUrl?: string;
|
|
144
|
-
/** Looks up request metadata by ID. */
|
|
145
|
-
lookupRequest: (requestId: string) => Promise<{
|
|
146
|
-
blobUrl: string;
|
|
147
|
-
commitSha: string;
|
|
148
|
-
branchName: string;
|
|
149
|
-
handlerPath: string;
|
|
150
|
-
}>;
|
|
151
|
-
/** Updates the request status (and optionally recording ID) in the database. */
|
|
152
|
-
updateStatus: (requestId: string, status: string, recordingId?: string) => Promise<void>;
|
|
153
|
-
/** Optional callback for the caller to emit structured log entries. */
|
|
154
|
-
onLog?: (level: "info" | "warn" | "error", message: string) => Promise<void>;
|
|
155
|
-
}
|
|
156
114
|
|
|
157
115
|
/**
|
|
158
116
|
* Called at the beginning of a Netlify handler execution.
|
|
@@ -212,32 +170,16 @@ interface FinishRequestOptions {
|
|
|
212
170
|
repositoryUrl?: string;
|
|
213
171
|
/**
|
|
214
172
|
* Pre-generated request ID. When provided, this ID is passed to the
|
|
215
|
-
* `
|
|
173
|
+
* `storeRequest` callback so the stored row matches the ID already
|
|
216
174
|
* sent to the client in the `X-Replay-Request-Id` header.
|
|
217
|
-
*
|
|
218
|
-
* Used by `createRecordingRequestHandler` in the `waitUntil` flow where
|
|
219
|
-
* the response is returned before `finishRequest` runs.
|
|
220
175
|
*/
|
|
221
176
|
requestId?: string;
|
|
222
|
-
/**
|
|
223
|
-
* Optional secret string. When set, the stored request is only
|
|
224
|
-
* accessible via API calls that provide the same secret value.
|
|
225
|
-
*/
|
|
226
|
-
secret?: string;
|
|
227
177
|
}
|
|
228
178
|
/**
|
|
229
179
|
* Called at the end of the handler execution.
|
|
230
180
|
* Restores original globals, serializes all captured data,
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* **Important:** The returned response includes the `X-Replay-Request-Id`
|
|
235
|
-
* header. You must send the returned response to the client — not the
|
|
236
|
-
* original response object you passed in.
|
|
237
|
-
*
|
|
238
|
-
* Logs a warning to `console.warn` when the total finishRequest time or
|
|
239
|
-
* individual callback steps exceed their thresholds, to help diagnose
|
|
240
|
-
* slow blob uploads or database writes.
|
|
181
|
+
* stores the request via the provided callback, and sets the
|
|
182
|
+
* X-Replay-Request-Id header.
|
|
241
183
|
*/
|
|
242
184
|
declare function finishRequest(requestContext: RequestContext, callbacks: FinishRequestCallbacks, response: FullHandlerResponse, options?: FinishRequestOptions): Promise<FullHandlerResponse>;
|
|
243
185
|
|
|
@@ -254,80 +196,15 @@ interface CreateRecordingRequestHandlerOptions extends FinishRequestOptions {
|
|
|
254
196
|
*
|
|
255
197
|
* Automatically calls `startRequest` before the handler and `finishRequest`
|
|
256
198
|
* after, capturing all outbound network calls and environment variable reads.
|
|
257
|
-
*
|
|
199
|
+
* The captured data is stored via the provided callbacks.
|
|
258
200
|
*
|
|
259
201
|
* **Response timing:** When the Netlify Functions v2 `context` object is
|
|
260
202
|
* available (with `waitUntil`), the response is returned to the client
|
|
261
203
|
* **immediately** with a pre-generated `X-Replay-Request-Id` header. The
|
|
262
|
-
*
|
|
263
|
-
* `context.waitUntil()`. This avoids adding latency to the client response.
|
|
264
|
-
*
|
|
265
|
-
* When `context.waitUntil` is not available (v1 handlers or missing context),
|
|
266
|
-
* the wrapper falls back to awaiting `finishRequest` before returning.
|
|
267
|
-
*
|
|
268
|
-
* For v2 handlers the request body is read from a clone internally — your
|
|
269
|
-
* handler still receives the original request with an unconsumed body.
|
|
270
|
-
*
|
|
271
|
-
* @example v1 handler (NetlifyEvent)
|
|
272
|
-
* ```typescript
|
|
273
|
-
* import { createRecordingRequestHandler, remoteCallbacks } from "@replayio-app-building/netlify-recorder";
|
|
274
|
-
*
|
|
275
|
-
* const handler = createRecordingRequestHandler(
|
|
276
|
-
* async (event) => {
|
|
277
|
-
* const result = await myBusinessLogic(event.body);
|
|
278
|
-
* return {
|
|
279
|
-
* statusCode: 200,
|
|
280
|
-
* headers: { "Content-Type": "application/json" },
|
|
281
|
-
* body: JSON.stringify(result),
|
|
282
|
-
* };
|
|
283
|
-
* },
|
|
284
|
-
* {
|
|
285
|
-
* callbacks: remoteCallbacks("https://netlify-recorder-bm4wmw.netlify.app"),
|
|
286
|
-
* handlerPath: "netlify/functions/my-handler",
|
|
287
|
-
* }
|
|
288
|
-
* );
|
|
289
|
-
*
|
|
290
|
-
* export { handler };
|
|
291
|
-
* ```
|
|
292
|
-
*
|
|
293
|
-
* @example v2 handler (Web API Request)
|
|
294
|
-
* ```typescript
|
|
295
|
-
* import { createRecordingRequestHandler, remoteCallbacks } from "@replayio-app-building/netlify-recorder";
|
|
296
|
-
*
|
|
297
|
-
* export default createRecordingRequestHandler(
|
|
298
|
-
* async (req) => {
|
|
299
|
-
* // Body is still available — startRequest reads from a clone
|
|
300
|
-
* const body = await (req as Request).json();
|
|
301
|
-
* const result = await myBusinessLogic(body);
|
|
302
|
-
* return {
|
|
303
|
-
* statusCode: 200,
|
|
304
|
-
* headers: { "Content-Type": "application/json" },
|
|
305
|
-
* body: JSON.stringify(result),
|
|
306
|
-
* };
|
|
307
|
-
* },
|
|
308
|
-
* {
|
|
309
|
-
* callbacks: remoteCallbacks("https://netlify-recorder-bm4wmw.netlify.app"),
|
|
310
|
-
* handlerPath: "netlify/functions/my-handler",
|
|
311
|
-
* }
|
|
312
|
-
* );
|
|
313
|
-
* ```
|
|
204
|
+
* data storage continues in the background via `context.waitUntil()`.
|
|
314
205
|
*/
|
|
315
206
|
declare function createRecordingRequestHandler(handler: (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>, options: CreateRecordingRequestHandlerOptions): (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>;
|
|
316
207
|
|
|
317
|
-
/**
|
|
318
|
-
* Creates `FinishRequestCallbacks` that send captured data to a remote
|
|
319
|
-
* Netlify Recorder service. This removes the need for the consuming app
|
|
320
|
-
* to set up its own blob storage or database tables.
|
|
321
|
-
*
|
|
322
|
-
* The callbacks upload blob data and store request metadata via the
|
|
323
|
-
* service's `/api/store-request` endpoint, which handles UploadThing
|
|
324
|
-
* storage and database insertion.
|
|
325
|
-
*
|
|
326
|
-
* @param serviceUrl - Base URL of the Netlify Recorder service
|
|
327
|
-
* (e.g. "https://netlify-recorder-bm4wmw.netlify.app")
|
|
328
|
-
*/
|
|
329
|
-
declare function remoteCallbacks(serviceUrl: string): FinishRequestCallbacks;
|
|
330
|
-
|
|
331
208
|
/**
|
|
332
209
|
* Redacts sensitive environment variable values from blob data.
|
|
333
210
|
*
|
|
@@ -348,28 +225,6 @@ declare function remoteCallbacks(serviceUrl: string): FinishRequestCallbacks;
|
|
|
348
225
|
*/
|
|
349
226
|
declare function redactBlobData(blobData: BlobData): BlobData;
|
|
350
227
|
|
|
351
|
-
/**
|
|
352
|
-
* Called by a background function to convert a request ID into a Replay recording ID.
|
|
353
|
-
*
|
|
354
|
-
* The function:
|
|
355
|
-
* 1. Looks up request metadata (blob URL, commit, handler path).
|
|
356
|
-
* 2. Delegates to `spawnRecordingContainer` which starts a detached Fly.io
|
|
357
|
-
* container, runs the recording script under replay-node, and uploads
|
|
358
|
-
* the resulting recording.
|
|
359
|
-
* 3. Updates the request status with the recording ID.
|
|
360
|
-
*
|
|
361
|
-
* **Required infrastructure:** Infisical credentials and a Fly.io token/app.
|
|
362
|
-
* See `ContainerInfraConfig` in types.ts for details. When these are not
|
|
363
|
-
* configured the function fails with an actionable error message listing
|
|
364
|
-
* the missing environment variables.
|
|
365
|
-
*/
|
|
366
|
-
declare function ensureRequestRecording(requestId: string, options: EnsureRecordingOptions): Promise<string>;
|
|
367
|
-
/**
|
|
368
|
-
* Reads infrastructure config from environment variables.
|
|
369
|
-
* Returns undefined if any required variable is missing.
|
|
370
|
-
*/
|
|
371
|
-
declare function readInfraConfigFromEnv(): ContainerInfraConfig | undefined;
|
|
372
|
-
|
|
373
228
|
interface RecordingResult {
|
|
374
229
|
/** Whether a response mismatch was detected between capture and replay. */
|
|
375
230
|
responseMismatch: boolean;
|
|
@@ -394,39 +249,49 @@ interface RecordingResult {
|
|
|
394
249
|
*/
|
|
395
250
|
declare function createRequestRecording(blobUrlOrData: string | BlobData, handlerPath: string, requestInfo: RequestInfo): Promise<RecordingResult>;
|
|
396
251
|
|
|
252
|
+
type SqlFunction$1 = (...args: any[]) => Promise<any[]>;
|
|
253
|
+
interface BackendRequest {
|
|
254
|
+
id: string;
|
|
255
|
+
blob_data: string;
|
|
256
|
+
handler_path: string;
|
|
257
|
+
commit_sha: string;
|
|
258
|
+
branch_name: string;
|
|
259
|
+
repository_url: string | null;
|
|
260
|
+
status: string;
|
|
261
|
+
recording_id: string | null;
|
|
262
|
+
error_message: string | null;
|
|
263
|
+
created_at: string;
|
|
264
|
+
updated_at: string;
|
|
265
|
+
}
|
|
397
266
|
/**
|
|
398
|
-
*
|
|
399
|
-
*
|
|
267
|
+
* Creates the `backend_requests` table. Call during schema initialization.
|
|
268
|
+
*
|
|
269
|
+
* Each package client stores captured request data in its own database
|
|
270
|
+
* using this table. The blob data (captured network calls, env reads, etc.)
|
|
271
|
+
* is stored directly in the `blob_data` column rather than in external
|
|
272
|
+
* blob storage.
|
|
400
273
|
*/
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
274
|
+
declare function backendRequestsEnsureTable(sql: SqlFunction$1): Promise<void>;
|
|
275
|
+
declare function backendRequestsInsert(sql: SqlFunction$1, data: {
|
|
276
|
+
id?: string;
|
|
277
|
+
blobData: string;
|
|
405
278
|
handlerPath: string;
|
|
406
|
-
/** Git commit SHA to check out inside the container. */
|
|
407
279
|
commitSha: string;
|
|
408
|
-
/** Git branch to clone. */
|
|
409
280
|
branchName: string;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
281
|
+
repositoryUrl?: string | null;
|
|
282
|
+
}): Promise<string>;
|
|
283
|
+
declare function backendRequestsGet(sql: SqlFunction$1, id: string): Promise<BackendRequest | null>;
|
|
284
|
+
declare function backendRequestsGetBlobData(sql: SqlFunction$1, id: string): Promise<string | null>;
|
|
285
|
+
declare function backendRequestsList(sql: SqlFunction$1, filters?: {
|
|
286
|
+
status?: string;
|
|
287
|
+
limit?: number;
|
|
288
|
+
}): Promise<BackendRequest[]>;
|
|
289
|
+
declare function backendRequestsUpdateStatus(sql: SqlFunction$1, id: string, status: string, recordingId?: string, errorMessage?: string): Promise<void>;
|
|
419
290
|
/**
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
* 2. Checks out the exact commit
|
|
423
|
-
* 3. Runs `scripts/create-request-recording.ts` under replay-node
|
|
424
|
-
* 4. Uploads the resulting recording
|
|
425
|
-
* 5. Outputs the recording ID
|
|
426
|
-
*
|
|
427
|
-
* Returns the recording ID on success, or throws on failure.
|
|
291
|
+
* Convenience helper: creates `FinishRequestCallbacks` that store
|
|
292
|
+
* captured request data directly in the `backend_requests` table.
|
|
428
293
|
*/
|
|
429
|
-
declare function
|
|
294
|
+
declare function databaseCallbacks(sql: SqlFunction$1): FinishRequestCallbacks;
|
|
430
295
|
|
|
431
296
|
type SqlFunction = (...args: any[]) => Promise<any[]>;
|
|
432
297
|
/**
|
|
@@ -451,4 +316,4 @@ declare function databaseAuditDumpLogTable(sql: SqlFunction): Promise<Record<str
|
|
|
451
316
|
|
|
452
317
|
declare function getCurrentRequestId(): string | null;
|
|
453
318
|
|
|
454
|
-
export { type
|
|
319
|
+
export { type BackendRequest, type BlobData, type CapturedData, type CreateRecordingRequestHandlerOptions, type EnvRead, type FinishRequestCallbacks, type FinishRequestOptions, type HandlerResponse$1 as HandlerResponse, type NetlifyEvent, type NetlifyV2Request, type NetworkCall, type RecordingResult, type RequestContext, type RequestInfo, backendRequestsEnsureTable, backendRequestsGet, backendRequestsGetBlobData, backendRequestsInsert, backendRequestsList, backendRequestsUpdateStatus, createRecordingRequestHandler, createRequestRecording, databaseAuditDumpLogTable, databaseAuditEnsureLogTable, databaseAuditMonitorTable, databaseCallbacks, finishRequest, getCurrentRequestId, redactBlobData, startRequest };
|
package/dist/index.js
CHANGED
|
@@ -490,7 +490,6 @@ function redactBlobData(blobData) {
|
|
|
490
490
|
|
|
491
491
|
// src/finishRequest.ts
|
|
492
492
|
var SLOW_THRESHOLD_MS = 2e3;
|
|
493
|
-
var SLOW_STEP_THRESHOLD_MS = 1e3;
|
|
494
493
|
async function finishRequest(requestContext, callbacks, response, options) {
|
|
495
494
|
const finishStart = Date.now();
|
|
496
495
|
requestContext.cleanup();
|
|
@@ -535,34 +534,20 @@ async function finishRequest(requestContext, callbacks, response, options) {
|
|
|
535
534
|
};
|
|
536
535
|
const blobData = redactBlobData(rawBlobData);
|
|
537
536
|
const blobContent = JSON.stringify(blobData);
|
|
538
|
-
const uploadStart = Date.now();
|
|
539
|
-
const blobUrl = await callbacks.uploadBlob(blobContent);
|
|
540
|
-
const uploadDuration = Date.now() - uploadStart;
|
|
541
|
-
if (uploadDuration > SLOW_STEP_THRESHOLD_MS) {
|
|
542
|
-
console.warn(
|
|
543
|
-
`netlify-recorder: uploadBlob took ${uploadDuration}ms (handler: ${handlerPath})`
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
537
|
const storeStart = Date.now();
|
|
547
|
-
const storedRequestId = await callbacks.
|
|
548
|
-
|
|
538
|
+
const storedRequestId = await callbacks.storeRequest({
|
|
539
|
+
blobData: blobContent,
|
|
549
540
|
commitSha,
|
|
550
541
|
branchName,
|
|
551
542
|
repositoryUrl,
|
|
552
543
|
handlerPath,
|
|
553
|
-
requestId: options?.requestId
|
|
554
|
-
secret: options?.secret
|
|
544
|
+
requestId: options?.requestId
|
|
555
545
|
});
|
|
556
546
|
const storeDuration = Date.now() - storeStart;
|
|
557
|
-
if (storeDuration > SLOW_STEP_THRESHOLD_MS) {
|
|
558
|
-
console.warn(
|
|
559
|
-
`netlify-recorder: storeRequestData took ${storeDuration}ms (handler: ${handlerPath})`
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
547
|
const totalDuration = Date.now() - finishStart;
|
|
563
548
|
if (totalDuration > SLOW_THRESHOLD_MS) {
|
|
564
549
|
console.warn(
|
|
565
|
-
`netlify-recorder: finishRequest took ${totalDuration}ms total (
|
|
550
|
+
`netlify-recorder: finishRequest took ${totalDuration}ms total (store: ${storeDuration}ms, handler: ${handlerPath})`
|
|
566
551
|
);
|
|
567
552
|
}
|
|
568
553
|
return {
|
|
@@ -618,284 +603,6 @@ function createRecordingRequestHandler(handler, options) {
|
|
|
618
603
|
};
|
|
619
604
|
}
|
|
620
605
|
|
|
621
|
-
// src/remoteCallbacks.ts
|
|
622
|
-
function remoteCallbacks(serviceUrl) {
|
|
623
|
-
const base = serviceUrl.replace(/\/+$/, "");
|
|
624
|
-
let pendingBlobData;
|
|
625
|
-
return {
|
|
626
|
-
uploadBlob: async (data) => {
|
|
627
|
-
pendingBlobData = data;
|
|
628
|
-
return "__pending__";
|
|
629
|
-
},
|
|
630
|
-
storeRequestData: async (metadata) => {
|
|
631
|
-
const blobData = pendingBlobData;
|
|
632
|
-
pendingBlobData = void 0;
|
|
633
|
-
if (!blobData) {
|
|
634
|
-
throw new Error(
|
|
635
|
-
"remoteCallbacks: uploadBlob must be called before storeRequestData"
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
const res = await fetch(
|
|
639
|
-
`${base}/api/store-request`,
|
|
640
|
-
{
|
|
641
|
-
method: "POST",
|
|
642
|
-
headers: { "Content-Type": "application/json" },
|
|
643
|
-
body: JSON.stringify({
|
|
644
|
-
blobData,
|
|
645
|
-
handlerPath: metadata.handlerPath,
|
|
646
|
-
commitSha: metadata.commitSha,
|
|
647
|
-
branchName: metadata.branchName,
|
|
648
|
-
repositoryUrl: metadata.repositoryUrl,
|
|
649
|
-
requestId: metadata.requestId,
|
|
650
|
-
secret: metadata.secret
|
|
651
|
-
})
|
|
652
|
-
}
|
|
653
|
-
);
|
|
654
|
-
if (!res.ok) {
|
|
655
|
-
const errBody = await res.text().catch(() => "(unreadable)");
|
|
656
|
-
throw new Error(
|
|
657
|
-
`Netlify Recorder store-request failed: ${res.status} ${errBody}`
|
|
658
|
-
);
|
|
659
|
-
}
|
|
660
|
-
const result = await res.json();
|
|
661
|
-
return result.requestId;
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// src/spawnRecordingContainer.ts
|
|
667
|
-
async function spawnRecordingContainer(options) {
|
|
668
|
-
const {
|
|
669
|
-
blobUrl,
|
|
670
|
-
handlerPath,
|
|
671
|
-
commitSha,
|
|
672
|
-
branchName,
|
|
673
|
-
repositoryUrl,
|
|
674
|
-
infraConfig,
|
|
675
|
-
logWebhookUrl,
|
|
676
|
-
onLog
|
|
677
|
-
} = options;
|
|
678
|
-
const emit = async (level, message) => {
|
|
679
|
-
if (onLog) {
|
|
680
|
-
try {
|
|
681
|
-
await onLog(level, message);
|
|
682
|
-
} catch {
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
};
|
|
686
|
-
await emit("info", "Logging in to Infisical");
|
|
687
|
-
const {
|
|
688
|
-
infisicalLogin,
|
|
689
|
-
startContainer,
|
|
690
|
-
FileContainerRegistry,
|
|
691
|
-
httpGet,
|
|
692
|
-
httpOptsFor
|
|
693
|
-
} = await import("@replayio/app-building");
|
|
694
|
-
const infisicalToken = await infisicalLogin(
|
|
695
|
-
infraConfig.infisicalClientId,
|
|
696
|
-
infraConfig.infisicalClientSecret
|
|
697
|
-
);
|
|
698
|
-
const infisicalConfig = {
|
|
699
|
-
token: infisicalToken,
|
|
700
|
-
projectId: infraConfig.infisicalProjectId,
|
|
701
|
-
environment: infraConfig.infisicalEnvironment
|
|
702
|
-
};
|
|
703
|
-
const registry = new FileContainerRegistry("/tmp/netlify-recorder-containers.json");
|
|
704
|
-
const initialPrompt = [
|
|
705
|
-
`IMPORTANT: Follow these steps EXACTLY. Run each command as shown. Print ALL output.`,
|
|
706
|
-
`Do NOT explore the codebase, read AGENTS.md, or deviate from these steps.`,
|
|
707
|
-
`Do NOT attempt to debug, fix, shim, or work around ANY errors. If a command fails,`,
|
|
708
|
-
`print the full error output and move on to the next step. Errors during handler replay`,
|
|
709
|
-
`(like "No more recorded network calls" or DB errors) are EXPECTED and harmless.`,
|
|
710
|
-
``,
|
|
711
|
-
`=== Step 1: Install dependencies ===`,
|
|
712
|
-
`cd /repo/apps/netlify-recorder && npm install 2>&1`,
|
|
713
|
-
``,
|
|
714
|
-
`=== Step 2: Checkout the exact commit ===`,
|
|
715
|
-
`git fetch origin ${commitSha} 2>&1 || git fetch --all 2>&1`,
|
|
716
|
-
`git checkout ${commitSha} 2>&1`,
|
|
717
|
-
``,
|
|
718
|
-
`=== Step 3: Verify recording script exists ===`,
|
|
719
|
-
`ls -la /repo/apps/netlify-recorder/scripts/create-request-recording.ts`,
|
|
720
|
-
`If the file does NOT exist, print "ERROR: create-request-recording.ts not found" and STOP.`,
|
|
721
|
-
``,
|
|
722
|
-
`=== Step 4: Pre-compile for replay-node (Node v16) ===`,
|
|
723
|
-
`replay-node is Node v16 \u2014 it cannot run TypeScript or use modern APIs directly.`,
|
|
724
|
-
`You MUST compile everything with esbuild first. Run these commands exactly:`,
|
|
725
|
-
``,
|
|
726
|
-
`# Install undici for web API polyfills (fetch, Headers, Response):`,
|
|
727
|
-
`cd /repo/apps/netlify-recorder && npm install undici@5 2>&1`,
|
|
728
|
-
``,
|
|
729
|
-
`# Create the polyfill loader:`,
|
|
730
|
-
`cat > /tmp/web-polyfill.cjs << 'POLYFILL'`,
|
|
731
|
-
`try {`,
|
|
732
|
-
` var u = require("undici");`,
|
|
733
|
-
` if (!globalThis.fetch) globalThis.fetch = u.fetch;`,
|
|
734
|
-
` if (!globalThis.Headers) globalThis.Headers = u.Headers;`,
|
|
735
|
-
` if (!globalThis.Response) globalThis.Response = u.Response;`,
|
|
736
|
-
` if (!globalThis.Request) globalThis.Request = u.Request;`,
|
|
737
|
-
`} catch(e) { console.error("polyfill warning:", e.message); }`,
|
|
738
|
-
`POLYFILL`,
|
|
739
|
-
``,
|
|
740
|
-
`# Compile the recording script (bundles all local TS dependencies):`,
|
|
741
|
-
`npx esbuild scripts/create-request-recording.ts \\`,
|
|
742
|
-
` --bundle --platform=node --target=node16 --format=cjs \\`,
|
|
743
|
-
` --outfile=/tmp/create-recording.cjs 2>&1`,
|
|
744
|
-
``,
|
|
745
|
-
`# Compile the handler (bundles everything including node_modules):`,
|
|
746
|
-
`npx esbuild ${handlerPath}.ts \\`,
|
|
747
|
-
` --bundle --platform=node --target=node16 --format=cjs \\`,
|
|
748
|
-
` --outfile=/tmp/handler.cjs 2>&1`,
|
|
749
|
-
``,
|
|
750
|
-
`=== Step 5: Run under replay-node ===`,
|
|
751
|
-
`This MUST use replay-node so the execution is recorded. Run exactly:`,
|
|
752
|
-
``,
|
|
753
|
-
`cd /repo/apps/netlify-recorder && npx @replayio/node \\`,
|
|
754
|
-
` -r /tmp/web-polyfill.cjs /tmp/create-recording.cjs \\`,
|
|
755
|
-
` --blob-url '${blobUrl}' \\`,
|
|
756
|
-
` --handler-path '/tmp/handler.cjs' 2>&1`,
|
|
757
|
-
``,
|
|
758
|
-
`The output will show captured data being replayed. Errors like "No more recorded`,
|
|
759
|
-
`network calls" or "DATABASE_URL" errors are EXPECTED \u2014 they come from post-handler`,
|
|
760
|
-
`DB operations that were not in the original blob. Do NOT try to fix these.`,
|
|
761
|
-
``,
|
|
762
|
-
`=== Step 6: Upload the recording ===`,
|
|
763
|
-
`exec-secrets RECORD_REPLAY_API_KEY -- npx replayio upload --all 2>&1`,
|
|
764
|
-
``,
|
|
765
|
-
`Find the recording ID (UUID) in the upload output and print:`,
|
|
766
|
-
` recording: <recording-id>`,
|
|
767
|
-
``,
|
|
768
|
-
`Then output <DONE>.`
|
|
769
|
-
].join("\n");
|
|
770
|
-
await emit("info", "Starting detached container on Fly.io");
|
|
771
|
-
const state = await startContainer(
|
|
772
|
-
{
|
|
773
|
-
infisical: infisicalConfig,
|
|
774
|
-
registry,
|
|
775
|
-
flyToken: infraConfig.flyToken,
|
|
776
|
-
flyApp: infraConfig.flyApp,
|
|
777
|
-
detached: true,
|
|
778
|
-
initialPrompt,
|
|
779
|
-
webhookUrl: logWebhookUrl
|
|
780
|
-
},
|
|
781
|
-
{
|
|
782
|
-
repoUrl: repositoryUrl,
|
|
783
|
-
cloneBranch: branchName
|
|
784
|
-
}
|
|
785
|
-
);
|
|
786
|
-
await emit("info", `Container started: ${state.containerName} at ${state.baseUrl}`);
|
|
787
|
-
const maxWaitMs = 10 * 60 * 1e3;
|
|
788
|
-
const pollIntervalMs = 1e4;
|
|
789
|
-
const deadline = Date.now() + maxWaitMs;
|
|
790
|
-
let containerDone = false;
|
|
791
|
-
while (Date.now() < deadline) {
|
|
792
|
-
try {
|
|
793
|
-
const status = await httpGet(`${state.baseUrl}/status`, httpOptsFor(state));
|
|
794
|
-
if (status?.state === "stopped" || status?.state === "stopping") {
|
|
795
|
-
containerDone = true;
|
|
796
|
-
break;
|
|
797
|
-
}
|
|
798
|
-
} catch {
|
|
799
|
-
containerDone = true;
|
|
800
|
-
break;
|
|
801
|
-
}
|
|
802
|
-
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
803
|
-
}
|
|
804
|
-
if (!containerDone) {
|
|
805
|
-
await emit("warn", "Container did not finish within 10 minutes");
|
|
806
|
-
}
|
|
807
|
-
let recordingId = null;
|
|
808
|
-
try {
|
|
809
|
-
const logs = await httpGet(`${state.baseUrl}/logs?offset=0`, httpOptsFor(state));
|
|
810
|
-
if (typeof logs === "string") {
|
|
811
|
-
const match = logs.match(/recording[:\s]+([a-f0-9-]{36})/i);
|
|
812
|
-
if (match?.[1]) {
|
|
813
|
-
recordingId = match[1];
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
} catch {
|
|
817
|
-
await emit("warn", "Could not read container logs after exit");
|
|
818
|
-
}
|
|
819
|
-
if (!recordingId) {
|
|
820
|
-
await emit("error", "Container completed but no recording ID was found in output");
|
|
821
|
-
throw new Error("Recording creation failed: no recording ID returned from container");
|
|
822
|
-
}
|
|
823
|
-
await emit("info", `Container completed \u2014 recording ID: ${recordingId}`);
|
|
824
|
-
return recordingId;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// src/ensureRequestRecording.ts
|
|
828
|
-
async function ensureRequestRecording(requestId, options) {
|
|
829
|
-
const { repositoryUrl, infraConfig, webhookUrl, lookupRequest, updateStatus, onLog } = options;
|
|
830
|
-
const emit = async (level, message) => {
|
|
831
|
-
if (onLog) {
|
|
832
|
-
try {
|
|
833
|
-
await onLog(level, message);
|
|
834
|
-
} catch {
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
};
|
|
838
|
-
await updateStatus(requestId, "processing");
|
|
839
|
-
try {
|
|
840
|
-
if (!infraConfig) {
|
|
841
|
-
const missing = getMissingInfraVars();
|
|
842
|
-
throw new Error(
|
|
843
|
-
`Container infrastructure not configured. Missing environment variables: ${missing.join(", ")}. These must be set on the Netlify site for recording creation to work.`
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
const requestData = await lookupRequest(requestId);
|
|
847
|
-
await emit("info", `Request data retrieved \u2014 handler: ${requestData.handlerPath}, branch: ${requestData.branchName}, commit: ${requestData.commitSha}`);
|
|
848
|
-
const recordingId = await spawnRecordingContainer({
|
|
849
|
-
blobUrl: requestData.blobUrl,
|
|
850
|
-
handlerPath: requestData.handlerPath,
|
|
851
|
-
commitSha: requestData.commitSha,
|
|
852
|
-
branchName: requestData.branchName,
|
|
853
|
-
repositoryUrl,
|
|
854
|
-
infraConfig,
|
|
855
|
-
logWebhookUrl: webhookUrl,
|
|
856
|
-
onLog
|
|
857
|
-
});
|
|
858
|
-
await emit("info", `Recording created successfully: ${recordingId}`);
|
|
859
|
-
await updateStatus(requestId, "recorded", recordingId);
|
|
860
|
-
return recordingId;
|
|
861
|
-
} catch (err) {
|
|
862
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
863
|
-
await emit("error", `Recording creation failed: ${message}`);
|
|
864
|
-
await updateStatus(requestId, "failed");
|
|
865
|
-
throw err;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
function getMissingInfraVars() {
|
|
869
|
-
const required = [
|
|
870
|
-
"INFISICAL_CLIENT_ID",
|
|
871
|
-
"INFISICAL_CLIENT_SECRET",
|
|
872
|
-
"INFISICAL_PROJECT_ID",
|
|
873
|
-
"INFISICAL_ENVIRONMENT",
|
|
874
|
-
"FLY_API_TOKEN",
|
|
875
|
-
"FLY_APP_NAME"
|
|
876
|
-
];
|
|
877
|
-
return required.filter((name) => !process.env[name]);
|
|
878
|
-
}
|
|
879
|
-
function readInfraConfigFromEnv() {
|
|
880
|
-
const clientId = process.env.INFISICAL_CLIENT_ID;
|
|
881
|
-
const clientSecret = process.env.INFISICAL_CLIENT_SECRET;
|
|
882
|
-
const projectId = process.env.INFISICAL_PROJECT_ID;
|
|
883
|
-
const environment = process.env.INFISICAL_ENVIRONMENT;
|
|
884
|
-
const flyToken = process.env.FLY_API_TOKEN;
|
|
885
|
-
const flyApp = process.env.FLY_APP_NAME;
|
|
886
|
-
if (!clientId || !clientSecret || !projectId || !environment || !flyToken || !flyApp) {
|
|
887
|
-
return void 0;
|
|
888
|
-
}
|
|
889
|
-
return {
|
|
890
|
-
infisicalClientId: clientId,
|
|
891
|
-
infisicalClientSecret: clientSecret,
|
|
892
|
-
infisicalProjectId: projectId,
|
|
893
|
-
infisicalEnvironment: environment,
|
|
894
|
-
flyToken,
|
|
895
|
-
flyApp
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
|
|
899
606
|
// src/createRequestRecording.ts
|
|
900
607
|
async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
|
|
901
608
|
let blobData;
|
|
@@ -1203,6 +910,131 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
|
|
|
1203
910
|
return result;
|
|
1204
911
|
}
|
|
1205
912
|
|
|
913
|
+
// src/backendRequests.ts
|
|
914
|
+
async function backendRequestsEnsureTable(sql) {
|
|
915
|
+
await sql`
|
|
916
|
+
CREATE TABLE IF NOT EXISTS backend_requests (
|
|
917
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
918
|
+
blob_data TEXT NOT NULL,
|
|
919
|
+
handler_path TEXT NOT NULL,
|
|
920
|
+
commit_sha TEXT NOT NULL,
|
|
921
|
+
branch_name TEXT NOT NULL DEFAULT 'main',
|
|
922
|
+
repository_url TEXT,
|
|
923
|
+
status TEXT NOT NULL DEFAULT 'captured'
|
|
924
|
+
CHECK (status IN ('captured', 'queued', 'processing', 'recorded', 'failed')),
|
|
925
|
+
recording_id TEXT,
|
|
926
|
+
error_message TEXT,
|
|
927
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
928
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
929
|
+
)
|
|
930
|
+
`;
|
|
931
|
+
await sql`
|
|
932
|
+
CREATE INDEX IF NOT EXISTS idx_backend_requests_status ON backend_requests (status)
|
|
933
|
+
`;
|
|
934
|
+
await sql`
|
|
935
|
+
CREATE INDEX IF NOT EXISTS idx_backend_requests_created_at ON backend_requests (created_at DESC)
|
|
936
|
+
`;
|
|
937
|
+
}
|
|
938
|
+
async function backendRequestsInsert(sql, data) {
|
|
939
|
+
if (data.id) {
|
|
940
|
+
await sql`
|
|
941
|
+
INSERT INTO backend_requests (id, blob_data, handler_path, commit_sha, branch_name, repository_url)
|
|
942
|
+
VALUES (
|
|
943
|
+
${data.id}::uuid,
|
|
944
|
+
${data.blobData},
|
|
945
|
+
${data.handlerPath},
|
|
946
|
+
${data.commitSha},
|
|
947
|
+
${data.branchName},
|
|
948
|
+
${data.repositoryUrl ?? null}
|
|
949
|
+
)
|
|
950
|
+
`;
|
|
951
|
+
return data.id;
|
|
952
|
+
}
|
|
953
|
+
const rows = await sql`
|
|
954
|
+
INSERT INTO backend_requests (blob_data, handler_path, commit_sha, branch_name, repository_url)
|
|
955
|
+
VALUES (
|
|
956
|
+
${data.blobData},
|
|
957
|
+
${data.handlerPath},
|
|
958
|
+
${data.commitSha},
|
|
959
|
+
${data.branchName},
|
|
960
|
+
${data.repositoryUrl ?? null}
|
|
961
|
+
)
|
|
962
|
+
RETURNING id
|
|
963
|
+
`;
|
|
964
|
+
return rows[0]?.id ?? "";
|
|
965
|
+
}
|
|
966
|
+
async function backendRequestsGet(sql, id) {
|
|
967
|
+
const rows = await sql`
|
|
968
|
+
SELECT id, blob_data, handler_path, commit_sha, branch_name, repository_url,
|
|
969
|
+
status, recording_id, error_message, created_at, updated_at
|
|
970
|
+
FROM backend_requests WHERE id = ${id}
|
|
971
|
+
`;
|
|
972
|
+
return rows[0] ?? null;
|
|
973
|
+
}
|
|
974
|
+
async function backendRequestsGetBlobData(sql, id) {
|
|
975
|
+
const rows = await sql`
|
|
976
|
+
SELECT blob_data FROM backend_requests WHERE id = ${id}
|
|
977
|
+
`;
|
|
978
|
+
return rows[0]?.blob_data ?? null;
|
|
979
|
+
}
|
|
980
|
+
async function backendRequestsList(sql, filters) {
|
|
981
|
+
const limit = filters?.limit ?? 50;
|
|
982
|
+
if (filters?.status) {
|
|
983
|
+
const rows2 = await sql`
|
|
984
|
+
SELECT id, blob_data, handler_path, commit_sha, branch_name, repository_url,
|
|
985
|
+
status, recording_id, error_message, created_at, updated_at
|
|
986
|
+
FROM backend_requests
|
|
987
|
+
WHERE status = ${filters.status}
|
|
988
|
+
ORDER BY created_at DESC
|
|
989
|
+
LIMIT ${limit}
|
|
990
|
+
`;
|
|
991
|
+
return rows2;
|
|
992
|
+
}
|
|
993
|
+
const rows = await sql`
|
|
994
|
+
SELECT id, blob_data, handler_path, commit_sha, branch_name, repository_url,
|
|
995
|
+
status, recording_id, error_message, created_at, updated_at
|
|
996
|
+
FROM backend_requests
|
|
997
|
+
ORDER BY created_at DESC
|
|
998
|
+
LIMIT ${limit}
|
|
999
|
+
`;
|
|
1000
|
+
return rows;
|
|
1001
|
+
}
|
|
1002
|
+
async function backendRequestsUpdateStatus(sql, id, status, recordingId, errorMessage) {
|
|
1003
|
+
if (recordingId) {
|
|
1004
|
+
await sql`
|
|
1005
|
+
UPDATE backend_requests
|
|
1006
|
+
SET status = ${status}, recording_id = ${recordingId}, updated_at = NOW()
|
|
1007
|
+
WHERE id = ${id}
|
|
1008
|
+
`;
|
|
1009
|
+
} else if (errorMessage) {
|
|
1010
|
+
await sql`
|
|
1011
|
+
UPDATE backend_requests
|
|
1012
|
+
SET status = ${status}, error_message = ${errorMessage}, updated_at = NOW()
|
|
1013
|
+
WHERE id = ${id}
|
|
1014
|
+
`;
|
|
1015
|
+
} else {
|
|
1016
|
+
await sql`
|
|
1017
|
+
UPDATE backend_requests
|
|
1018
|
+
SET status = ${status}, updated_at = NOW()
|
|
1019
|
+
WHERE id = ${id}
|
|
1020
|
+
`;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function databaseCallbacks(sql) {
|
|
1024
|
+
return {
|
|
1025
|
+
storeRequest: async (data) => {
|
|
1026
|
+
return backendRequestsInsert(sql, {
|
|
1027
|
+
id: data.requestId,
|
|
1028
|
+
blobData: data.blobData,
|
|
1029
|
+
handlerPath: data.handlerPath,
|
|
1030
|
+
commitSha: data.commitSha,
|
|
1031
|
+
branchName: data.branchName,
|
|
1032
|
+
repositoryUrl: data.repositoryUrl
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1206
1038
|
// src/databaseAudit.ts
|
|
1207
1039
|
async function databaseAuditEnsureLogTable(sql) {
|
|
1208
1040
|
await sql`
|
|
@@ -1283,17 +1115,20 @@ async function databaseAuditDumpLogTable(sql) {
|
|
|
1283
1115
|
return rows;
|
|
1284
1116
|
}
|
|
1285
1117
|
export {
|
|
1118
|
+
backendRequestsEnsureTable,
|
|
1119
|
+
backendRequestsGet,
|
|
1120
|
+
backendRequestsGetBlobData,
|
|
1121
|
+
backendRequestsInsert,
|
|
1122
|
+
backendRequestsList,
|
|
1123
|
+
backendRequestsUpdateStatus,
|
|
1286
1124
|
createRecordingRequestHandler,
|
|
1287
1125
|
createRequestRecording,
|
|
1288
1126
|
databaseAuditDumpLogTable,
|
|
1289
1127
|
databaseAuditEnsureLogTable,
|
|
1290
1128
|
databaseAuditMonitorTable,
|
|
1291
|
-
|
|
1129
|
+
databaseCallbacks,
|
|
1292
1130
|
finishRequest,
|
|
1293
1131
|
getCurrentRequestId,
|
|
1294
|
-
readInfraConfigFromEnv,
|
|
1295
1132
|
redactBlobData,
|
|
1296
|
-
remoteCallbacks,
|
|
1297
|
-
spawnRecordingContainer,
|
|
1298
1133
|
startRequest
|
|
1299
1134
|
};
|