@replayio-app-building/netlify-recorder 0.1.0 → 0.2.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,19 +1,136 @@
1
- # @netlify-recorder/core
1
+ # @replayio-app-building/netlify-recorder
2
2
 
3
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 as a blob, and can later reproduce the exact execution as a Replay recording for debugging and analysis.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @netlify-recorder/core
8
+ npm install @replayio-app-building/netlify-recorder
9
9
  ```
10
10
 
11
- ## Quick Start
11
+ ## Integration Options
12
12
 
13
- ### 1. Wrap your Netlify function with startRequest / finishRequest
13
+ There are two ways to use this package:
14
+
15
+ - **Option A: Use the Netlify Recorder service (recommended)** — The service handles blob storage, request tracking, and recording creation. Your app just wraps its handlers and calls `remoteCallbacks()`. No database or container infrastructure needed.
16
+
17
+ - **Option B: Self-hosted** — You manage your own blob storage, database tables, and recording containers. Full control, but requires more setup.
18
+
19
+ ---
20
+
21
+ ## Option A: Using the Netlify Recorder Service
22
+
23
+ The Netlify Recorder app (`https://netlify-recorder-bm4wmw.netlify.app`) provides a hosted service that stores captured request data, manages a pool of recording containers, and creates Replay recordings on demand. Your app needs zero database or infrastructure setup.
24
+
25
+ ### 1. Wrap your Netlify function
26
+
27
+ Use `startRequest` / `finishRequest` with `remoteCallbacks()` to capture request data and send it to the service:
14
28
 
15
29
  ```typescript
16
- import { startRequest, finishRequest } from "@netlify-recorder/core";
30
+ import {
31
+ startRequest,
32
+ finishRequest,
33
+ remoteCallbacks,
34
+ } from "@replayio-app-building/netlify-recorder";
35
+ import type { Handler } from "@netlify/functions";
36
+
37
+ const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
38
+
39
+ const handler: Handler = async (event) => {
40
+ const reqContext = startRequest({
41
+ method: event.httpMethod,
42
+ url: event.path,
43
+ headers: event.headers as Record<string, string>,
44
+ body: event.body ?? undefined,
45
+ });
46
+
47
+ try {
48
+ const result = await myBusinessLogic();
49
+
50
+ return await finishRequest(
51
+ reqContext,
52
+ remoteCallbacks(RECORDER_URL),
53
+ {
54
+ statusCode: 200,
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify(result),
57
+ },
58
+ { handlerPath: "netlify/functions/my-handler" }
59
+ );
60
+ } catch (err) {
61
+ reqContext.cleanup();
62
+ throw err;
63
+ }
64
+ };
65
+
66
+ export { handler };
67
+ ```
68
+
69
+ That's it for capturing. The `remoteCallbacks(url)` helper sends the captured blob data to the Netlify Recorder service, which uploads it to blob storage and stores the request metadata. The response includes an `X-Replay-Request-Id` header with the request ID managed by the service.
70
+
71
+ ### 2. Create recordings
72
+
73
+ When you want to turn a captured request into a Replay recording, POST to the service's `create-recording` endpoint with the request ID:
74
+
75
+ ```typescript
76
+ const response = await fetch(
77
+ `${RECORDER_URL}/.netlify/functions/create-recording`,
78
+ {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify({ requestId }),
82
+ }
83
+ );
84
+ ```
85
+
86
+ The service looks up the stored blob data, dispatches the work to a pool container, and creates the recording. You can check the recording status via the service's Requests page, or provide a webhook to be notified on completion.
87
+
88
+ ### 3. (Optional) Get notified when recordings complete
89
+
90
+ If you want to be notified when a recording finishes, call the blob endpoint directly with a `webhookUrl`:
91
+
92
+ ```typescript
93
+ await fetch(
94
+ `${RECORDER_URL}/.netlify/functions/create-recording-from-blob-background`,
95
+ {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({
99
+ blobUrl: "https://...", // blob URL from the service
100
+ handlerPath: "netlify/functions/my-handler",
101
+ commitSha: "a1b2c3d4...",
102
+ branchName: "main",
103
+ repositoryUrl: "https://github.com/your-org/your-repo",
104
+ webhookUrl: "https://your-app.netlify.app/.netlify/functions/on-recording-complete",
105
+ }),
106
+ }
107
+ );
108
+ ```
109
+
110
+ **Webhook payload (POSTed to `webhookUrl`):**
111
+
112
+ On success:
113
+ ```json
114
+ { "status": "recorded", "recordingId": "a1b2c3d4-..." }
115
+ ```
116
+
117
+ On failure:
118
+ ```json
119
+ { "status": "failed", "error": "Error message" }
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Option B: Self-Hosted
125
+
126
+ If you need full control, you can manage your own blob storage, database, and recording containers. This requires more setup but gives you complete ownership of the data pipeline.
127
+
128
+ ### 1. Wrap your Netlify function
129
+
130
+ Use `startRequest` / `finishRequest` with custom callbacks that write to your own storage and database:
131
+
132
+ ```typescript
133
+ import { startRequest, finishRequest } from "@replayio-app-building/netlify-recorder";
17
134
  import type { Handler } from "@netlify/functions";
18
135
 
19
136
  const handler: Handler = async (event) => {
@@ -25,8 +142,6 @@ const handler: Handler = async (event) => {
25
142
  });
26
143
 
27
144
  try {
28
- // Your handler logic — all fetch() calls and process.env reads
29
- // are automatically captured while the context is active.
30
145
  const result = await myBusinessLogic();
31
146
 
32
147
  return await finishRequest(
@@ -34,7 +149,6 @@ const handler: Handler = async (event) => {
34
149
  {
35
150
  uploadBlob: async (data) => {
36
151
  // Upload the JSON string to your blob storage (S3, R2, etc.)
37
- // and return the public URL.
38
152
  const res = await originalFetch("https://storage.example.com/upload", {
39
153
  method: "PUT",
40
154
  body: data,
@@ -43,7 +157,6 @@ const handler: Handler = async (event) => {
43
157
  return url;
44
158
  },
45
159
  storeRequestData: async ({ blobUrl, commitSha, branchName, handlerPath }) => {
46
- // Insert a row into your requests table and return the request ID.
47
160
  const [row] = await sql`
48
161
  INSERT INTO requests (blob_url, commit_sha, branch_name, handler_path, status)
49
162
  VALUES (${blobUrl}, ${commitSha}, ${branchName}, ${handlerPath}, 'captured')
@@ -59,7 +172,7 @@ const handler: Handler = async (event) => {
59
172
  }
60
173
  );
61
174
  } catch (err) {
62
- reqContext.cleanup(); // Always restore globals on error
175
+ reqContext.cleanup();
63
176
  throw err;
64
177
  }
65
178
  };
@@ -67,12 +180,8 @@ const handler: Handler = async (event) => {
67
180
  export { handler };
68
181
  ```
69
182
 
70
- The response returned by `finishRequest` includes the `X-Replay-Request-Id` header, which the frontend can read to display the request ID.
71
-
72
183
  ### 2. Create a requests database table
73
184
 
74
- The consuming app must provide a PostgreSQL table to track captured requests:
75
-
76
185
  ```sql
77
186
  CREATE TABLE IF NOT EXISTS requests (
78
187
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -87,24 +196,10 @@ CREATE TABLE IF NOT EXISTS requests (
87
196
  );
88
197
  ```
89
198
 
90
- **Columns:**
91
-
92
- | Column | Type | Description |
93
- |---|---|---|
94
- | `id` | UUID | Unique request ID |
95
- | `blob_url` | TEXT | URL to the stored captured data blob |
96
- | `commit_sha` | TEXT | Git commit hash when the request was captured |
97
- | `branch_name` | TEXT | Git branch name for container cloning |
98
- | `handler_path` | TEXT | Path to the handler file that was executed |
99
- | `recording_id` | TEXT | Replay recording ID (null until recording is created) |
100
- | `status` | TEXT | One of: `captured`, `processing`, `recorded`, `failed` |
101
- | `created_at` | TIMESTAMPTZ | When the request was captured |
102
- | `updated_at` | TIMESTAMPTZ | Last status update |
103
-
104
199
  ### 3. Create a background function to produce recordings
105
200
 
106
201
  ```typescript
107
- import { ensureRequestRecording } from "@netlify-recorder/core";
202
+ import { ensureRequestRecording } from "@replayio-app-building/netlify-recorder";
108
203
  import type { Handler } from "@netlify/functions";
109
204
 
110
205
  const handler: Handler = async (event) => {
@@ -145,13 +240,13 @@ const handler: Handler = async (event) => {
145
240
  export { handler };
146
241
  ```
147
242
 
148
- ### 4. Create a container script for createRequestRecording
243
+ ### 4. Create a container script
149
244
 
150
- This script runs inside the container spawned by `ensureRequestRecording`, under `replay-node`:
245
+ This script runs inside the recording container under `replay-node`:
151
246
 
152
247
  ```typescript
153
248
  // scripts/create-request-recording.ts
154
- import { createRequestRecording } from "@netlify-recorder/core";
249
+ import { createRequestRecording } from "@replayio-app-building/netlify-recorder";
155
250
 
156
251
  const args = process.argv.slice(2);
157
252
  const blobUrl = args[args.indexOf("--blob-url") + 1]!;
@@ -164,6 +259,23 @@ await createRequestRecording(blobUrl, handlerPath, {
164
259
  });
165
260
  ```
166
261
 
262
+ ### Required infrastructure
263
+
264
+ Self-hosted recording requires these environment variables:
265
+
266
+ | Variable | Description |
267
+ |---|---|
268
+ | `INFISICAL_CLIENT_ID` | Infisical service account client ID |
269
+ | `INFISICAL_CLIENT_SECRET` | Infisical service account client secret |
270
+ | `INFISICAL_PROJECT_ID` | Infisical project ID |
271
+ | `INFISICAL_ENVIRONMENT` | Infisical environment (e.g. `production`) |
272
+ | `FLY_API_TOKEN` | Fly.io API token for container management |
273
+ | `FLY_APP_NAME` | Fly.io app name for container deployment |
274
+ | `APP_REPOSITORY_URL` | Git repository URL for container cloning |
275
+ | `RECORD_REPLAY_API_KEY` | Replay API key for recording upload |
276
+
277
+ ---
278
+
167
279
  ## API Reference
168
280
 
169
281
  ### `startRequest(requestInfo): RequestContext`
@@ -178,19 +290,26 @@ Begins capturing a Netlify handler execution. Patches `globalThis.fetch` and `pr
178
290
 
179
291
  **Returns:** A `RequestContext` object to pass to `finishRequest`.
180
292
 
181
- ### `finishRequest(requestContext, callbacks, response): Promise<HandlerResponse>`
293
+ ### `finishRequest(requestContext, callbacks, response, options?): Promise<HandlerResponse>`
182
294
 
183
- Finalizes the request capture. Restores original `fetch` and `process.env`, serializes the captured data, uploads it via the `uploadBlob` callback, stores metadata via `storeRequestData`, and returns the response with `X-Replay-Request-Id` header set.
295
+ Finalizes the request capture. Restores original `fetch` and `process.env`, serializes the captured data, uploads it via the callbacks, and returns the response with `X-Replay-Request-Id` header set.
184
296
 
185
297
  **Parameters:**
186
298
  - `requestContext` — The context returned by `startRequest`
187
- - `callbacks.uploadBlob(data)` Receives serialized JSON, must return a URL
188
- - `callbacks.storeRequestData({ blobUrl, commitSha, branchName, handlerPath })` — Stores metadata, returns request ID
299
+ - `callbacks` — Either `remoteCallbacks(serviceUrl)` or custom `{ uploadBlob, storeRequestData }` callbacks
189
300
  - `response` — The handler's response object (`{ statusCode, headers?, body? }`)
301
+ - `options.handlerPath` — Path to the handler file (used for recording metadata)
302
+
303
+ ### `remoteCallbacks(serviceUrl): FinishRequestCallbacks`
304
+
305
+ Creates callbacks for `finishRequest` that send captured data to the Netlify Recorder service. The service handles blob storage and request tracking — no local database needed.
306
+
307
+ **Parameters:**
308
+ - `serviceUrl` — Base URL of the Netlify Recorder service (e.g. `"https://netlify-recorder-bm4wmw.netlify.app"`)
190
309
 
191
310
  ### `ensureRequestRecording(requestId, options): Promise<string>`
192
311
 
193
- Spawns a container via `@replayio/app-building` to create a Replay recording from captured request data. Returns the recording ID.
312
+ Spawns a container via `@replayio/app-building` to create a Replay recording from captured request data. Returns the recording ID. Only needed for self-hosted setups (Option B).
194
313
 
195
314
  **Parameters:**
196
315
  - `requestId` — The request to create a recording for
@@ -201,7 +320,7 @@ Spawns a container via `@replayio/app-building` to create a Replay recording fro
201
320
 
202
321
  ### `createRequestRecording(blobUrl, handlerPath, requestInfo): Promise<void>`
203
322
 
204
- Called inside a container running under `replay-node`. Downloads the captured data blob, installs replay-mode interceptors (which return pre-recorded responses instead of making real calls), and executes the original handler so replay-node can record the execution.
323
+ Called inside a container running under `replay-node`. Downloads the captured data blob, installs replay-mode interceptors (which return pre-recorded responses instead of making real calls), and executes the original handler so replay-node can record the execution. Only needed for self-hosted setups (Option B).
205
324
 
206
325
  **Parameters:**
207
326
  - `blobUrl` — URL to the captured data blob
@@ -214,11 +333,11 @@ Called inside a container running under `replay-node`. Downloads the captured da
214
333
  |---|---|---|
215
334
  | `COMMIT_SHA` | No | Git commit hash (defaults to `"HEAD"`) |
216
335
  | `BRANCH_NAME` | No | Git branch name for container cloning (defaults to `"main"`) |
217
- | `RECORD_REPLAY_API_KEY` | For recording | Replay API key for uploading recordings |
218
- | `APP_REPOSITORY_URL` | For recording | Git repository URL for container cloning |
336
+ | `RECORD_REPLAY_API_KEY` | For self-hosted recording | Replay API key for uploading recordings |
337
+ | `APP_REPOSITORY_URL` | For self-hosted recording | Git repository URL for container cloning |
219
338
 
220
339
  ## How It Works
221
340
 
222
- 1. **Capture phase** (`startRequest` / `finishRequest`): When a Netlify function handles a request, `startRequest` patches `globalThis.fetch` and `process.env` with Proxies that record every outbound network call and environment variable read. When the handler completes, `finishRequest` restores the originals, serializes the captured data to JSON, uploads it as a blob, and records the request in the database.
341
+ 1. **Capture phase** (`startRequest` / `finishRequest`): When a Netlify function handles a request, `startRequest` patches `globalThis.fetch` and `process.env` with Proxies that record every outbound network call and environment variable read. When the handler completes, `finishRequest` restores the originals, serializes the captured data to JSON, and sends it to either the remote service (via `remoteCallbacks`) or your own storage (via custom callbacks).
223
342
 
224
- 2. **Recording phase** (`ensureRequestRecording` / `createRequestRecording`): A background function calls `ensureRequestRecording`, which spawns a container with `@replayio/app-building`. Inside the container, `createRequestRecording` downloads the blob, installs replay-mode interceptors that return the pre-recorded responses, 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.
343
+ 2. **Recording phase**: The captured blob is sent to a recording container (either via the Netlify Recorder service or self-hosted). Inside the container, `createRequestRecording` downloads the blob, installs replay-mode interceptors that return the pre-recorded responses, 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.
package/dist/index.d.ts CHANGED
@@ -118,6 +118,20 @@ interface FinishRequestOptions {
118
118
  */
119
119
  declare function finishRequest(requestContext: RequestContext, callbacks: FinishRequestCallbacks, response: HandlerResponse, options?: FinishRequestOptions): Promise<HandlerResponse>;
120
120
 
121
+ /**
122
+ * Creates `FinishRequestCallbacks` that send captured data to a remote
123
+ * Netlify Recorder service. This removes the need for the consuming app
124
+ * to set up its own blob storage or database tables.
125
+ *
126
+ * The callbacks upload blob data and store request metadata via the
127
+ * service's `store-request` endpoint, which handles UploadThing storage
128
+ * and database insertion.
129
+ *
130
+ * @param serviceUrl - Base URL of the Netlify Recorder service
131
+ * (e.g. "https://netlify-recorder-bm4wmw.netlify.app")
132
+ */
133
+ declare function remoteCallbacks(serviceUrl: string): FinishRequestCallbacks;
134
+
121
135
  /**
122
136
  * Redacts sensitive environment variable values from blob data.
123
137
  *
@@ -206,4 +220,4 @@ interface SpawnRecordingContainerOptions {
206
220
  */
207
221
  declare function spawnRecordingContainer(options: SpawnRecordingContainerOptions): Promise<string>;
208
222
 
209
- export { type BlobData, type CapturedData, type ContainerInfraConfig, type EnsureRecordingOptions, type EnvRead, type FinishRequestCallbacks, type NetworkCall, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, spawnRecordingContainer, startRequest };
223
+ export { type BlobData, type CapturedData, type ContainerInfraConfig, type EnsureRecordingOptions, type EnvRead, type FinishRequestCallbacks, type NetworkCall, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
package/dist/index.js CHANGED
@@ -301,6 +301,48 @@ async function finishRequest(requestContext, callbacks, response, options) {
301
301
  };
302
302
  }
303
303
 
304
+ // src/remoteCallbacks.ts
305
+ function remoteCallbacks(serviceUrl) {
306
+ const base = serviceUrl.replace(/\/+$/, "");
307
+ let pendingBlobData;
308
+ return {
309
+ uploadBlob: async (data) => {
310
+ pendingBlobData = data;
311
+ return "__pending__";
312
+ },
313
+ storeRequestData: async (metadata) => {
314
+ const blobData = pendingBlobData;
315
+ pendingBlobData = void 0;
316
+ if (!blobData) {
317
+ throw new Error(
318
+ "remoteCallbacks: uploadBlob must be called before storeRequestData"
319
+ );
320
+ }
321
+ const res = await fetch(
322
+ `${base}/.netlify/functions/store-request`,
323
+ {
324
+ method: "POST",
325
+ headers: { "Content-Type": "application/json" },
326
+ body: JSON.stringify({
327
+ blobData,
328
+ handlerPath: metadata.handlerPath,
329
+ commitSha: metadata.commitSha,
330
+ branchName: metadata.branchName
331
+ })
332
+ }
333
+ );
334
+ if (!res.ok) {
335
+ const errBody = await res.text().catch(() => "(unreadable)");
336
+ throw new Error(
337
+ `Netlify Recorder store-request failed: ${res.status} ${errBody}`
338
+ );
339
+ }
340
+ const result = await res.json();
341
+ return result.requestId;
342
+ }
343
+ };
344
+ }
345
+
304
346
  // src/spawnRecordingContainer.ts
305
347
  async function spawnRecordingContainer(options) {
306
348
  const {
@@ -609,6 +651,7 @@ export {
609
651
  finishRequest,
610
652
  readInfraConfigFromEnv,
611
653
  redactBlobData,
654
+ remoteCallbacks,
612
655
  spawnRecordingContainer,
613
656
  startRequest
614
657
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {