@replayio-app-building/netlify-recorder 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -114,47 +114,40 @@ export default createRecordingRequestHandler(
114
114
 
115
115
  ### 4. Create recordings via the Netlify Recorder service
116
116
 
117
- When you want to turn a captured request into a Replay recording, POST to the Netlify Recorder service's `create-recording` endpoint. You need to provide a `blobDataUrl` a publicly accessible URL where the recording container can fetch the captured blob data.
118
-
119
- If your app exposes the blob data via an endpoint (e.g. using `backendRequestsGetBlobData`), construct the URL and pass it:
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"`.
120
118
 
121
119
  ```typescript
120
+ import { ensureRequestRecording } from "@replayio-app-building/netlify-recorder";
121
+
122
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
123
 
125
- const response = await fetch(
126
- `${RECORDER_URL}/api/create-recording`,
127
- {
128
- method: "POST",
129
- headers: { "Content-Type": "application/json" },
130
- body: JSON.stringify({
131
- blobDataUrl,
132
- handlerPath: "netlify/functions/my-handler",
133
- commitSha: "abc123",
134
- branchName: "main",
135
- repositoryUrl: "https://github.com/org/repo.git", // optional
136
- webhookUrl: "https://your-app.netlify.app/api/on-recording-complete", // optional
137
- }),
138
- }
139
- );
124
+ const recordingId = await ensureRequestRecording(sql, requestId, {
125
+ recorderUrl: RECORDER_URL,
126
+ });
140
127
 
141
- const { requestId: serviceRequestId } = await response.json();
128
+ if (recordingId) {
129
+ console.log(`Recording already exists: ${recordingId}`);
130
+ } else {
131
+ console.log("Recording queued — check back later or use a webhook");
132
+ }
142
133
  ```
143
134
 
144
- The `create-recording` endpoint accepts:
135
+ The function is idempotent — calling it multiple times for the same request is safe:
136
+ - **`status: "recorded"`** — returns the `recording_id` immediately
137
+ - **`status: "queued"` or `"processing"`** — returns `null` without re-queuing
138
+ - **`status: "captured"` or `"failed"`** — calls the service, updates status to `"queued"`, returns `null`
145
139
 
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 |
140
+ By default, the blob data URL points to `${recorderUrl}/api/get-backend-request-blob?requestId=${requestId}`. If your app serves blob data from a different endpoint, pass a custom `blobDataUrl`:
154
141
 
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.
142
+ ```typescript
143
+ const recordingId = await ensureRequestRecording(sql, requestId, {
144
+ recorderUrl: RECORDER_URL,
145
+ blobDataUrl: `https://your-app.netlify.app/api/get-blob?id=${requestId}`,
146
+ webhookUrl: "https://your-app.netlify.app/api/on-recording-complete", // optional
147
+ });
148
+ ```
156
149
 
157
- If `webhookUrl` is provided, the service will POST the result to that URL when the recording completes (or fails):
150
+ When `webhookUrl` is provided, the service POSTs the result when the recording completes:
158
151
 
159
152
  On success:
160
153
  ```json
@@ -410,6 +403,21 @@ Updates the status of a request. Optionally sets `recording_id` (on success) or
410
403
  - `recordingId` — Replay recording ID (optional, for `"recorded"` status)
411
404
  - `errorMessage` — Error details (optional, for `"failed"` status)
412
405
 
406
+ ### `ensureRequestRecording(sql, requestId, options): Promise<string | null>`
407
+
408
+ Ensures a Replay recording exists (or is being created) for a backend request. Looks up the request in `backend_requests`, returns the `recording_id` if already recorded, or calls the Netlify Recorder service to queue a recording. Idempotent — safe to call multiple times for the same request.
409
+
410
+ **Parameters:**
411
+ - `sql` — A Neon SQL tagged-template function
412
+ - `requestId` — The backend request UUID
413
+ - `options.recorderUrl` — Base URL of the Netlify Recorder service
414
+ - `options.blobDataUrl` — Override the URL where the recording container fetches the blob JSON (defaults to `${recorderUrl}/api/get-backend-request-blob?requestId=${requestId}`)
415
+ - `options.webhookUrl` — URL to POST the result to when the recording completes or fails
416
+
417
+ **Returns:** The recording ID (`string`) if the request is already recorded, or `null` if the recording was queued or is in progress.
418
+
419
+ **Throws:** If the request ID is not found in `backend_requests`, or if the service call fails.
420
+
413
421
  ### `createRequestRecording(blobUrlOrData, handlerPath, requestInfo): Promise<RecordingResult>`
414
422
 
415
423
  Called inside a recording container running under `replay-node`. Downloads the captured data blob (or accepts pre-parsed `BlobData`), installs replay-mode interceptors that return pre-recorded responses instead of making real calls, and executes the original handler so `replay-node` can record the execution.
package/dist/index.d.ts CHANGED
@@ -292,6 +292,30 @@ declare function backendRequestsUpdateStatus(sql: SqlFunction$1, id: string, sta
292
292
  * captured request data directly in the `backend_requests` table.
293
293
  */
294
294
  declare function databaseCallbacks(sql: SqlFunction$1): FinishRequestCallbacks;
295
+ interface EnsureRequestRecordingOptions {
296
+ /** Base URL of the Netlify Recorder service (e.g. "https://netlify-recorder-bm4wmw.netlify.app"). */
297
+ recorderUrl: string;
298
+ /**
299
+ * Full URL where the recording container can fetch the blob JSON for this request.
300
+ * Defaults to `${recorderUrl}/api/get-backend-request-blob?requestId=${requestId}`.
301
+ */
302
+ blobDataUrl?: string;
303
+ /** URL to POST the result to when the recording completes or fails. */
304
+ webhookUrl?: string;
305
+ }
306
+ /**
307
+ * Ensures a Replay recording exists (or is being created) for a backend request.
308
+ *
309
+ * 1. Looks up the request in `backend_requests`.
310
+ * 2. If a recording already exists (`status === "recorded"`), returns the recording ID immediately.
311
+ * 3. If the request is already queued or processing, returns `null` without re-queuing.
312
+ * 4. Otherwise, calls the Netlify Recorder service's `create-recording` endpoint,
313
+ * updates the row to `"queued"`, and returns `null`.
314
+ *
315
+ * This function is idempotent — calling it multiple times for the same request
316
+ * is safe. Once the recording completes, subsequent calls return the recording ID.
317
+ */
318
+ declare function ensureRequestRecording(sql: SqlFunction$1, requestId: string, options: EnsureRequestRecordingOptions): Promise<string | null>;
295
319
 
296
320
  type SqlFunction = (...args: any[]) => Promise<any[]>;
297
321
  /**
@@ -316,4 +340,4 @@ declare function databaseAuditDumpLogTable(sql: SqlFunction): Promise<Record<str
316
340
 
317
341
  declare function getCurrentRequestId(): string | null;
318
342
 
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 };
343
+ 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 };
package/dist/index.js CHANGED
@@ -1034,6 +1034,40 @@ function databaseCallbacks(sql) {
1034
1034
  }
1035
1035
  };
1036
1036
  }
1037
+ async function ensureRequestRecording(sql, requestId, options) {
1038
+ const request = await backendRequestsGet(sql, requestId);
1039
+ if (!request) {
1040
+ throw new Error(`Backend request ${requestId} not found`);
1041
+ }
1042
+ if (request.status === "recorded" && request.recording_id) {
1043
+ return request.recording_id;
1044
+ }
1045
+ if (request.status === "queued" || request.status === "processing") {
1046
+ return null;
1047
+ }
1048
+ const recorderUrl = options.recorderUrl.replace(/\/+$/, "");
1049
+ const blobDataUrl = options.blobDataUrl ?? `${recorderUrl}/api/get-backend-request-blob?requestId=${requestId}`;
1050
+ const res = await fetch(`${recorderUrl}/api/create-recording`, {
1051
+ method: "POST",
1052
+ headers: { "Content-Type": "application/json" },
1053
+ body: JSON.stringify({
1054
+ blobDataUrl,
1055
+ handlerPath: request.handler_path,
1056
+ commitSha: request.commit_sha,
1057
+ branchName: request.branch_name,
1058
+ repositoryUrl: request.repository_url ?? void 0,
1059
+ webhookUrl: options.webhookUrl
1060
+ })
1061
+ });
1062
+ if (!res.ok) {
1063
+ const errBody = await res.text().catch(() => "(unreadable)");
1064
+ throw new Error(
1065
+ `Netlify Recorder create-recording failed: ${res.status} ${errBody}`
1066
+ );
1067
+ }
1068
+ await backendRequestsUpdateStatus(sql, requestId, "queued");
1069
+ return null;
1070
+ }
1037
1071
 
1038
1072
  // src/databaseAudit.ts
1039
1073
  async function databaseAuditEnsureLogTable(sql) {
@@ -1127,6 +1161,7 @@ export {
1127
1161
  databaseAuditEnsureLogTable,
1128
1162
  databaseAuditMonitorTable,
1129
1163
  databaseCallbacks,
1164
+ ensureRequestRecording,
1130
1165
  finishRequest,
1131
1166
  getCurrentRequestId,
1132
1167
  redactBlobData,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {