@replayio-app-building/netlify-recorder 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 in your app's own database, and can later reproduce the exact execution as a Replay recording for debugging and analysis.
3
+ Capture and replay Netlify function executions as [Replay](https://replay.io) recordings. This package intercepts outbound network calls and environment variable reads during handler execution, uploads the captured data to UploadThing, stores the URL in your app's database, and can later reproduce the exact execution as a Replay recording for debugging and analysis.
4
4
 
5
5
  ## Installation
6
6
 
@@ -20,19 +20,20 @@ import { backendRequestsEnsureTable } from "@replayio-app-building/netlify-recor
20
20
  await backendRequestsEnsureTable(sql);
21
21
  ```
22
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.
23
+ This creates a table with columns for the blob data URL (UploadThing), git metadata (commit SHA, branch, repository URL), handler path, recording status, and timestamps.
24
24
 
25
25
  ### 2. Set required environment variables
26
26
 
27
- `finishRequest` needs to know which repository, branch, and commit your deployed code belongs to. Set these environment variables on your Netlify site:
27
+ Set these environment variables on your Netlify site:
28
28
 
29
29
  | Variable | Description | How to set |
30
30
  |---|---|---|
31
31
  | `REPLAY_REPOSITORY_URL` | Your app's git repository URL (e.g. `https://github.com/org/repo.git`) | Set in your deploy script or Netlify site settings |
32
32
  | `COMMIT_SHA` | The git commit hash of the deployed code | Set in your deploy script via `git rev-parse HEAD` |
33
33
  | `BRANCH_NAME` | The git branch of the deployed code | Set in your deploy script via `git rev-parse --abbrev-ref HEAD` |
34
+ | `UPLOADTHING_TOKEN` | UploadThing API token for blob data storage | Set in Netlify site settings |
34
35
 
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:
36
+ The first three are **required** by `finishRequest` (it will throw if missing). `UPLOADTHING_TOKEN` is required by `databaseCallbacks` to upload captured blob data. Your deploy script should resolve the git values and set them on the Netlify site before deploying. Example:
36
37
 
37
38
  ```typescript
38
39
  // In your deploy script:
@@ -46,7 +47,7 @@ const repositoryUrl = execSync("git remote get-url origin", { encoding: "utf-8"
46
47
 
47
48
  ### 3. Wrap your Netlify function
48
49
 
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.
50
+ Use `createRecordingRequestHandler` with `databaseCallbacks(sql)` to wrap your handler with automatic request capture. The captured data is uploaded to UploadThing and the URL is stored in the `backend_requests` table.
50
51
 
51
52
  **v1 handler** (Netlify Functions v1 — `event` with `httpMethod`, `path`, etc.):
52
53
 
@@ -108,13 +109,13 @@ export default createRecordingRequestHandler(
108
109
  );
109
110
  ```
110
111
 
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.
112
+ `createRecordingRequestHandler` automatically captures all outbound network calls and environment variable reads during your handler's execution, uploads the captured data to UploadThing, and stores the URL in the `backend_requests` table. The response includes an `X-Replay-Request-Id` header with the ID of the stored request.
112
113
 
113
114
  > **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.
114
115
 
115
116
  ### 4. Create recordings via the Netlify Recorder service
116
117
 
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"`.
118
+ 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 passes the stored blob data URL to the Netlify Recorder service and updates the row status to `"queued"`.
118
119
 
119
120
  ```typescript
120
121
  import { ensureRequestRecording } from "@replayio-app-building/netlify-recorder";
@@ -137,16 +138,6 @@ The function is idempotent — calling it multiple times for the same request is
137
138
  - **`status: "queued"` or `"processing"`** — returns `null` without re-queuing
138
139
  - **`status: "captured"` or `"failed"`** — calls the service, updates status to `"queued"`, returns `null`
139
140
 
140
- The `blobDataUrl` must be a direct URL to the blob JSON (e.g. an UploadThing URL). The recording container fetches this URL directly — no callback to the recorder service is involved:
141
-
142
- ```typescript
143
- const recordingId = await ensureRequestRecording(sql, requestId, {
144
- recorderUrl: RECORDER_URL,
145
- blobDataUrl: `https://utfs.io/f/${blobFileKey}`, // direct URL to stored blob data
146
- webhookUrl: "https://your-app.netlify.app/api/on-recording-complete", // optional
147
- });
148
- ```
149
-
150
141
  When `webhookUrl` is provided, the service POSTs the result when the recording completes:
151
142
 
152
143
  On success:
@@ -324,12 +315,14 @@ Logs a `console.warn` when the total duration exceeds 2 seconds to help diagnose
324
315
 
325
316
  ### `databaseCallbacks(sql): FinishRequestCallbacks`
326
317
 
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`.
318
+ Creates a `FinishRequestCallbacks` object that uploads captured request data to UploadThing and stores the URL in the `backend_requests` table. This is the standard way to provide callbacks to `createRecordingRequestHandler` or `finishRequest`.
319
+
320
+ Requires `UPLOADTHING_TOKEN` environment variable and the `uploadthing` package.
328
321
 
329
322
  **Parameters:**
330
323
  - `sql` — A Neon SQL tagged-template function
331
324
 
332
- **Returns:** An object with a `storeRequest` method that inserts rows into `backend_requests`.
325
+ **Returns:** An object with a `storeRequest` method that uploads blob data and inserts rows into `backend_requests`.
333
326
 
334
327
  ### `backendRequestsEnsureTable(sql): Promise<void>`
335
328
 
@@ -340,7 +333,7 @@ The table schema:
340
333
  | Column | Type | Description |
341
334
  |---|---|---|
342
335
  | `id` | UUID (PK) | Auto-generated request ID |
343
- | `blob_data` | TEXT | Serialized JSON of captured request data |
336
+ | `blob_data_url` | TEXT | URL to the uploaded blob JSON (UploadThing) |
344
337
  | `handler_path` | TEXT | Path to the handler file |
345
338
  | `commit_sha` | TEXT | Git commit SHA |
346
339
  | `branch_name` | TEXT | Git branch name (default: `'main'`) |
@@ -360,7 +353,7 @@ Inserts a new row into `backend_requests` and returns the generated (or provided
360
353
 
361
354
  **Parameters:**
362
355
  - `sql` — A Neon SQL tagged-template function
363
- - `data.blobData` — Serialized blob JSON string
356
+ - `data.blobDataUrl` — URL to the uploaded blob JSON (e.g. UploadThing URL)
364
357
  - `data.handlerPath` — Handler file path
365
358
  - `data.commitSha` — Git commit SHA
366
359
  - `data.branchName` — Git branch name
@@ -375,9 +368,9 @@ Retrieves a single request by ID, or `null` if not found.
375
368
  - `sql` — A Neon SQL tagged-template function
376
369
  - `id` — The request UUID
377
370
 
378
- ### `backendRequestsGetBlobData(sql, id): Promise<string | null>`
371
+ ### `backendRequestsGetBlobUrl(sql, id): Promise<string | null>`
379
372
 
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.
373
+ Retrieves only the `blob_data_url` column for a request, or `null` if not found.
381
374
 
382
375
  **Parameters:**
383
376
  - `sql` — A Neon SQL tagged-template function
@@ -411,7 +404,6 @@ Ensures a Replay recording exists (or is being created) for a backend request. L
411
404
  - `sql` — A Neon SQL tagged-template function
412
405
  - `requestId` — The backend request UUID
413
406
  - `options.recorderUrl` — Base URL of the Netlify Recorder service
414
- - `options.blobDataUrl` — Direct URL where the recording container fetches the blob JSON (e.g. an UploadThing URL)
415
407
  - `options.webhookUrl` — URL to POST the result to when the recording completes or fails
416
408
 
417
409
  **Returns:** The recording ID (`string`) if the request is already recorded, or `null` if the recording was queued or is in progress.
package/dist/index.d.ts CHANGED
@@ -234,7 +234,7 @@ declare function createRequestRecording(blobUrlOrData: string | BlobData, handle
234
234
  type SqlFunction$1 = (...args: any[]) => Promise<any[]>;
235
235
  interface BackendRequest {
236
236
  id: string;
237
- blob_data: string;
237
+ blob_data_url: string;
238
238
  handler_path: string;
239
239
  commit_sha: string;
240
240
  branch_name: string;
@@ -250,39 +250,35 @@ interface BackendRequest {
250
250
  *
251
251
  * Each package client stores captured request data in its own database
252
252
  * using this table. The blob data (captured network calls, env reads, etc.)
253
- * is stored directly in the `blob_data` column rather than in external
254
- * blob storage.
253
+ * is uploaded to UploadThing at capture time and only the URL is stored.
255
254
  */
256
255
  declare function backendRequestsEnsureTable(sql: SqlFunction$1): Promise<void>;
257
256
  declare function backendRequestsInsert(sql: SqlFunction$1, data: {
258
257
  id?: string;
259
- blobData: string;
258
+ blobDataUrl: string;
260
259
  handlerPath: string;
261
260
  commitSha: string;
262
261
  branchName: string;
263
262
  repositoryUrl?: string | null;
264
263
  }): Promise<string>;
265
264
  declare function backendRequestsGet(sql: SqlFunction$1, id: string): Promise<BackendRequest | null>;
266
- declare function backendRequestsGetBlobData(sql: SqlFunction$1, id: string): Promise<string | null>;
265
+ declare function backendRequestsGetBlobUrl(sql: SqlFunction$1, id: string): Promise<string | null>;
267
266
  declare function backendRequestsList(sql: SqlFunction$1, filters?: {
268
267
  status?: string;
269
268
  limit?: number;
270
269
  }): Promise<BackendRequest[]>;
271
270
  declare function backendRequestsUpdateStatus(sql: SqlFunction$1, id: string, status: string, recordingId?: string, errorMessage?: string): Promise<void>;
272
271
  /**
273
- * Convenience helper: creates `FinishRequestCallbacks` that store
274
- * captured request data directly in the `backend_requests` table.
272
+ * Convenience helper: creates `FinishRequestCallbacks` that upload
273
+ * captured request data to UploadThing and store the URL in the
274
+ * `backend_requests` table.
275
+ *
276
+ * Requires `UPLOADTHING_TOKEN` environment variable and the `uploadthing` package.
275
277
  */
276
278
  declare function databaseCallbacks(sql: SqlFunction$1): FinishRequestCallbacks;
277
279
  interface EnsureRequestRecordingOptions {
278
280
  /** Base URL of the Netlify Recorder service (e.g. "https://netlify-recorder-bm4wmw.netlify.app"). */
279
281
  recorderUrl: string;
280
- /**
281
- * Full URL where the recording container can fetch the blob JSON for this request.
282
- * Must be a direct URL to the blob data (e.g. an UploadThing URL). The container
283
- * fetches this URL directly — no callback to the recorder service is involved.
284
- */
285
- blobDataUrl: string;
286
282
  /** URL to POST the result to when the recording completes or fails. */
287
283
  webhookUrl?: string;
288
284
  }
@@ -292,8 +288,9 @@ interface EnsureRequestRecordingOptions {
292
288
  * 1. Looks up the request in `backend_requests`.
293
289
  * 2. If a recording already exists (`status === "recorded"`), returns the recording ID immediately.
294
290
  * 3. If the request is already queued or processing, returns `null` without re-queuing.
295
- * 4. Otherwise, calls the Netlify Recorder service's `create-recording` endpoint,
296
- * updates the row to `"queued"`, and returns `null`.
291
+ * 4. Otherwise, calls the Netlify Recorder service's `create-recording` endpoint
292
+ * with the blob data URL from the stored request, updates the row to `"queued"`,
293
+ * and returns `null`.
297
294
  *
298
295
  * This function is idempotent — calling it multiple times for the same request
299
296
  * is safe. Once the recording completes, subsequent calls return the recording ID.
@@ -323,4 +320,4 @@ declare function databaseAuditDumpLogTable(sql: SqlFunction): Promise<Record<str
323
320
 
324
321
  declare function getCurrentRequestId(): string | null;
325
322
 
326
- export { type BackendRequest, type BlobData, type CapturedData, type CreateRecordingRequestHandlerOptions, type EnsureRequestRecordingOptions, 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, ensureRequestRecording, finishRequest, getCurrentRequestId, redactBlobData, startRequest };
323
+ export { type BackendRequest, type BlobData, type CapturedData, type CreateRecordingRequestHandlerOptions, type EnsureRequestRecordingOptions, 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, backendRequestsGetBlobUrl, backendRequestsInsert, backendRequestsList, backendRequestsUpdateStatus, createRecordingRequestHandler, createRequestRecording, databaseAuditDumpLogTable, databaseAuditEnsureLogTable, databaseAuditMonitorTable, databaseCallbacks, ensureRequestRecording, finishRequest, getCurrentRequestId, redactBlobData, startRequest };
package/dist/index.js CHANGED
@@ -563,10 +563,10 @@ async function finishRequest(requestContext, callbacks, response, options) {
563
563
  }
564
564
 
565
565
  // src/createRecordingRequestHandler.ts
566
- import crypto from "crypto";
566
+ import crypto2 from "crypto";
567
567
  function createRecordingRequestHandler(handler, options) {
568
568
  return async (event, context) => {
569
- const requestId = crypto.randomUUID();
569
+ const requestId = crypto2.randomUUID();
570
570
  setCurrentRequestId(requestId);
571
571
  const reqContext = startRequest(event);
572
572
  let response;
@@ -944,7 +944,7 @@ async function backendRequestsEnsureTable(sql) {
944
944
  await sql`
945
945
  CREATE TABLE IF NOT EXISTS backend_requests (
946
946
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
947
- blob_data TEXT NOT NULL,
947
+ blob_data_url TEXT NOT NULL,
948
948
  handler_path TEXT NOT NULL,
949
949
  commit_sha TEXT NOT NULL,
950
950
  branch_name TEXT NOT NULL DEFAULT 'main',
@@ -967,10 +967,10 @@ async function backendRequestsEnsureTable(sql) {
967
967
  async function backendRequestsInsert(sql, data) {
968
968
  if (data.id) {
969
969
  await sql`
970
- INSERT INTO backend_requests (id, blob_data, handler_path, commit_sha, branch_name, repository_url)
970
+ INSERT INTO backend_requests (id, blob_data_url, handler_path, commit_sha, branch_name, repository_url)
971
971
  VALUES (
972
972
  ${data.id}::uuid,
973
- ${data.blobData},
973
+ ${data.blobDataUrl},
974
974
  ${data.handlerPath},
975
975
  ${data.commitSha},
976
976
  ${data.branchName},
@@ -980,9 +980,9 @@ async function backendRequestsInsert(sql, data) {
980
980
  return data.id;
981
981
  }
982
982
  const rows = await sql`
983
- INSERT INTO backend_requests (blob_data, handler_path, commit_sha, branch_name, repository_url)
983
+ INSERT INTO backend_requests (blob_data_url, handler_path, commit_sha, branch_name, repository_url)
984
984
  VALUES (
985
- ${data.blobData},
985
+ ${data.blobDataUrl},
986
986
  ${data.handlerPath},
987
987
  ${data.commitSha},
988
988
  ${data.branchName},
@@ -994,23 +994,23 @@ async function backendRequestsInsert(sql, data) {
994
994
  }
995
995
  async function backendRequestsGet(sql, id) {
996
996
  const rows = await sql`
997
- SELECT id, blob_data, handler_path, commit_sha, branch_name, repository_url,
997
+ SELECT id, blob_data_url, handler_path, commit_sha, branch_name, repository_url,
998
998
  status, recording_id, error_message, created_at, updated_at
999
999
  FROM backend_requests WHERE id = ${id}
1000
1000
  `;
1001
1001
  return rows[0] ?? null;
1002
1002
  }
1003
- async function backendRequestsGetBlobData(sql, id) {
1003
+ async function backendRequestsGetBlobUrl(sql, id) {
1004
1004
  const rows = await sql`
1005
- SELECT blob_data FROM backend_requests WHERE id = ${id}
1005
+ SELECT blob_data_url FROM backend_requests WHERE id = ${id}
1006
1006
  `;
1007
- return rows[0]?.blob_data ?? null;
1007
+ return rows[0]?.blob_data_url ?? null;
1008
1008
  }
1009
1009
  async function backendRequestsList(sql, filters) {
1010
1010
  const limit = filters?.limit ?? 50;
1011
1011
  if (filters?.status) {
1012
1012
  const rows2 = await sql`
1013
- SELECT id, blob_data, handler_path, commit_sha, branch_name, repository_url,
1013
+ SELECT id, blob_data_url, handler_path, commit_sha, branch_name, repository_url,
1014
1014
  status, recording_id, error_message, created_at, updated_at
1015
1015
  FROM backend_requests
1016
1016
  WHERE status = ${filters.status}
@@ -1020,7 +1020,7 @@ async function backendRequestsList(sql, filters) {
1020
1020
  return rows2;
1021
1021
  }
1022
1022
  const rows = await sql`
1023
- SELECT id, blob_data, handler_path, commit_sha, branch_name, repository_url,
1023
+ SELECT id, blob_data_url, handler_path, commit_sha, branch_name, repository_url,
1024
1024
  status, recording_id, error_message, created_at, updated_at
1025
1025
  FROM backend_requests
1026
1026
  ORDER BY created_at DESC
@@ -1049,12 +1049,42 @@ async function backendRequestsUpdateStatus(sql, id, status, recordingId, errorMe
1049
1049
  `;
1050
1050
  }
1051
1051
  }
1052
+ async function uploadBlobData(blobData, requestId) {
1053
+ let UTApi;
1054
+ try {
1055
+ ({ UTApi } = await Function('return import("uploadthing/server")')());
1056
+ } catch {
1057
+ throw new Error(
1058
+ "netlify-recorder requires the 'uploadthing' package. Install it with: npm install uploadthing"
1059
+ );
1060
+ }
1061
+ const token = process.env.UPLOADTHING_TOKEN;
1062
+ if (!token) {
1063
+ throw new Error(
1064
+ "netlify-recorder: UPLOADTHING_TOKEN environment variable is required to upload blob data"
1065
+ );
1066
+ }
1067
+ const utapi = new UTApi({ token });
1068
+ const file = new File(
1069
+ [blobData],
1070
+ `blob-${requestId}.json`,
1071
+ { type: "application/json" }
1072
+ );
1073
+ const result = await utapi.uploadFiles(file);
1074
+ if (result.error || !result.data?.ufsUrl) {
1075
+ throw new Error(
1076
+ `Failed to upload blob data for request ${requestId}: ${JSON.stringify(result.error ?? "no URL returned")}`
1077
+ );
1078
+ }
1079
+ return result.data.ufsUrl;
1080
+ }
1052
1081
  function databaseCallbacks(sql) {
1053
1082
  return {
1054
1083
  storeRequest: async (data) => {
1084
+ const blobDataUrl = await uploadBlobData(data.blobData, data.requestId ?? crypto.randomUUID());
1055
1085
  return backendRequestsInsert(sql, {
1056
1086
  id: data.requestId,
1057
- blobData: data.blobData,
1087
+ blobDataUrl,
1058
1088
  handlerPath: data.handlerPath,
1059
1089
  commitSha: data.commitSha,
1060
1090
  branchName: data.branchName,
@@ -1079,7 +1109,7 @@ async function ensureRequestRecording(sql, requestId, options) {
1079
1109
  method: "POST",
1080
1110
  headers: { "Content-Type": "application/json" },
1081
1111
  body: JSON.stringify({
1082
- blobDataUrl: options.blobDataUrl,
1112
+ blobDataUrl: request.blob_data_url,
1083
1113
  handlerPath: request.handler_path,
1084
1114
  commitSha: request.commit_sha,
1085
1115
  branchName: request.branch_name,
@@ -1190,7 +1220,7 @@ async function databaseAuditDumpLogTable(sql) {
1190
1220
  export {
1191
1221
  backendRequestsEnsureTable,
1192
1222
  backendRequestsGet,
1193
- backendRequestsGetBlobData,
1223
+ backendRequestsGetBlobUrl,
1194
1224
  backendRequestsInsert,
1195
1225
  backendRequestsList,
1196
1226
  backendRequestsUpdateStatus,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,11 +18,15 @@
18
18
  "prepublishOnly": "tsup"
19
19
  },
20
20
  "peerDependencies": {
21
- "@replayio/app-building": ">=1.0.0"
21
+ "@replayio/app-building": ">=1.0.0",
22
+ "uploadthing": ">=7.0.0"
22
23
  },
23
24
  "peerDependenciesMeta": {
24
25
  "@replayio/app-building": {
25
26
  "optional": true
27
+ },
28
+ "uploadthing": {
29
+ "optional": true
26
30
  }
27
31
  },
28
32
  "devDependencies": {