npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@replayio-app-building/netlify-recorder

v0.61.0

Published

Capture and replay Netlify function executions as Replay recordings

Readme

@replayio-app-building/netlify-recorder

Capture and replay Netlify function executions as Replay 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.

Installation

npm install @replayio-app-building/netlify-recorder

Setup

1. Create the backend_requests table

The package stores captured request data directly in your app's database. Call backendRequestsEnsureTable once during schema initialization to create the backend_requests table:

import { backendRequestsEnsureTable } from "@replayio-app-building/netlify-recorder";

await backendRequestsEnsureTable(sql);

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.

2. Set required environment variables

Set these environment variables on your Netlify site:

| Variable | Description | How to set | |---|---|---| | 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 | | COMMIT_SHA | The git commit hash of the deployed code | Set in your deploy script via git rev-parse HEAD | | BRANCH_NAME | The git branch of the deployed code | Set in your deploy script via git rev-parse --abbrev-ref HEAD | | UPLOADTHING_TOKEN | UploadThing API token for blob data storage | Set in Netlify site settings |

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:

// In your deploy script:
const commitSha = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
const branchName = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
const repositoryUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim()
  .replace(/\/\/[^@]+@/, "//"); // strip embedded credentials

// Set these on your Netlify site via the Netlify API or CLI

3. Wrap your Netlify function

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.

v1 handler (Netlify Functions v1 — event with httpMethod, path, etc.):

import {
  createRecordingRequestHandler,
  databaseCallbacks,
} from "@replayio-app-building/netlify-recorder";
import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL!);

const handler = createRecordingRequestHandler(
  async (event) => {
    const result = await myBusinessLogic();

    return {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(result),
    };
  },
  {
    callbacks: databaseCallbacks(sql),
    handlerPath: "netlify/functions/my-handler",
  }
);

export { handler };

v2 handler (Netlify Functions v2 — Web API Request):

import {
  createRecordingRequestHandler,
  databaseCallbacks,
} from "@replayio-app-building/netlify-recorder";
import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL!);

// The wrapper reads the body from a clone — you can still read req.json() etc.
export default createRecordingRequestHandler(
  async (req) => {
    const body = await (req as Request).json();
    const result = await myBusinessLogic(body);

    return {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(result),
    };
  },
  {
    callbacks: databaseCallbacks(sql),
    handlerPath: "netlify/functions/my-handler",
  }
);

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.

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.

4. Expose a recording endpoint for other services

Use createRecordingEndpoint to create a standalone Netlify function that other services can call to trigger recording creation or check recording status. This is the simplest way to let external services interact with the backend_requests table without importing the full package.

// netlify/functions/ensure-recording.ts
import { createRecordingEndpoint } from "@replayio-app-building/netlify-recorder";
import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL!);

export default createRecordingEndpoint({
  sql,
  recorderUrl: "https://netlify-recorder-bm4wmw.netlify.app",
  // Optional: require callers to authenticate with a shared secret
  secret: process.env.RECORDER_ENDPOINT_SECRET,
  // Optional: receive a webhook when the recording completes
  webhookUrl: "https://my-app.netlify.app/.netlify/functions/recording-webhook",
});

Calling the endpoint from another service:

// Trigger recording creation (POST)
const res = await fetch("https://my-app.netlify.app/.netlify/functions/ensure-recording", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    // Include if `secret` is configured:
    "Authorization": "Bearer my-shared-secret",
  },
  body: JSON.stringify({ requestId: "a1b2c3d4-..." }),
});
const result = await res.json();
// { status: "queued", requestId: "a1b2c3d4-..." }
// or { status: "recorded", recordingId: "...", requestId: "..." }
// or { status: "pending", requestId: "..." }

// Check status without triggering (GET)
const status = await fetch(
  "https://my-app.netlify.app/.netlify/functions/ensure-recording?requestId=a1b2c3d4-...",
  { headers: { "Authorization": "Bearer my-shared-secret" } },
);

Response statuses:

| Status | HTTP Code | Meaning | |--------|-----------|---------| | recorded | 200 | Recording exists — recordingId is included | | pending | 200 | Recording is queued or processing — check back later | | queued | 202 | Recording was just queued by this POST call | | not_found | 404 | Request ID not found in backend_requests | | error | 4xx/5xx | Validation error, auth failure, or recording failure |

Warm-start replay (preceding requests)

Netlify Functions run on AWS Lambda, where module-level state persists across invocations on the same container instance (warm starts). For example, a module-level Map used as a cache will retain entries across requests handled by the same container. This means a recording of a warm-start request may behave differently than the original if module-level state is empty.

The recorder handles this automatically. createRecordingRequestHandler maintains a module-level originalRequestId that captures the ID of the first request handled by each module instance. Every subsequent request on the same instance includes this reference in its blob data, linking all requests from the same warm-start chain.

When a recording is triggered for a request that has an originalRequestId, the recording service:

  1. Looks up all preceding requests from the same module instance (same original_request_id, earlier created_at)
  2. Fetches the blob data for each preceding request
  3. Replays the preceding requests in order on the handler module before executing the target request — each preceding request runs with its own replay interceptors so recorded network responses are served without making real calls
  4. Executes the target request, now with module-level state populated exactly as it was in production

This is fully automatic — no configuration or code changes are needed beyond wrapping your handlers with createRecordingRequestHandler. The original_request_id column is stored in the backend_requests table and the preceding blob URLs are resolved server-side.

5. Create recordings programmatically

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".

import { ensureRequestRecording } from "@replayio-app-building/netlify-recorder";

const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";

const recordingId = await ensureRequestRecording(sql, requestId, {
  recorderUrl: RECORDER_URL,
});

if (recordingId) {
  console.log(`Recording already exists: ${recordingId}`);
} else {
  console.log("Recording queued — check back later or use a webhook");
}

The function is idempotent — calling it multiple times for the same request is safe:

  • status: "recorded" — returns the recording_id immediately
  • status: "queued" or "processing" — returns null without re-queuing
  • status: "captured" or "failed" — calls the service, updates status to "queued", returns null

When webhookUrl is provided, the service POSTs the result when the recording completes:

On success:

{ "status": "recorded", "recordingId": "a1b2c3d4-..." }

On failure:

{ "status": "failed", "error": "Error message" }

6. Manage stored requests

Use the backendRequests* helpers to query and manage captured requests in your database:

import {
  backendRequestsGet,
  backendRequestsList,
  backendRequestsUpdateStatus,
} from "@replayio-app-building/netlify-recorder";

// Get a single request by ID
const request = await backendRequestsGet(sql, requestId);
// request.status: "captured" | "queued" | "processing" | "recorded" | "failed"
// request.recording_id: string | null

// List requests with optional filters
const requests = await backendRequestsList(sql, { status: "captured", limit: 20 });

// Update status after recording completes
await backendRequestsUpdateStatus(sql, requestId, "recorded", recordingId);

// Update status on failure
await backendRequestsUpdateStatus(sql, requestId, "failed", undefined, "Error message");

Audit Log Support

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.

Setup

1. Create the audit log table

Call databaseAuditEnsureLogTable once during schema initialization. This creates the audit_log table, its indexes, and a reusable PL/pgSQL trigger function:

import { databaseAuditEnsureLogTable } from "@replayio-app-building/netlify-recorder";

await databaseAuditEnsureLogTable(sql);

2. Monitor tables

For each table you want to audit, call databaseAuditMonitorTable. This attaches a trigger that logs every INSERT, UPDATE, and DELETE:

import { databaseAuditMonitorTable } from "@replayio-app-building/netlify-recorder";

await databaseAuditMonitorTable(sql, "users");
await databaseAuditMonitorTable(sql, "orders");

3. Use SQL normally in your handlers

No special SQL wrapper is needed. Any Neon SQL query inside a handler wrapped with createRecordingRequestHandler is automatically tagged with the replay request ID:

import {
  createRecordingRequestHandler,
  databaseCallbacks,
} from "@replayio-app-building/netlify-recorder";

export default createRecordingRequestHandler(
  async (req) => {
    await sql`INSERT INTO orders (product, qty) VALUES (${product}, ${qty})`;

    return { statusCode: 200, body: "OK" };
  },
  {
    callbacks: databaseCallbacks(sql),
    handlerPath: "netlify/functions/create-order",
  }
);

4. Query the audit log

Use databaseAuditDumpLogTable to retrieve all audit entries (ordered by most recent first):

import { databaseAuditDumpLogTable } from "@replayio-app-building/netlify-recorder";

const entries = await databaseAuditDumpLogTable(sql);

Each entry contains:

| Field | Description | |---|---| | table_name | The table where the change occurred | | record_id | The id of the affected row | | action | INSERT, UPDATE, or DELETE | | old_data | Previous row data (UPDATE/DELETE only) | | new_data | New row data (INSERT/UPDATE only) | | changed_fields | Array of column names that changed (UPDATE only) | | performed_at | Timestamp of the change | | replay_request_id | The Replay request ID that caused the change | | replay_request_call_index | The sequential query index within the request |

How it works

The network interceptor detects Neon SQL HTTP requests (which use fetch internally) and automatically wraps each query in a transaction with SET LOCAL statements that inject the current request ID and call index. The PostgreSQL trigger function reads these via current_setting() and stamps audit rows atomically. Outside a handler context, queries execute normally without audit metadata.


API Reference

createRecordingRequestHandler(handler, options): Handler

Wraps a Netlify handler function with automatic request recording. This is the recommended way to integrate — it handles startRequest/finishRequest and error cleanup internally.

Response timing: When the Netlify Functions v2 context object is available (with waitUntil), the response is returned to the client immediately with a pre-generated X-Replay-Request-Id header. The data storage continues in the background via context.waitUntil(), adding zero latency to the client response.

When context.waitUntil is not available (v1 handlers or missing context), the wrapper falls back to awaiting finishRequest before returning.

Parameters:

  • handler — Your async handler function (event, context?) => Promise<{ statusCode, headers?, body? }>
  • options.callbacksdatabaseCallbacks(sql) to store captured data in the backend_requests table
  • options.handlerPath — Path to the handler file (used for recording metadata)
  • options.commitSha — Override COMMIT_SHA env var
  • options.branchName — Override BRANCH_NAME env var
  • options.repositoryUrl — Override REPLAY_REPOSITORY_URL env var

Returns: A wrapped handler function with the same signature.

Callbacks note: When using the waitUntil flow, storeRequest receives a requestId field in its data parameter. Callbacks should use this as the row ID so the stored record matches the ID already sent to the client.

startRequest(event): RequestContext

Lower-level API. Begins capturing a Netlify handler execution. Patches globalThis.fetch and process.env to record all outbound network calls and environment variable reads. Use this with finishRequest when you need more control than createRecordingRequestHandler provides.

Accepts either a v1 NetlifyEvent (has httpMethod) or a v2 Web API Request (has method + url). The version is detected automatically.

For v2 Request inputs, the body is read from a clone — the original request body remains consumable by your handler.

Parameters:

  • event — A v1 NetlifyEvent or v2 Request object (passed directly — no need to clone)

Returns: A RequestContext object to pass to finishRequest.

finishRequest(requestContext, callbacks, response, options?): Promise<HandlerResponse>

Lower-level API. Finalizes the request capture. Restores original fetch and process.env, serializes the captured data, stores it via the callback, and returns the response with X-Replay-Request-Id header set.

Important: You must send the returned response to the client — it contains the X-Replay-Request-Id header.

Logs a console.warn when the total duration exceeds 2 seconds to help diagnose slow operations.

Requires the following environment variables (or equivalent options overrides): COMMIT_SHA, BRANCH_NAME, REPLAY_REPOSITORY_URL. Throws if any are missing.

Parameters:

  • requestContext — The context returned by startRequest
  • callbacksdatabaseCallbacks(sql) or a custom { storeRequest } callback
  • response — The handler's response object ({ statusCode, headers?, body? })
  • options.handlerPath — Path to the handler file (used for recording metadata)
  • options.commitSha — Override COMMIT_SHA env var
  • options.branchName — Override BRANCH_NAME env var
  • options.repositoryUrl — Override REPLAY_REPOSITORY_URL env var
  • options.requestId — Pre-generated request ID (used by createRecordingRequestHandler in the waitUntil flow)

databaseCallbacks(sql): FinishRequestCallbacks

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.

Requires UPLOADTHING_TOKEN environment variable and the uploadthing package.

Parameters:

  • sql — A Neon SQL tagged-template function

Returns: An object with a storeRequest method that uploads blob data and inserts rows into backend_requests.

backendRequestsEnsureTable(sql): Promise<void>

Creates the backend_requests table and its indexes. Call once during schema initialization.

The table schema:

| Column | Type | Description | |---|---|---| | id | UUID (PK) | Auto-generated request ID | | blob_data_url | TEXT | URL to the uploaded blob JSON (UploadThing) | | handler_path | TEXT | Path to the handler file | | commit_sha | TEXT | Git commit SHA | | branch_name | TEXT | Git branch name (default: 'main') | | repository_url | TEXT | Git repository URL (nullable) | | status | TEXT | 'captured', 'queued', 'processing', 'recorded', or 'failed' | | recording_id | TEXT | Replay recording ID (set when status is 'recorded') | | error_message | TEXT | Error details (set when status is 'failed') | | created_at | TIMESTAMPTZ | Row creation time | | updated_at | TIMESTAMPTZ | Last update time |

Parameters:

  • sql — A Neon SQL tagged-template function

backendRequestsInsert(sql, data): Promise<string>

Inserts a new row into backend_requests and returns the generated (or provided) ID.

Parameters:

  • sql — A Neon SQL tagged-template function
  • data.blobDataUrl — URL to the uploaded blob JSON (e.g. UploadThing URL)
  • data.handlerPath — Handler file path
  • data.commitSha — Git commit SHA
  • data.branchName — Git branch name
  • data.repositoryUrl — Git repository URL (optional)
  • data.id — Pre-generated UUID (optional; auto-generated if omitted)

backendRequestsGet(sql, id): Promise<BackendRequest | null>

Retrieves a single request by ID, or null if not found.

Parameters:

  • sql — A Neon SQL tagged-template function
  • id — The request UUID

backendRequestsGetBlobUrl(sql, id): Promise<string | null>

Retrieves only the blob_data_url column for a request, or null if not found.

Parameters:

  • sql — A Neon SQL tagged-template function
  • id — The request UUID

backendRequestsList(sql, filters?): Promise<BackendRequest[]>

Lists requests ordered by created_at DESC, with optional filters.

Parameters:

  • sql — A Neon SQL tagged-template function
  • filters.status — Filter by status (e.g. "captured", "recorded")
  • filters.limit — Maximum rows to return (default: 50)

backendRequestsUpdateStatus(sql, id, status, recordingId?, errorMessage?): Promise<void>

Updates the status of a request. Optionally sets recording_id (on success) or error_message (on failure).

Parameters:

  • sql — A Neon SQL tagged-template function
  • id — The request UUID
  • status — New status string
  • recordingId — Replay recording ID (optional, for "recorded" status)
  • errorMessage — Error details (optional, for "failed" status)

ensureRequestRecording(sql, requestId, options): Promise<string | null>

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.

Parameters:

  • sql — A Neon SQL tagged-template function
  • requestId — The backend request UUID
  • options.recorderUrl — Base URL of the Netlify Recorder service
  • options.webhookUrl — URL to POST the result to when the recording completes or fails

Returns: The recording ID (string) if the request is already recorded, or null if the recording was queued or is in progress.

Throws: If the request ID is not found in backend_requests, or if the service call fails.

createRecordingEndpoint(options): (req: Request) => Promise<Response>

Creates a Netlify Function v2 handler that other services can call to trigger recording creation or check recording status for entries in the backend_requests table.

Supports POST (trigger recording) and GET (check status). When secret is provided, all requests must include an Authorization: Bearer <secret> header.

Parameters:

  • options.sql — A Neon SQL tagged-template function
  • options.recorderUrl — Base URL of the Netlify Recorder service
  • options.secret — Shared secret for authentication (optional — when omitted, the endpoint is open)
  • options.webhookUrl — URL to POST the recording result to when complete (optional)

Returns: An async function (req: Request) => Promise<Response> suitable as a Netlify Functions v2 default export.

POST body: { "requestId": "<uuid>" } — triggers recording if needed.

GET query: ?requestId=<uuid> — returns current status without triggering.

Response body (RecordingEndpointResponse):

  • status"recorded", "pending", "queued", "not_found", or "error"
  • recordingId — Present when status is "recorded"
  • requestId — The request ID echoed back
  • error — Error message when status is "not_found" or "error"

createRequestRecording(blobUrlOrData, handlerPath, requestInfo): Promise<RecordingResult>

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.

Returns a RecordingResult with mismatch detection:

| Field | Type | Description | |---|---|---| | responseMismatch | boolean | Whether the replay response differs from the captured response | | mismatchDetails | string? | Description of the mismatch | | replayResponse | HandlerResponse? | The response produced during replay | | capturedResponse | HandlerResponse? | The original captured response | | unconsumedNetworkCalls | boolean | Whether some recorded network calls were not replayed | | unconsumedNetworkDetails | string? | Details about unconsumed calls |

Parameters:

  • blobUrlOrData — URL to the captured data blob, or pre-parsed BlobData object
  • handlerPath — Path to the handler module to execute
  • requestInfo — The original request info to replay

getCurrentRequestId(): string | null

Returns the request ID for the currently executing handler, or null if no handler is active. Useful for correlating logs or database operations with the current request.

redactBlobData(data): BlobData

Redacts sensitive environment variable values from captured blob data before storage. Applied automatically by finishRequest — you only need to call this directly if using the lower-level APIs.

Redaction rules:

  • Allow-listed keys (standard system/runtime variables like NODE_ENV, PATH, COMMIT_SHA) are never redacted
  • Values that are undefined or 8 characters or shorter are kept as-is
  • All other values are replaced with * repeated to the same length
  • Redacted values are also scrubbed from all other string fields in the blob (request headers, network call bodies, etc.) to prevent leakage through embedded values

Parameters:

  • data — A BlobData object

Returns: A new BlobData object with sensitive values masked.

databaseAuditEnsureLogTable(sql): Promise<void>

Creates the audit_log table, its indexes, and a reusable PL/pgSQL trigger function (audit_trigger_function). Call once during schema initialization.

Parameters:

  • sql — A Neon SQL tagged-template function

databaseAuditMonitorTable(sql, tableName, primaryKeyColumn?): Promise<void>

Attaches a trigger to the specified table that logs INSERT, UPDATE, and DELETE operations to the audit_log table. Throws if tableName is 'audit_log'.

Parameters:

  • sql — A Neon SQL tagged-template function
  • tableName — Name of the table to monitor (must match ^[a-zA-Z_][a-zA-Z0-9_]*$)
  • primaryKeyColumn — Name of the primary key column (default: "id", must match ^[a-zA-Z_][a-zA-Z0-9_]*$)

databaseAuditDumpLogTable(sql): Promise<Record<string, unknown>[]>

Returns all rows from the audit_log table, ordered by performed_at DESC.

Parameters:

  • sql — A Neon SQL tagged-template function

Environment Variables

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.

| Variable | Description | How to resolve | |---|---|---| | COMMIT_SHA | Git commit hash of the deployed code | git rev-parse HEAD | | BRANCH_NAME | Git branch of the deployed code | git rev-parse --abbrev-ref HEAD | | REPLAY_REPOSITORY_URL | Git repository URL (no embedded credentials) | git remote get-url origin (strip tokens) |

How It Works

  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. Sensitive environment variable values are automatically redacted. When the handler completes, the originals are restored, the captured data is serialized to JSON, and stored in the backend_requests table in your database via databaseCallbacks.

  2. Recording phase: To create a Replay recording, POST to the Netlify Recorder service's create-recording endpoint with a blobDataUrl pointing to the captured data. The service dispatches the work to a recording container, which fetches the blob data from the URL, installs replay-mode interceptors that return pre-recorded responses instead of making real calls, 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. The recording result includes mismatch detection — the service compares the replay response against the originally captured response to flag any divergence.