@replayio-app-building/netlify-recorder 0.2.0 → 0.4.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
@@ -22,7 +22,29 @@ There are two ways to use this package:
22
22
 
23
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
24
 
25
- ### 1. Wrap your Netlify function
25
+ ### 1. Set required environment variables
26
+
27
+ `finishRequest` needs to know which repository, branch, and commit your deployed code belongs to. Set these three environment variables on your Netlify site:
28
+
29
+ | Variable | Description | How to set |
30
+ |---|---|---|
31
+ | `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
+ | `COMMIT_SHA` | The git commit hash of the deployed code | Set in your deploy script via `git rev-parse HEAD` |
33
+ | `BRANCH_NAME` | The git branch of the deployed code | Set in your deploy script via `git rev-parse --abbrev-ref HEAD` |
34
+
35
+ **These are required.** `finishRequest` will throw an error if any are missing. Your deploy script should resolve them from git and set them on the Netlify site before deploying. Example:
36
+
37
+ ```typescript
38
+ // In your deploy script:
39
+ const commitSha = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
40
+ const branchName = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
41
+ const repositoryUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim()
42
+ .replace(/\/\/[^@]+@/, "//"); // strip embedded credentials
43
+
44
+ // Set these on your Netlify site via the Netlify API or CLI
45
+ ```
46
+
47
+ ### 2. Wrap your Netlify function
26
48
 
27
49
  Use `startRequest` / `finishRequest` with `remoteCallbacks()` to capture request data and send it to the service:
28
50
 
@@ -66,7 +88,7 @@ const handler: Handler = async (event) => {
66
88
  export { handler };
67
89
  ```
68
90
 
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.
91
+ The `remoteCallbacks(url)` helper sends the captured blob data — including the repository URL, branch, and commit — to the Netlify Recorder service's `/api/store-request` endpoint, 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
92
 
71
93
  ### 2. Create recordings
72
94
 
@@ -74,40 +96,21 @@ When you want to turn a captured request into a Replay recording, POST to the se
74
96
 
75
97
  ```typescript
76
98
  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`,
99
+ `${RECORDER_URL}/api/create-recording`,
95
100
  {
96
101
  method: "POST",
97
102
  headers: { "Content-Type": "application/json" },
98
103
  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",
104
+ requestId,
105
+ webhookUrl: "https://your-app.netlify.app/api/on-recording-complete", // optional
105
106
  }),
106
107
  }
107
108
  );
108
109
  ```
109
110
 
110
- **Webhook payload (POSTed to `webhookUrl`):**
111
+ The service looks up the stored blob data, dispatches the work to a pool container, and creates the recording.
112
+
113
+ If `webhookUrl` is provided, the service will POST the result to that URL when the recording completes (or fails):
111
114
 
112
115
  On success:
113
116
  ```json
@@ -119,13 +122,30 @@ On failure:
119
122
  { "status": "failed", "error": "Error message" }
120
123
  ```
121
124
 
125
+ ### 3. Check recording status
126
+
127
+ You can poll the recording status at any time:
128
+
129
+ ```typescript
130
+ const res = await fetch(
131
+ `${RECORDER_URL}/api/get-request?requestId=${requestId}`
132
+ );
133
+ const { status, recordingId } = await res.json();
134
+ // status: "captured" | "processing" | "recorded" | "failed"
135
+ // recordingId: string | null
136
+ ```
137
+
122
138
  ---
123
139
 
124
140
  ## Option B: Self-Hosted
125
141
 
126
142
  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
143
 
128
- ### 1. Wrap your Netlify function
144
+ ### 1. Set required environment variables
145
+
146
+ Same as Option A — you must set `REPOSITORY_URL`, `COMMIT_SHA`, and `BRANCH_NAME` on your Netlify site. See the table in Option A, Step 1 above.
147
+
148
+ ### 2. Wrap your Netlify function
129
149
 
130
150
  Use `startRequest` / `finishRequest` with custom callbacks that write to your own storage and database:
131
151
 
@@ -156,10 +176,10 @@ const handler: Handler = async (event) => {
156
176
  const { url } = await res.json();
157
177
  return url;
158
178
  },
159
- storeRequestData: async ({ blobUrl, commitSha, branchName, handlerPath }) => {
179
+ storeRequestData: async ({ blobUrl, commitSha, branchName, repositoryUrl, handlerPath }) => {
160
180
  const [row] = await sql`
161
- INSERT INTO requests (blob_url, commit_sha, branch_name, handler_path, status)
162
- VALUES (${blobUrl}, ${commitSha}, ${branchName}, ${handlerPath}, 'captured')
181
+ INSERT INTO requests (blob_url, commit_sha, branch_name, repository_url, handler_path, status)
182
+ VALUES (${blobUrl}, ${commitSha}, ${branchName}, ${repositoryUrl}, ${handlerPath}, 'captured')
163
183
  RETURNING id
164
184
  `;
165
185
  return row.id;
@@ -180,13 +200,15 @@ const handler: Handler = async (event) => {
180
200
  export { handler };
181
201
  ```
182
202
 
183
- ### 2. Create a requests database table
203
+ ### 3. Create a requests database table
184
204
 
185
205
  ```sql
186
206
  CREATE TABLE IF NOT EXISTS requests (
187
207
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
188
208
  blob_url TEXT,
189
209
  commit_sha TEXT,
210
+ branch_name TEXT,
211
+ repository_url TEXT,
190
212
  handler_path TEXT,
191
213
  recording_id TEXT,
192
214
  status TEXT NOT NULL DEFAULT 'captured'
@@ -196,7 +218,7 @@ CREATE TABLE IF NOT EXISTS requests (
196
218
  );
197
219
  ```
198
220
 
199
- ### 3. Create a background function to produce recordings
221
+ ### 4. Create a background function to produce recordings
200
222
 
201
223
  ```typescript
202
224
  import { ensureRequestRecording } from "@replayio-app-building/netlify-recorder";
@@ -240,7 +262,7 @@ const handler: Handler = async (event) => {
240
262
  export { handler };
241
263
  ```
242
264
 
243
- ### 4. Create a container script
265
+ ### 5. Create a container script
244
266
 
245
267
  This script runs inside the recording container under `replay-node`:
246
268
 
@@ -294,11 +316,16 @@ Begins capturing a Netlify handler execution. Patches `globalThis.fetch` and `pr
294
316
 
295
317
  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.
296
318
 
319
+ **Requires** the following environment variables (or equivalent options overrides): `COMMIT_SHA`, `BRANCH_NAME`, `REPOSITORY_URL`. Throws if any are missing.
320
+
297
321
  **Parameters:**
298
322
  - `requestContext` — The context returned by `startRequest`
299
323
  - `callbacks` — Either `remoteCallbacks(serviceUrl)` or custom `{ uploadBlob, storeRequestData }` callbacks
300
324
  - `response` — The handler's response object (`{ statusCode, headers?, body? }`)
301
325
  - `options.handlerPath` — Path to the handler file (used for recording metadata)
326
+ - `options.commitSha` — Override `COMMIT_SHA` env var
327
+ - `options.branchName` — Override `BRANCH_NAME` env var
328
+ - `options.repositoryUrl` — Override `REPOSITORY_URL` env var
302
329
 
303
330
  ### `remoteCallbacks(serviceUrl): FinishRequestCallbacks`
304
331
 
@@ -329,12 +356,22 @@ Called inside a container running under `replay-node`. Downloads the captured da
329
356
 
330
357
  ## Environment Variables
331
358
 
332
- | Variable | Required | Description |
359
+ ### Required for all setups (read by `finishRequest`)
360
+
361
+ 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.
362
+
363
+ | Variable | Description | How to resolve |
333
364
  |---|---|---|
334
- | `COMMIT_SHA` | No | Git commit hash (defaults to `"HEAD"`) |
335
- | `BRANCH_NAME` | No | Git branch name for container cloning (defaults to `"main"`) |
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 |
365
+ | `COMMIT_SHA` | Git commit hash of the deployed code | `git rev-parse HEAD` |
366
+ | `BRANCH_NAME` | Git branch of the deployed code | `git rev-parse --abbrev-ref HEAD` |
367
+ | `REPOSITORY_URL` | Git repository URL (no embedded credentials) | `git remote get-url origin` (strip tokens) |
368
+
369
+ ### Required for self-hosted recording (Option B)
370
+
371
+ | Variable | Description |
372
+ |---|---|
373
+ | `RECORD_REPLAY_API_KEY` | Replay API key for uploading recordings |
374
+ | `APP_REPOSITORY_URL` | Git repository URL for container cloning |
338
375
 
339
376
  ## How It Works
340
377
 
package/dist/index.d.ts CHANGED
@@ -46,6 +46,7 @@ interface FinishRequestCallbacks {
46
46
  blobUrl: string;
47
47
  commitSha: string;
48
48
  branchName: string;
49
+ repositoryUrl: string;
49
50
  handlerPath: string;
50
51
  }) => Promise<string>;
51
52
  }
@@ -109,6 +110,12 @@ interface HandlerResponse {
109
110
  }
110
111
  interface FinishRequestOptions {
111
112
  handlerPath?: string;
113
+ /** Override COMMIT_SHA env var. */
114
+ commitSha?: string;
115
+ /** Override BRANCH_NAME env var. */
116
+ branchName?: string;
117
+ /** Override REPOSITORY_URL env var. */
118
+ repositoryUrl?: string;
112
119
  }
113
120
  /**
114
121
  * Called at the end of the handler execution.
@@ -124,8 +131,8 @@ declare function finishRequest(requestContext: RequestContext, callbacks: Finish
124
131
  * to set up its own blob storage or database tables.
125
132
  *
126
133
  * 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.
134
+ * service's `/api/store-request` endpoint, which handles UploadThing
135
+ * storage and database insertion.
129
136
  *
130
137
  * @param serviceUrl - Base URL of the Netlify Recorder service
131
138
  * (e.g. "https://netlify-recorder-bm4wmw.netlify.app")
@@ -220,4 +227,4 @@ interface SpawnRecordingContainerOptions {
220
227
  */
221
228
  declare function spawnRecordingContainer(options: SpawnRecordingContainerOptions): Promise<string>;
222
229
 
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 };
230
+ export { type BlobData, type CapturedData, type ContainerInfraConfig, type EnsureRecordingOptions, type EnvRead, type FinishRequestCallbacks, type FinishRequestOptions, type NetworkCall, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
package/dist/index.js CHANGED
@@ -168,6 +168,7 @@ var ENV_ALLOW_LIST = /* @__PURE__ */ new Set([
168
168
  "LOGNAME",
169
169
  // Deploy metadata (non-secret identifiers)
170
170
  "COMMIT_SHA",
171
+ "BRANCH_NAME",
171
172
  // Common non-secret Netlify build vars
172
173
  "NETLIFY",
173
174
  "NETLIFY_DEV",
@@ -274,8 +275,21 @@ async function finishRequest(requestContext, callbacks, response, options) {
274
275
  if (globalThis.__REPLAY_RECORDING_MODE__ === true) {
275
276
  return response;
276
277
  }
277
- const commitSha = process.env.COMMIT_SHA ?? "HEAD";
278
- const branchName = process.env.BRANCH_NAME ?? "main";
278
+ const rawCommitSha = options?.commitSha ?? process.env.COMMIT_SHA;
279
+ const rawBranchName = options?.branchName ?? process.env.BRANCH_NAME;
280
+ const rawRepositoryUrl = options?.repositoryUrl ?? process.env.REPOSITORY_URL;
281
+ const missing = [];
282
+ if (!rawCommitSha) missing.push("COMMIT_SHA");
283
+ if (!rawBranchName) missing.push("BRANCH_NAME");
284
+ if (!rawRepositoryUrl) missing.push("REPOSITORY_URL");
285
+ if (missing.length > 0) {
286
+ throw new Error(
287
+ `netlify-recorder: missing required configuration: ${missing.join(", ")}. Set these as environment variables on your Netlify site, or pass them via the options parameter. See the package README for setup instructions.`
288
+ );
289
+ }
290
+ const commitSha = rawCommitSha;
291
+ const branchName = rawBranchName;
292
+ const repositoryUrl = rawRepositoryUrl;
279
293
  const rawBlobData = {
280
294
  requestInfo: requestContext.requestInfo,
281
295
  capturedData: requestContext.capturedData,
@@ -290,6 +304,7 @@ async function finishRequest(requestContext, callbacks, response, options) {
290
304
  blobUrl,
291
305
  commitSha,
292
306
  branchName,
307
+ repositoryUrl,
293
308
  handlerPath: options?.handlerPath ?? "unknown"
294
309
  });
295
310
  return {
@@ -319,7 +334,7 @@ function remoteCallbacks(serviceUrl) {
319
334
  );
320
335
  }
321
336
  const res = await fetch(
322
- `${base}/.netlify/functions/store-request`,
337
+ `${base}/api/store-request`,
323
338
  {
324
339
  method: "POST",
325
340
  headers: { "Content-Type": "application/json" },
@@ -327,7 +342,8 @@ function remoteCallbacks(serviceUrl) {
327
342
  blobData,
328
343
  handlerPath: metadata.handlerPath,
329
344
  commitSha: metadata.commitSha,
330
- branchName: metadata.branchName
345
+ branchName: metadata.branchName,
346
+ repositoryUrl: metadata.repositoryUrl
331
347
  })
332
348
  }
333
349
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio-app-building/netlify-recorder",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Capture and replay Netlify function executions as Replay recordings",
5
5
  "type": "module",
6
6
  "exports": {