@replayio-app-building/netlify-recorder 0.15.10 → 0.17.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 as a blob, 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, stores the captured data via the Netlify Recorder service, and can later reproduce the exact execution as a Replay recording for debugging and analysis.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,23 +8,11 @@ Capture and replay Netlify function executions as [Replay](https://replay.io) re
8
8
  npm install @replayio-app-building/netlify-recorder
9
9
  ```
10
10
 
11
- ## Integration Options
12
-
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.
11
+ ## Setup
24
12
 
25
13
  ### 1. Set required environment variables
26
14
 
27
- `finishRequest` needs to know which repository, branch, and commit your deployed code belongs to. Set these three environment variables on your Netlify site:
15
+ `finishRequest` needs to know which repository, branch, and commit your deployed code belongs to. Set these environment variables on your Netlify site:
28
16
 
29
17
  | Variable | Description | How to set |
30
18
  |---|---|---|
@@ -115,7 +103,7 @@ export default createRecordingRequestHandler(
115
103
 
116
104
  ### 3. Create recordings
117
105
 
118
- When you want to turn a captured request into a Replay recording, POST to the service's `create-recording` endpoint with the request ID. If the request was created with a secret, you must include it:
106
+ When you want to turn a captured request into a Replay recording, POST to the Netlify Recorder service's `create-recording` endpoint with the request ID. If the request was created with a secret, you must include it:
119
107
 
120
108
  ```typescript
121
109
  const response = await fetch(
@@ -132,7 +120,7 @@ const response = await fetch(
132
120
  );
133
121
  ```
134
122
 
135
- The service looks up the stored blob data, dispatches the work to a pool container, and creates the recording.
123
+ The service looks up the stored blob data, dispatches the work to a recording container, and creates the recording.
136
124
 
137
125
  If `webhookUrl` is provided, the service will POST the result to that URL when the recording completes (or fails):
138
126
 
@@ -225,7 +213,7 @@ Requests created without a secret remain accessible without one (backward compat
225
213
 
226
214
  Store your secret as a `NETLIFY_RECORDER_SECRET` environment variable. To keep it secure:
227
215
 
228
- 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.
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.
229
217
 
230
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:
231
219
 
@@ -239,158 +227,6 @@ Use a strong random string (e.g. `openssl rand -base64 32`) and rotate it if com
239
227
 
240
228
  ---
241
229
 
242
- ## Option B: Self-Hosted
243
-
244
- 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.
245
-
246
- ### 1. Set required environment variables
247
-
248
- Same as Option A — you must set `REPLAY_REPOSITORY_URL`, `COMMIT_SHA`, and `BRANCH_NAME` on your Netlify site. See the table in Option A, Step 1 above.
249
-
250
- ### 2. Wrap your Netlify function
251
-
252
- Use `createRecordingRequestHandler` with custom callbacks that write to your own storage and database:
253
-
254
- ```typescript
255
- import { createRecordingRequestHandler } from "@replayio-app-building/netlify-recorder";
256
-
257
- const handler = createRecordingRequestHandler(
258
- async (event) => {
259
- const result = await myBusinessLogic();
260
-
261
- return {
262
- statusCode: 200,
263
- headers: { "Content-Type": "application/json" },
264
- body: JSON.stringify(result),
265
- };
266
- },
267
- {
268
- secret: process.env.NETLIFY_RECORDER_SECRET,
269
- callbacks: {
270
- uploadBlob: async (data) => {
271
- // Upload the JSON string to your blob storage (S3, R2, etc.)
272
- const res = await fetch("https://storage.example.com/upload", {
273
- method: "PUT",
274
- body: data,
275
- });
276
- const { url } = await res.json();
277
- return url;
278
- },
279
- storeRequestData: async ({ blobUrl, commitSha, branchName, repositoryUrl, handlerPath, secret }) => {
280
- const [row] = await sql`
281
- INSERT INTO requests (blob_url, commit_sha, branch_name, repository_url, handler_path, secret, status)
282
- VALUES (${blobUrl}, ${commitSha}, ${branchName}, ${repositoryUrl}, ${handlerPath}, ${secret}, 'captured')
283
- RETURNING id
284
- `;
285
- return row.id;
286
- },
287
- },
288
- }
289
- );
290
-
291
- export { handler };
292
- ```
293
-
294
- ### 3. Create a requests database table
295
-
296
- ```sql
297
- CREATE TABLE IF NOT EXISTS requests (
298
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
299
- blob_url TEXT,
300
- commit_sha TEXT,
301
- branch_name TEXT,
302
- repository_url TEXT,
303
- handler_path TEXT,
304
- secret TEXT,
305
- recording_id TEXT,
306
- status TEXT NOT NULL DEFAULT 'captured'
307
- CHECK (status IN ('captured', 'processing', 'recorded', 'failed')),
308
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
309
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
310
- );
311
-
312
- CREATE INDEX IF NOT EXISTS idx_requests_secret ON requests (secret) WHERE secret IS NOT NULL;
313
- ```
314
-
315
- ### 4. Create a background function to produce recordings
316
-
317
- ```typescript
318
- import { ensureRequestRecording } from "@replayio-app-building/netlify-recorder";
319
- import type { Handler } from "@netlify/functions";
320
-
321
- const handler: Handler = async (event) => {
322
- const { requestId } = JSON.parse(event.body ?? "{}");
323
-
324
- const recordingId = await ensureRequestRecording(requestId, {
325
- repositoryUrl: process.env.APP_REPOSITORY_URL!,
326
- lookupRequest: async (id) => {
327
- const [row] = await sql`
328
- SELECT blob_url, commit_sha, branch_name, handler_path
329
- FROM requests WHERE id = ${id}
330
- `;
331
- return {
332
- blobUrl: row.blob_url,
333
- commitSha: row.commit_sha,
334
- branchName: row.branch_name ?? "main",
335
- handlerPath: row.handler_path,
336
- };
337
- },
338
- updateStatus: async (id, status, recordingId) => {
339
- await sql`
340
- UPDATE requests
341
- SET status = ${status},
342
- recording_id = ${recordingId ?? null},
343
- updated_at = NOW()
344
- WHERE id = ${id}
345
- `;
346
- },
347
- });
348
-
349
- return {
350
- statusCode: 200,
351
- body: JSON.stringify({ recordingId }),
352
- };
353
- };
354
-
355
- export { handler };
356
- ```
357
-
358
- ### 5. Create a container script
359
-
360
- This script runs inside the recording container under `replay-node`:
361
-
362
- ```typescript
363
- // scripts/create-request-recording.ts
364
- import { createRequestRecording } from "@replayio-app-building/netlify-recorder";
365
-
366
- const args = process.argv.slice(2);
367
- const blobUrl = args[args.indexOf("--blob-url") + 1]!;
368
- const handlerPath = args[args.indexOf("--handler-path") + 1]!;
369
-
370
- await createRequestRecording(blobUrl, handlerPath, {
371
- method: "POST",
372
- url: handlerPath,
373
- headers: {},
374
- });
375
- ```
376
-
377
- ### Required infrastructure
378
-
379
- Self-hosted recording requires these environment variables:
380
-
381
- | Variable | Description |
382
- |---|---|
383
- | `INFISICAL_CLIENT_ID` | Infisical service account client ID |
384
- | `INFISICAL_CLIENT_SECRET` | Infisical service account client secret |
385
- | `INFISICAL_PROJECT_ID` | Infisical project ID |
386
- | `INFISICAL_ENVIRONMENT` | Infisical environment (e.g. `production`) |
387
- | `FLY_API_TOKEN` | Fly.io API token for container management |
388
- | `FLY_APP_NAME` | Fly.io app name for container deployment |
389
- | `APP_REPOSITORY_URL` | Git repository URL for container cloning |
390
- | `RECORD_REPLAY_API_KEY` | Replay API key for recording upload |
391
-
392
- ---
393
-
394
230
  ## Audit Log Support
395
231
 
396
232
  The package automatically tracks database mutations (INSERT, UPDATE, DELETE) in an `audit_log` table and links each change to the Replay request that caused it. When your handler is wrapped with `createRecordingRequestHandler`, all Neon SQL queries are automatically tagged with the request ID — no changes to your SQL code required.
@@ -484,7 +320,7 @@ When `context.waitUntil` is not available (v1 handlers or missing context), the
484
320
 
485
321
  **Parameters:**
486
322
  - `handler` — Your async handler function `(event, context?) => Promise<{ statusCode, headers?, body? }>`
487
- - `options.callbacks` — Either `remoteCallbacks(serviceUrl)` or custom `{ uploadBlob, storeRequestData }` callbacks
323
+ - `options.callbacks` — `remoteCallbacks(serviceUrl)` for sending captured data to the Netlify Recorder service
488
324
  - `options.handlerPath` — Path to the handler file (used for recording metadata)
489
325
  - `options.commitSha` — Override `COMMIT_SHA` env var
490
326
  - `options.branchName` — Override `BRANCH_NAME` env var
@@ -520,7 +356,7 @@ Logs a `console.warn` when the total duration exceeds 2 seconds or when individu
520
356
 
521
357
  **Parameters:**
522
358
  - `requestContext` — The context returned by `startRequest`
523
- - `callbacks` — Either `remoteCallbacks(serviceUrl)` or custom `{ uploadBlob, storeRequestData }` callbacks
359
+ - `callbacks` — `remoteCallbacks(serviceUrl)` for sending captured data to the Netlify Recorder service
524
360
  - `response` — The handler's response object (`{ statusCode, headers?, body? }`)
525
361
  - `options.handlerPath` — Path to the handler file (used for recording metadata)
526
362
  - `options.commitSha` — Override `COMMIT_SHA` env var
@@ -531,30 +367,11 @@ Logs a `console.warn` when the total duration exceeds 2 seconds or when individu
531
367
 
532
368
  ### `remoteCallbacks(serviceUrl): FinishRequestCallbacks`
533
369
 
534
- 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.
370
+ Creates callbacks for `finishRequest` that send captured data to the Netlify Recorder service. The service handles blob storage and request tracking.
535
371
 
536
372
  **Parameters:**
537
373
  - `serviceUrl` — Base URL of the Netlify Recorder service (e.g. `"https://netlify-recorder-bm4wmw.netlify.app"`)
538
374
 
539
- ### `ensureRequestRecording(requestId, options): Promise<string>`
540
-
541
- 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).
542
-
543
- **Parameters:**
544
- - `requestId` — The request to create a recording for
545
- - `options.repositoryUrl` — Git repository URL for the container to clone
546
- - `options.lookupRequest(id)` — Fetches `{ blobUrl, commitSha, branchName, handlerPath }` from the database
547
- - `options.updateStatus(id, status, recordingId?)` — Updates the request status in the database
548
-
549
- ### `createRequestRecording(blobUrl, handlerPath, requestInfo): Promise<void>`
550
-
551
- 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).
552
-
553
- **Parameters:**
554
- - `blobUrl` — URL to the captured data blob
555
- - `handlerPath` — Path to the handler module to execute
556
- - `requestInfo` — The original request info to replay
557
-
558
375
  ### `databaseAuditEnsureLogTable(sql): Promise<void>`
559
376
 
560
377
  Creates the `audit_log` table, its indexes, and a reusable PL/pgSQL trigger function (`audit_trigger_function`). Call once during schema initialization.
@@ -579,8 +396,6 @@ Returns all rows from the `audit_log` table, ordered by `performed_at` DESC.
579
396
 
580
397
  ## Environment Variables
581
398
 
582
- ### Required for all setups (read by `finishRequest`)
583
-
584
399
  These must be set on your Netlify site. Your deploy script should resolve them from git and push them to the Netlify environment before deploying. `finishRequest` will throw an error if any are missing.
585
400
 
586
401
  | Variable | Description | How to resolve |
@@ -590,15 +405,8 @@ These must be set on your Netlify site. Your deploy script should resolve them f
590
405
  | `REPLAY_REPOSITORY_URL` | Git repository URL (no embedded credentials) | `git remote get-url origin` (strip tokens) |
591
406
  | `NETLIFY_RECORDER_SECRET` | Secret for access control (strongly recommended) | `openssl rand -base64 32` — store in Netlify site env vars |
592
407
 
593
- ### Required for self-hosted recording (Option B)
594
-
595
- | Variable | Description |
596
- |---|---|
597
- | `RECORD_REPLAY_API_KEY` | Replay API key for uploading recordings |
598
- | `APP_REPOSITORY_URL` | Git repository URL for container cloning |
599
-
600
408
  ## How It Works
601
409
 
602
- 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 sent to either the remote service (via `remoteCallbacks`) or your own storage (via custom callbacks).
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 sent to the Netlify Recorder service via `remoteCallbacks`.
603
411
 
604
- 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.
412
+ 2. **Recording phase**: The Netlify Recorder service dispatches the captured data to a recording container. Inside the container, the captured blob is downloaded, replay-mode interceptors return the pre-recorded responses instead of making real calls, and the original handler is re-executed 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
@@ -312,7 +312,7 @@ interface CreateRecordingRequestHandlerOptions extends FinishRequestOptions {
312
312
  * );
313
313
  * ```
314
314
  */
315
- declare function createRecordingRequestHandler(handler: (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>, options: CreateRecordingRequestHandlerOptions): (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse | Response>;
315
+ declare function createRecordingRequestHandler(handler: (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>, options: CreateRecordingRequestHandlerOptions): (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>;
316
316
 
317
317
  /**
318
318
  * Creates `FinishRequestCallbacks` that send captured data to a remote
package/dist/index.js CHANGED
@@ -578,7 +578,6 @@ async function finishRequest(requestContext, callbacks, response, options) {
578
578
  import crypto from "crypto";
579
579
  function createRecordingRequestHandler(handler, options) {
580
580
  return async (event, context) => {
581
- const isV2 = isWebApiRequest(event);
582
581
  const requestId = crypto.randomUUID();
583
582
  setCurrentRequestId(requestId);
584
583
  const reqContext = startRequest(event);
@@ -612,18 +611,12 @@ function createRecordingRequestHandler(handler, options) {
612
611
  }
613
612
  )
614
613
  );
615
- return isV2 ? toWebResponse(responseWithHeader) : responseWithHeader;
614
+ return responseWithHeader;
616
615
  }
617
616
  await finishRequest(reqContext, options.callbacks, response, finishOpts);
618
- return isV2 ? toWebResponse(responseWithHeader) : responseWithHeader;
617
+ return responseWithHeader;
619
618
  };
620
619
  }
621
- function toWebResponse(result) {
622
- return new Response(result.body, {
623
- status: result.statusCode,
624
- headers: result.headers
625
- });
626
- }
627
620
 
628
621
  // src/remoteCallbacks.ts
629
622
  function remoteCallbacks(serviceUrl) {
@@ -1236,23 +1229,7 @@ async function databaseAuditEnsureLogTable(sql) {
1236
1229
  changed_cols TEXT[];
1237
1230
  req_id TEXT;
1238
1231
  call_idx INTEGER;
1239
- pk_col TEXT;
1240
- pk_val TEXT;
1241
1232
  BEGIN
1242
- -- Dynamically look up the primary key column for this table
1243
- SELECT a.attname INTO pk_col
1244
- FROM pg_index i
1245
- JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
1246
- WHERE i.indrelid = TG_RELID AND i.indisprimary
1247
- LIMIT 1;
1248
-
1249
- -- Extract PK value from the appropriate record
1250
- IF TG_OP = 'DELETE' THEN
1251
- EXECUTE format('SELECT ($1.%I)::TEXT', pk_col) INTO pk_val USING OLD;
1252
- ELSE
1253
- EXECUTE format('SELECT ($1.%I)::TEXT', pk_col) INTO pk_val USING NEW;
1254
- END IF;
1255
-
1256
1233
  -- Read application context injected by the network interceptor
1257
1234
  req_id := COALESCE(current_setting('app.replay_request_id', true), '');
1258
1235
  IF req_id = '' THEN req_id := NULL; END IF;
@@ -1265,7 +1242,7 @@ async function databaseAuditEnsureLogTable(sql) {
1265
1242
 
1266
1243
  IF TG_OP = 'INSERT' THEN
1267
1244
  INSERT INTO audit_log (table_name, record_id, action, new_data, replay_request_id, replay_request_call_index)
1268
- VALUES (TG_TABLE_NAME, pk_val, 'INSERT', to_jsonb(NEW), req_id, call_idx);
1245
+ VALUES (TG_TABLE_NAME, NEW.id::TEXT, 'INSERT', to_jsonb(NEW), req_id, call_idx);
1269
1246
  RETURN NEW;
1270
1247
  ELSIF TG_OP = 'UPDATE' THEN
1271
1248
  SELECT ARRAY_AGG(n.key) INTO changed_cols
@@ -1274,11 +1251,11 @@ async function databaseAuditEnsureLogTable(sql) {
1274
1251
  WHERE o.value IS DISTINCT FROM n.value;
1275
1252
 
1276
1253
  INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, changed_fields, replay_request_id, replay_request_call_index)
1277
- VALUES (TG_TABLE_NAME, pk_val, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW), COALESCE(changed_cols, ARRAY[]::TEXT[]), req_id, call_idx);
1254
+ VALUES (TG_TABLE_NAME, NEW.id::TEXT, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW), COALESCE(changed_cols, ARRAY[]::TEXT[]), req_id, call_idx);
1278
1255
  RETURN NEW;
1279
1256
  ELSIF TG_OP = 'DELETE' THEN
1280
1257
  INSERT INTO audit_log (table_name, record_id, action, old_data, replay_request_id, replay_request_call_index)
1281
- VALUES (TG_TABLE_NAME, pk_val, 'DELETE', to_jsonb(OLD), req_id, call_idx);
1258
+ VALUES (TG_TABLE_NAME, OLD.id::TEXT, 'DELETE', to_jsonb(OLD), req_id, call_idx);
1282
1259
  RETURN OLD;
1283
1260
  END IF;
1284
1261
  RETURN NULL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.15.10",
3
+ "version": "0.17.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {