@replayio-app-building/netlify-recorder 0.7.0 → 0.9.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 +62 -63
- package/dist/index.d.ts +44 -5
- package/dist/index.js +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,44 +46,36 @@ const repositoryUrl = execSync("git remote get-url origin", { encoding: "utf-8"
|
|
|
46
46
|
|
|
47
47
|
### 2. Wrap your Netlify function
|
|
48
48
|
|
|
49
|
-
Use `
|
|
49
|
+
Use `createRecordingRequestHandler` with `remoteCallbacks()` to wrap your handler with automatic request capture:
|
|
50
50
|
|
|
51
51
|
```typescript
|
|
52
52
|
import {
|
|
53
|
-
|
|
54
|
-
finishRequest,
|
|
53
|
+
createRecordingRequestHandler,
|
|
55
54
|
remoteCallbacks,
|
|
56
55
|
} from "@replayio-app-building/netlify-recorder";
|
|
57
|
-
import type { Handler } from "@netlify/functions";
|
|
58
56
|
|
|
59
57
|
const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
|
|
60
58
|
|
|
61
|
-
const handler
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
try {
|
|
59
|
+
const handler = createRecordingRequestHandler(
|
|
60
|
+
async (event) => {
|
|
65
61
|
const result = await myBusinessLogic();
|
|
66
62
|
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
);
|
|
77
|
-
} catch (err) {
|
|
78
|
-
reqContext.cleanup();
|
|
79
|
-
throw err;
|
|
63
|
+
return {
|
|
64
|
+
statusCode: 200,
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: JSON.stringify(result),
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
callbacks: remoteCallbacks(RECORDER_URL),
|
|
71
|
+
handlerPath: "netlify/functions/my-handler",
|
|
80
72
|
}
|
|
81
|
-
|
|
73
|
+
);
|
|
82
74
|
|
|
83
75
|
export { handler };
|
|
84
76
|
```
|
|
85
77
|
|
|
86
|
-
|
|
78
|
+
`createRecordingRequestHandler` automatically captures all outbound network calls and environment variable reads during your handler's execution, then sends the captured data to the Netlify Recorder service. The response includes an `X-Replay-Request-Id` header with the request ID managed by the service.
|
|
87
79
|
|
|
88
80
|
### 2. Create recordings
|
|
89
81
|
|
|
@@ -142,50 +134,43 @@ Same as Option A — you must set `REPLAY_REPOSITORY_URL`, `COMMIT_SHA`, and `BR
|
|
|
142
134
|
|
|
143
135
|
### 2. Wrap your Netlify function
|
|
144
136
|
|
|
145
|
-
Use `
|
|
137
|
+
Use `createRecordingRequestHandler` with custom callbacks that write to your own storage and database:
|
|
146
138
|
|
|
147
139
|
```typescript
|
|
148
|
-
import {
|
|
149
|
-
import type { Handler } from "@netlify/functions";
|
|
150
|
-
|
|
151
|
-
const handler: Handler = async (event) => {
|
|
152
|
-
const reqContext = startRequest(event);
|
|
140
|
+
import { createRecordingRequestHandler } from "@replayio-app-building/netlify-recorder";
|
|
153
141
|
|
|
154
|
-
|
|
142
|
+
const handler = createRecordingRequestHandler(
|
|
143
|
+
async (event) => {
|
|
155
144
|
const result = await myBusinessLogic();
|
|
156
145
|
|
|
157
|
-
return
|
|
158
|
-
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
146
|
+
return {
|
|
147
|
+
statusCode: 200,
|
|
148
|
+
headers: { "Content-Type": "application/json" },
|
|
149
|
+
body: JSON.stringify(result),
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
callbacks: {
|
|
154
|
+
uploadBlob: async (data) => {
|
|
155
|
+
// Upload the JSON string to your blob storage (S3, R2, etc.)
|
|
156
|
+
const res = await fetch("https://storage.example.com/upload", {
|
|
157
|
+
method: "PUT",
|
|
158
|
+
body: data,
|
|
159
|
+
});
|
|
160
|
+
const { url } = await res.json();
|
|
161
|
+
return url;
|
|
162
|
+
},
|
|
163
|
+
storeRequestData: async ({ blobUrl, commitSha, branchName, repositoryUrl, handlerPath }) => {
|
|
164
|
+
const [row] = await sql`
|
|
165
|
+
INSERT INTO requests (blob_url, commit_sha, branch_name, repository_url, handler_path, status)
|
|
166
|
+
VALUES (${blobUrl}, ${commitSha}, ${branchName}, ${repositoryUrl}, ${handlerPath}, 'captured')
|
|
167
|
+
RETURNING id
|
|
168
|
+
`;
|
|
169
|
+
return row.id;
|
|
177
170
|
},
|
|
178
|
-
|
|
179
|
-
statusCode: 200,
|
|
180
|
-
headers: { "Content-Type": "application/json" },
|
|
181
|
-
body: JSON.stringify(result),
|
|
182
|
-
}
|
|
183
|
-
);
|
|
184
|
-
} catch (err) {
|
|
185
|
-
reqContext.cleanup();
|
|
186
|
-
throw err;
|
|
171
|
+
},
|
|
187
172
|
}
|
|
188
|
-
|
|
173
|
+
);
|
|
189
174
|
|
|
190
175
|
export { handler };
|
|
191
176
|
```
|
|
@@ -290,9 +275,23 @@ Self-hosted recording requires these environment variables:
|
|
|
290
275
|
|
|
291
276
|
## API Reference
|
|
292
277
|
|
|
278
|
+
### `createRecordingRequestHandler(handler, options): Handler`
|
|
279
|
+
|
|
280
|
+
Wraps a Netlify handler function with automatic request recording. This is the recommended way to integrate — it handles `startRequest`/`finishRequest` and error cleanup internally.
|
|
281
|
+
|
|
282
|
+
**Parameters:**
|
|
283
|
+
- `handler` — Your async handler function `(event, context?) => Promise<{ statusCode, headers?, body? }>`
|
|
284
|
+
- `options.callbacks` — Either `remoteCallbacks(serviceUrl)` or custom `{ uploadBlob, storeRequestData }` callbacks
|
|
285
|
+
- `options.handlerPath` — Path to the handler file (used for recording metadata)
|
|
286
|
+
- `options.commitSha` — Override `COMMIT_SHA` env var
|
|
287
|
+
- `options.branchName` — Override `BRANCH_NAME` env var
|
|
288
|
+
- `options.repositoryUrl` — Override `REPLAY_REPOSITORY_URL` env var
|
|
289
|
+
|
|
290
|
+
**Returns:** A wrapped handler function with the same signature.
|
|
291
|
+
|
|
293
292
|
### `startRequest(event): RequestContext`
|
|
294
293
|
|
|
295
|
-
Begins capturing a Netlify handler execution.
|
|
294
|
+
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.
|
|
296
295
|
|
|
297
296
|
**Parameters:**
|
|
298
297
|
- `event` — The Netlify handler event object (passed directly)
|
|
@@ -301,7 +300,7 @@ Begins capturing a Netlify handler execution. Accepts the raw Netlify event obje
|
|
|
301
300
|
|
|
302
301
|
### `finishRequest(requestContext, callbacks, response, options?): Promise<HandlerResponse>`
|
|
303
302
|
|
|
304
|
-
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.
|
|
303
|
+
Lower-level API. 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.
|
|
305
304
|
|
|
306
305
|
**Requires** the following environment variables (or equivalent options overrides): `COMMIT_SHA`, `BRANCH_NAME`, `REPLAY_REPOSITORY_URL`. Throws if any are missing.
|
|
307
306
|
|
|
@@ -362,6 +361,6 @@ These must be set on your Netlify site. Your deploy script should resolve them f
|
|
|
362
361
|
|
|
363
362
|
## How It Works
|
|
364
363
|
|
|
365
|
-
1. **Capture phase** (`startRequest` / `finishRequest`): When a Netlify function handles a request,
|
|
364
|
+
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).
|
|
366
365
|
|
|
367
366
|
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
|
@@ -57,7 +57,7 @@ interface EnvRead {
|
|
|
57
57
|
value: string | undefined;
|
|
58
58
|
timestamp: number;
|
|
59
59
|
}
|
|
60
|
-
interface HandlerResponse {
|
|
60
|
+
interface HandlerResponse$1 {
|
|
61
61
|
statusCode: number;
|
|
62
62
|
body?: string;
|
|
63
63
|
}
|
|
@@ -69,7 +69,7 @@ interface BlobData {
|
|
|
69
69
|
startTime: number;
|
|
70
70
|
endTime: number;
|
|
71
71
|
/** The response returned to the client, used to detect replay mismatches. */
|
|
72
|
-
handlerResponse?: HandlerResponse;
|
|
72
|
+
handlerResponse?: HandlerResponse$1;
|
|
73
73
|
}
|
|
74
74
|
interface FinishRequestCallbacks {
|
|
75
75
|
/** Uploads serialized captured data and returns the blob URL. */
|
|
@@ -160,6 +160,45 @@ interface FinishRequestOptions {
|
|
|
160
160
|
*/
|
|
161
161
|
declare function finishRequest(requestContext: RequestContext, callbacks: FinishRequestCallbacks, response: FullHandlerResponse, options?: FinishRequestOptions): Promise<FullHandlerResponse>;
|
|
162
162
|
|
|
163
|
+
interface HandlerResponse {
|
|
164
|
+
statusCode: number;
|
|
165
|
+
headers?: Record<string, string>;
|
|
166
|
+
body?: string;
|
|
167
|
+
}
|
|
168
|
+
interface CreateRecordingRequestHandlerOptions extends FinishRequestOptions {
|
|
169
|
+
callbacks: FinishRequestCallbacks;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Wraps a Netlify handler function with request recording.
|
|
173
|
+
*
|
|
174
|
+
* Automatically calls `startRequest` before the handler and `finishRequest`
|
|
175
|
+
* after, capturing all outbound network calls and environment variable reads.
|
|
176
|
+
* On error, interceptors are cleaned up and the error is re-thrown.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* import { createRecordingRequestHandler, remoteCallbacks } from "@replayio-app-building/netlify-recorder";
|
|
181
|
+
*
|
|
182
|
+
* const handler = createRecordingRequestHandler(
|
|
183
|
+
* async (event) => {
|
|
184
|
+
* const result = await myBusinessLogic();
|
|
185
|
+
* return {
|
|
186
|
+
* statusCode: 200,
|
|
187
|
+
* headers: { "Content-Type": "application/json" },
|
|
188
|
+
* body: JSON.stringify(result),
|
|
189
|
+
* };
|
|
190
|
+
* },
|
|
191
|
+
* {
|
|
192
|
+
* callbacks: remoteCallbacks("https://netlify-recorder-bm4wmw.netlify.app"),
|
|
193
|
+
* handlerPath: "netlify/functions/my-handler",
|
|
194
|
+
* }
|
|
195
|
+
* );
|
|
196
|
+
*
|
|
197
|
+
* export { handler };
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
declare function createRecordingRequestHandler(handler: (event: NetlifyEvent | Record<string, unknown>, context?: unknown) => Promise<HandlerResponse>, options: CreateRecordingRequestHandlerOptions): (event: NetlifyEvent | Record<string, unknown>, context?: unknown) => Promise<HandlerResponse>;
|
|
201
|
+
|
|
163
202
|
/**
|
|
164
203
|
* Creates `FinishRequestCallbacks` that send captured data to a remote
|
|
165
204
|
* Netlify Recorder service. This removes the need for the consuming app
|
|
@@ -222,9 +261,9 @@ interface RecordingResult {
|
|
|
222
261
|
/** Details about the mismatch, if any. */
|
|
223
262
|
mismatchDetails?: string;
|
|
224
263
|
/** The response produced during replay. */
|
|
225
|
-
replayResponse?: HandlerResponse;
|
|
264
|
+
replayResponse?: HandlerResponse$1;
|
|
226
265
|
/** The original response captured in the blob. */
|
|
227
|
-
capturedResponse?: HandlerResponse;
|
|
266
|
+
capturedResponse?: HandlerResponse$1;
|
|
228
267
|
/** Whether some recorded network calls were not consumed during replay. */
|
|
229
268
|
unconsumedNetworkCalls: boolean;
|
|
230
269
|
/** Details about unconsumed network calls, if any. */
|
|
@@ -276,4 +315,4 @@ interface SpawnRecordingContainerOptions {
|
|
|
276
315
|
*/
|
|
277
316
|
declare function spawnRecordingContainer(options: SpawnRecordingContainerOptions): Promise<string>;
|
|
278
317
|
|
|
279
|
-
export { type BlobData, type CapturedData, type ContainerInfraConfig, type EnsureRecordingOptions, type EnvRead, type FinishRequestCallbacks, type FinishRequestOptions, type HandlerResponse, type NetlifyEvent, type NetworkCall, type RecordingResult, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
|
|
318
|
+
export { type BlobData, type CapturedData, type ContainerInfraConfig, type CreateRecordingRequestHandlerOptions, type EnsureRecordingOptions, type EnvRead, type FinishRequestCallbacks, type FinishRequestOptions, type HandlerResponse$1 as HandlerResponse, type NetlifyEvent, type NetworkCall, type RecordingResult, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRecordingRequestHandler, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
|
package/dist/index.js
CHANGED
|
@@ -426,6 +426,20 @@ async function finishRequest(requestContext, callbacks, response, options) {
|
|
|
426
426
|
};
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
// src/createRecordingRequestHandler.ts
|
|
430
|
+
function createRecordingRequestHandler(handler, options) {
|
|
431
|
+
return async (event, context) => {
|
|
432
|
+
const reqContext = startRequest(event);
|
|
433
|
+
try {
|
|
434
|
+
const response = await handler(event, context);
|
|
435
|
+
return await finishRequest(reqContext, options.callbacks, response, options);
|
|
436
|
+
} catch (err) {
|
|
437
|
+
reqContext.cleanup();
|
|
438
|
+
throw err;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
429
443
|
// src/remoteCallbacks.ts
|
|
430
444
|
function remoteCallbacks(serviceUrl) {
|
|
431
445
|
const base = serviceUrl.replace(/\/+$/, "");
|
|
@@ -994,6 +1008,7 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
|
|
|
994
1008
|
return result;
|
|
995
1009
|
}
|
|
996
1010
|
export {
|
|
1011
|
+
createRecordingRequestHandler,
|
|
997
1012
|
createRequestRecording,
|
|
998
1013
|
ensureRequestRecording,
|
|
999
1014
|
finishRequest,
|