@replayio-app-building/netlify-recorder 0.26.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,8 +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 function reads the `blob_data` from `backend_requests`, uploads it to UploadThing, and passes the URL to the recorder service. This requires the `uploadthing` package and `UPLOADTHING_TOKEN` environment variable.
141
-
142
141
  When `webhookUrl` is provided, the service POSTs the result when the recording completes:
143
142
 
144
143
  On success:
@@ -316,12 +315,14 @@ Logs a `console.warn` when the total duration exceeds 2 seconds to help diagnose
316
315
 
317
316
  ### `databaseCallbacks(sql): FinishRequestCallbacks`
318
317
 
319
- 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.
320
321
 
321
322
  **Parameters:**
322
323
  - `sql` — A Neon SQL tagged-template function
323
324
 
324
- **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`.
325
326
 
326
327
  ### `backendRequestsEnsureTable(sql): Promise<void>`
327
328
 
@@ -332,7 +333,7 @@ The table schema:
332
333
  | Column | Type | Description |
333
334
  |---|---|---|
334
335
  | `id` | UUID (PK) | Auto-generated request ID |
335
- | `blob_data` | TEXT | Serialized JSON of captured request data |
336
+ | `blob_data_url` | TEXT | URL to the uploaded blob JSON (UploadThing) |
336
337
  | `handler_path` | TEXT | Path to the handler file |
337
338
  | `commit_sha` | TEXT | Git commit SHA |
338
339
  | `branch_name` | TEXT | Git branch name (default: `'main'`) |
@@ -352,7 +353,7 @@ Inserts a new row into `backend_requests` and returns the generated (or provided
352
353
 
353
354
  **Parameters:**
354
355
  - `sql` — A Neon SQL tagged-template function
355
- - `data.blobData` — Serialized blob JSON string
356
+ - `data.blobDataUrl` — URL to the uploaded blob JSON (e.g. UploadThing URL)
356
357
  - `data.handlerPath` — Handler file path
357
358
  - `data.commitSha` — Git commit SHA
358
359
  - `data.branchName` — Git branch name
@@ -367,9 +368,9 @@ Retrieves a single request by ID, or `null` if not found.
367
368
  - `sql` — A Neon SQL tagged-template function
368
369
  - `id` — The request UUID
369
370
 
370
- ### `backendRequestsGetBlobData(sql, id): Promise<string | null>`
371
+ ### `backendRequestsGetBlobUrl(sql, id): Promise<string | null>`
371
372
 
372
- 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.
373
374
 
374
375
  **Parameters:**
375
376
  - `sql` — A Neon SQL tagged-template function
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,28 +250,30 @@ 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 {
@@ -286,10 +288,9 @@ interface EnsureRequestRecordingOptions {
286
288
  * 1. Looks up the request in `backend_requests`.
287
289
  * 2. If a recording already exists (`status === "recorded"`), returns the recording ID immediately.
288
290
  * 3. If the request is already queued or processing, returns `null` without re-queuing.
289
- * 4. Otherwise, uploads the blob data to UploadThing, calls the Netlify Recorder
290
- * service's `create-recording` endpoint, updates the row to `"queued"`, and returns `null`.
291
- *
292
- * Requires `UPLOADTHING_TOKEN` environment variable and the `uploadthing` package.
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`.
293
294
  *
294
295
  * This function is idempotent — calling it multiple times for the same request
295
296
  * is safe. Once the recording completes, subsequent calls return the recording ID.
@@ -319,4 +320,4 @@ declare function databaseAuditDumpLogTable(sql: SqlFunction): Promise<Record<str
319
320
 
320
321
  declare function getCurrentRequestId(): string | null;
321
322
 
322
- 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,33 +1049,19 @@ async function backendRequestsUpdateStatus(sql, id, status, recordingId, errorMe
1049
1049
  `;
1050
1050
  }
1051
1051
  }
1052
- function databaseCallbacks(sql) {
1053
- return {
1054
- storeRequest: async (data) => {
1055
- return backendRequestsInsert(sql, {
1056
- id: data.requestId,
1057
- blobData: data.blobData,
1058
- handlerPath: data.handlerPath,
1059
- commitSha: data.commitSha,
1060
- branchName: data.branchName,
1061
- repositoryUrl: data.repositoryUrl
1062
- });
1063
- }
1064
- };
1065
- }
1066
1052
  async function uploadBlobData(blobData, requestId) {
1067
1053
  let UTApi;
1068
1054
  try {
1069
1055
  ({ UTApi } = await Function('return import("uploadthing/server")')());
1070
1056
  } catch {
1071
1057
  throw new Error(
1072
- "ensureRequestRecording requires the 'uploadthing' package. Install it with: npm install uploadthing"
1058
+ "netlify-recorder requires the 'uploadthing' package. Install it with: npm install uploadthing"
1073
1059
  );
1074
1060
  }
1075
1061
  const token = process.env.UPLOADTHING_TOKEN;
1076
1062
  if (!token) {
1077
1063
  throw new Error(
1078
- "ensureRequestRecording: UPLOADTHING_TOKEN environment variable is required to upload blob data"
1064
+ "netlify-recorder: UPLOADTHING_TOKEN environment variable is required to upload blob data"
1079
1065
  );
1080
1066
  }
1081
1067
  const utapi = new UTApi({ token });
@@ -1092,6 +1078,21 @@ async function uploadBlobData(blobData, requestId) {
1092
1078
  }
1093
1079
  return result.data.ufsUrl;
1094
1080
  }
1081
+ function databaseCallbacks(sql) {
1082
+ return {
1083
+ storeRequest: async (data) => {
1084
+ const blobDataUrl = await uploadBlobData(data.blobData, data.requestId ?? crypto.randomUUID());
1085
+ return backendRequestsInsert(sql, {
1086
+ id: data.requestId,
1087
+ blobDataUrl,
1088
+ handlerPath: data.handlerPath,
1089
+ commitSha: data.commitSha,
1090
+ branchName: data.branchName,
1091
+ repositoryUrl: data.repositoryUrl
1092
+ });
1093
+ }
1094
+ };
1095
+ }
1095
1096
  async function ensureRequestRecording(sql, requestId, options) {
1096
1097
  const request = await backendRequestsGet(sql, requestId);
1097
1098
  if (!request) {
@@ -1103,13 +1104,12 @@ async function ensureRequestRecording(sql, requestId, options) {
1103
1104
  if (request.status === "queued" || request.status === "processing") {
1104
1105
  return null;
1105
1106
  }
1106
- const blobDataUrl = await uploadBlobData(request.blob_data, requestId);
1107
1107
  const recorderUrl = options.recorderUrl.replace(/\/+$/, "");
1108
1108
  const res = await fetch(`${recorderUrl}/api/create-recording`, {
1109
1109
  method: "POST",
1110
1110
  headers: { "Content-Type": "application/json" },
1111
1111
  body: JSON.stringify({
1112
- blobDataUrl,
1112
+ blobDataUrl: request.blob_data_url,
1113
1113
  handlerPath: request.handler_path,
1114
1114
  commitSha: request.commit_sha,
1115
1115
  branchName: request.branch_name,
@@ -1220,7 +1220,7 @@ async function databaseAuditDumpLogTable(sql) {
1220
1220
  export {
1221
1221
  backendRequestsEnsureTable,
1222
1222
  backendRequestsGet,
1223
- backendRequestsGetBlobData,
1223
+ backendRequestsGetBlobUrl,
1224
1224
  backendRequestsInsert,
1225
1225
  backendRequestsList,
1226
1226
  backendRequestsUpdateStatus,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.26.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": {