@replayio-app-building/netlify-recorder 0.9.0 → 0.10.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 +43 -2
- package/dist/index.d.ts +99 -13
- package/dist/index.js +25 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,9 @@ const repositoryUrl = execSync("git remote get-url origin", { encoding: "utf-8"
|
|
|
46
46
|
|
|
47
47
|
### 2. Wrap your Netlify function
|
|
48
48
|
|
|
49
|
-
Use `createRecordingRequestHandler` with `remoteCallbacks()` to wrap your handler with automatic request capture
|
|
49
|
+
Use `createRecordingRequestHandler` with `remoteCallbacks()` to wrap your handler with automatic request capture.
|
|
50
|
+
|
|
51
|
+
**v1 handler** (Netlify Functions v1 — `event` with `httpMethod`, `path`, etc.):
|
|
50
52
|
|
|
51
53
|
```typescript
|
|
52
54
|
import {
|
|
@@ -75,8 +77,39 @@ const handler = createRecordingRequestHandler(
|
|
|
75
77
|
export { handler };
|
|
76
78
|
```
|
|
77
79
|
|
|
80
|
+
**v2 handler** (Netlify Functions v2 — Web API `Request`):
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import {
|
|
84
|
+
createRecordingRequestHandler,
|
|
85
|
+
remoteCallbacks,
|
|
86
|
+
} from "@replayio-app-building/netlify-recorder";
|
|
87
|
+
|
|
88
|
+
const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
|
|
89
|
+
|
|
90
|
+
// The wrapper reads the body from a clone — you can still read req.json() etc.
|
|
91
|
+
export default createRecordingRequestHandler(
|
|
92
|
+
async (req) => {
|
|
93
|
+
const body = await (req as Request).json();
|
|
94
|
+
const result = await myBusinessLogic(body);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
statusCode: 200,
|
|
98
|
+
headers: { "Content-Type": "application/json" },
|
|
99
|
+
body: JSON.stringify(result),
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
callbacks: remoteCallbacks(RECORDER_URL),
|
|
104
|
+
handlerPath: "netlify/functions/my-handler",
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
78
109
|
`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.
|
|
79
110
|
|
|
111
|
+
> **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.
|
|
112
|
+
|
|
80
113
|
### 2. Create recordings
|
|
81
114
|
|
|
82
115
|
When you want to turn a captured request into a Replay recording, POST to the service's `create-recording` endpoint with the request ID:
|
|
@@ -293,8 +326,12 @@ Wraps a Netlify handler function with automatic request recording. This is the r
|
|
|
293
326
|
|
|
294
327
|
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.
|
|
295
328
|
|
|
329
|
+
Accepts either a **v1** `NetlifyEvent` (has `httpMethod`) or a **v2** Web API `Request` (has `method` + `url`). The version is detected automatically.
|
|
330
|
+
|
|
331
|
+
For v2 Request inputs, the body is read from a **clone** — the original request body remains consumable by your handler.
|
|
332
|
+
|
|
296
333
|
**Parameters:**
|
|
297
|
-
- `event` —
|
|
334
|
+
- `event` — A v1 `NetlifyEvent` or v2 `Request` object (passed directly — no need to clone)
|
|
298
335
|
|
|
299
336
|
**Returns:** A `RequestContext` object to pass to `finishRequest`.
|
|
300
337
|
|
|
@@ -302,6 +339,10 @@ Lower-level API. Begins capturing a Netlify handler execution. Patches `globalTh
|
|
|
302
339
|
|
|
303
340
|
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.
|
|
304
341
|
|
|
342
|
+
**Important:** You must send the returned response to the client — it contains the `X-Replay-Request-Id` header.
|
|
343
|
+
|
|
344
|
+
Logs a `console.warn` when the total duration exceeds 2 seconds or when individual callback steps (upload, store) exceed 1 second, to help diagnose slow operations.
|
|
345
|
+
|
|
305
346
|
**Requires** the following environment variables (or equivalent options overrides): `COMMIT_SHA`, `BRANCH_NAME`, `REPLAY_REPOSITORY_URL`. Throws if any are missing.
|
|
306
347
|
|
|
307
348
|
**Parameters:**
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,27 @@ interface NetlifyEvent {
|
|
|
28
28
|
rawQuery?: string;
|
|
29
29
|
isBase64Encoded?: boolean;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Minimal interface for a Netlify Functions v2 Web API Request.
|
|
33
|
+
*
|
|
34
|
+
* This is a subset of the standard `Request` interface — only the properties
|
|
35
|
+
* that `startRequest` actually reads are required. Any spec-compliant
|
|
36
|
+
* `Request` object (including `new Request(...)`) satisfies this interface.
|
|
37
|
+
*
|
|
38
|
+
* `startRequest` reads the body from a **clone**, so the original request
|
|
39
|
+
* remains consumable by your handler. You do **not** need to clone the
|
|
40
|
+
* request before passing it in.
|
|
41
|
+
*/
|
|
42
|
+
interface NetlifyV2Request {
|
|
43
|
+
readonly method: string;
|
|
44
|
+
readonly url: string;
|
|
45
|
+
readonly headers: Headers | {
|
|
46
|
+
forEach(cb: (value: string, key: string) => void): void;
|
|
47
|
+
};
|
|
48
|
+
readonly body: ReadableStream<Uint8Array> | null;
|
|
49
|
+
clone(): NetlifyV2Request;
|
|
50
|
+
text(): Promise<string>;
|
|
51
|
+
}
|
|
31
52
|
interface RequestContext {
|
|
32
53
|
requestInfo: RequestInfo;
|
|
33
54
|
capturedData: CapturedData;
|
|
@@ -125,18 +146,46 @@ interface EnsureRecordingOptions {
|
|
|
125
146
|
|
|
126
147
|
/**
|
|
127
148
|
* Called at the beginning of a Netlify handler execution.
|
|
128
|
-
* Accepts either a Netlify Functions v1 event (HandlerEvent with httpMethod,
|
|
129
|
-
* path, etc.) or a v2 Web API Request. Extracts all request metadata
|
|
130
|
-
* internally, installs interceptors on globalThis.fetch and process.env to
|
|
131
|
-
* capture outbound network calls and environment variable reads, and returns
|
|
132
|
-
* a request context used by finishRequest.
|
|
133
149
|
*
|
|
134
|
-
*
|
|
150
|
+
* Accepts either a **Netlify Functions v1** event (`NetlifyEvent` — has
|
|
151
|
+
* `httpMethod`, `path`, etc.) or a **v2 Web API `Request`** object
|
|
152
|
+
* (`NetlifyV2Request`). The version is detected automatically: v1 events
|
|
153
|
+
* have an `httpMethod` property while v2 Request objects have `method` +
|
|
154
|
+
* `url`.
|
|
155
|
+
*
|
|
156
|
+
* **v2 body handling:** For v2 Request inputs the body is read from a
|
|
157
|
+
* **clone** of the request, so the original `Request` body remains
|
|
158
|
+
* consumable by your handler. You do **not** need to call `req.clone()`
|
|
159
|
+
* before passing it in. The body text is resolved asynchronously and
|
|
160
|
+
* captured when `finishRequest` is called.
|
|
161
|
+
*
|
|
162
|
+
* Installs interceptors on `globalThis.fetch` and `process.env` to capture
|
|
163
|
+
* outbound network calls and environment variable reads, and returns a
|
|
164
|
+
* request context that must be passed to `finishRequest`.
|
|
165
|
+
*
|
|
166
|
+
* When running inside `createRequestRecording` (replay mode), the replay
|
|
135
167
|
* interceptors are already installed. In that case we skip installing
|
|
136
|
-
* capture interceptors to avoid overwriting the replay layer
|
|
137
|
-
*
|
|
168
|
+
* capture interceptors to avoid overwriting the replay layer.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* // v1 handler
|
|
173
|
+
* export async function handler(event: NetlifyEvent) {
|
|
174
|
+
* const ctx = startRequest(event);
|
|
175
|
+
* // ... your logic ...
|
|
176
|
+
* return finishRequest(ctx, callbacks, response);
|
|
177
|
+
* }
|
|
178
|
+
*
|
|
179
|
+
* // v2 handler — no need to clone the request
|
|
180
|
+
* export default async function handler(req: Request) {
|
|
181
|
+
* const ctx = startRequest(req);
|
|
182
|
+
* const body = await req.json(); // still works — body was NOT consumed
|
|
183
|
+
* // ... your logic ...
|
|
184
|
+
* return finishRequest(ctx, callbacks, response);
|
|
185
|
+
* }
|
|
186
|
+
* ```
|
|
138
187
|
*/
|
|
139
|
-
declare function startRequest(event: NetlifyEvent |
|
|
188
|
+
declare function startRequest(event: NetlifyEvent | NetlifyV2Request): RequestContext;
|
|
140
189
|
|
|
141
190
|
interface FullHandlerResponse {
|
|
142
191
|
statusCode: number;
|
|
@@ -157,6 +206,14 @@ interface FinishRequestOptions {
|
|
|
157
206
|
* Restores original globals, serializes all captured data,
|
|
158
207
|
* uploads it as a JSON blob via the provided callback,
|
|
159
208
|
* stores the request metadata, and sets the X-Replay-Request-Id header.
|
|
209
|
+
*
|
|
210
|
+
* **Important:** The returned response includes the `X-Replay-Request-Id`
|
|
211
|
+
* header. You must send the returned response to the client — not the
|
|
212
|
+
* original response object you passed in.
|
|
213
|
+
*
|
|
214
|
+
* Logs a warning to `console.warn` when the total finishRequest time or
|
|
215
|
+
* individual callback steps exceed their thresholds, to help diagnose
|
|
216
|
+
* slow blob uploads or database writes.
|
|
160
217
|
*/
|
|
161
218
|
declare function finishRequest(requestContext: RequestContext, callbacks: FinishRequestCallbacks, response: FullHandlerResponse, options?: FinishRequestOptions): Promise<FullHandlerResponse>;
|
|
162
219
|
|
|
@@ -175,13 +232,20 @@ interface CreateRecordingRequestHandlerOptions extends FinishRequestOptions {
|
|
|
175
232
|
* after, capturing all outbound network calls and environment variable reads.
|
|
176
233
|
* On error, interceptors are cleaned up and the error is re-thrown.
|
|
177
234
|
*
|
|
178
|
-
*
|
|
235
|
+
* **Important:** You must use the response returned by the wrapped handler
|
|
236
|
+
* (not your original response object), because it includes the
|
|
237
|
+
* `X-Replay-Request-Id` header set by `finishRequest`.
|
|
238
|
+
*
|
|
239
|
+
* For v2 handlers the request body is read from a clone internally — your
|
|
240
|
+
* handler still receives the original request with an unconsumed body.
|
|
241
|
+
*
|
|
242
|
+
* @example v1 handler (NetlifyEvent)
|
|
179
243
|
* ```typescript
|
|
180
244
|
* import { createRecordingRequestHandler, remoteCallbacks } from "@replayio-app-building/netlify-recorder";
|
|
181
245
|
*
|
|
182
246
|
* const handler = createRecordingRequestHandler(
|
|
183
247
|
* async (event) => {
|
|
184
|
-
* const result = await myBusinessLogic();
|
|
248
|
+
* const result = await myBusinessLogic(event.body);
|
|
185
249
|
* return {
|
|
186
250
|
* statusCode: 200,
|
|
187
251
|
* headers: { "Content-Type": "application/json" },
|
|
@@ -196,8 +260,30 @@ interface CreateRecordingRequestHandlerOptions extends FinishRequestOptions {
|
|
|
196
260
|
*
|
|
197
261
|
* export { handler };
|
|
198
262
|
* ```
|
|
263
|
+
*
|
|
264
|
+
* @example v2 handler (Web API Request)
|
|
265
|
+
* ```typescript
|
|
266
|
+
* import { createRecordingRequestHandler, remoteCallbacks } from "@replayio-app-building/netlify-recorder";
|
|
267
|
+
*
|
|
268
|
+
* export default createRecordingRequestHandler(
|
|
269
|
+
* async (req) => {
|
|
270
|
+
* // Body is still available — startRequest reads from a clone
|
|
271
|
+
* const body = await (req as Request).json();
|
|
272
|
+
* const result = await myBusinessLogic(body);
|
|
273
|
+
* return {
|
|
274
|
+
* statusCode: 200,
|
|
275
|
+
* headers: { "Content-Type": "application/json" },
|
|
276
|
+
* body: JSON.stringify(result),
|
|
277
|
+
* };
|
|
278
|
+
* },
|
|
279
|
+
* {
|
|
280
|
+
* callbacks: remoteCallbacks("https://netlify-recorder-bm4wmw.netlify.app"),
|
|
281
|
+
* handlerPath: "netlify/functions/my-handler",
|
|
282
|
+
* }
|
|
283
|
+
* );
|
|
284
|
+
* ```
|
|
199
285
|
*/
|
|
200
|
-
declare function createRecordingRequestHandler(handler: (event: NetlifyEvent |
|
|
286
|
+
declare function createRecordingRequestHandler(handler: (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>, options: CreateRecordingRequestHandlerOptions): (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>;
|
|
201
287
|
|
|
202
288
|
/**
|
|
203
289
|
* Creates `FinishRequestCallbacks` that send captured data to a remote
|
|
@@ -315,4 +401,4 @@ interface SpawnRecordingContainerOptions {
|
|
|
315
401
|
*/
|
|
316
402
|
declare function spawnRecordingContainer(options: SpawnRecordingContainerOptions): Promise<string>;
|
|
317
403
|
|
|
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 };
|
|
404
|
+
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 NetlifyV2Request, type NetworkCall, type RecordingResult, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRecordingRequestHandler, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
|
package/dist/index.js
CHANGED
|
@@ -367,7 +367,10 @@ function redactBlobData(blobData) {
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
// src/finishRequest.ts
|
|
370
|
+
var SLOW_THRESHOLD_MS = 2e3;
|
|
371
|
+
var SLOW_STEP_THRESHOLD_MS = 1e3;
|
|
370
372
|
async function finishRequest(requestContext, callbacks, response, options) {
|
|
373
|
+
const finishStart = Date.now();
|
|
371
374
|
requestContext.cleanup();
|
|
372
375
|
if (globalThis.__REPLAY_RECORDING_MODE__ === true) {
|
|
373
376
|
return response;
|
|
@@ -387,6 +390,7 @@ async function finishRequest(requestContext, callbacks, response, options) {
|
|
|
387
390
|
const commitSha = rawCommitSha;
|
|
388
391
|
const branchName = rawBranchName;
|
|
389
392
|
const repositoryUrl = rawRepositoryUrl;
|
|
393
|
+
const handlerPath = options?.handlerPath ?? "unknown";
|
|
390
394
|
if (requestContext._v2BodyPromise && !requestContext.requestInfo.body) {
|
|
391
395
|
try {
|
|
392
396
|
const bodyText = await requestContext._v2BodyPromise;
|
|
@@ -409,14 +413,34 @@ async function finishRequest(requestContext, callbacks, response, options) {
|
|
|
409
413
|
};
|
|
410
414
|
const blobData = redactBlobData(rawBlobData);
|
|
411
415
|
const blobContent = JSON.stringify(blobData);
|
|
416
|
+
const uploadStart = Date.now();
|
|
412
417
|
const blobUrl = await callbacks.uploadBlob(blobContent);
|
|
418
|
+
const uploadDuration = Date.now() - uploadStart;
|
|
419
|
+
if (uploadDuration > SLOW_STEP_THRESHOLD_MS) {
|
|
420
|
+
console.warn(
|
|
421
|
+
`netlify-recorder: uploadBlob took ${uploadDuration}ms (handler: ${handlerPath})`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const storeStart = Date.now();
|
|
413
425
|
const storedRequestId = await callbacks.storeRequestData({
|
|
414
426
|
blobUrl,
|
|
415
427
|
commitSha,
|
|
416
428
|
branchName,
|
|
417
429
|
repositoryUrl,
|
|
418
|
-
handlerPath
|
|
430
|
+
handlerPath
|
|
419
431
|
});
|
|
432
|
+
const storeDuration = Date.now() - storeStart;
|
|
433
|
+
if (storeDuration > SLOW_STEP_THRESHOLD_MS) {
|
|
434
|
+
console.warn(
|
|
435
|
+
`netlify-recorder: storeRequestData took ${storeDuration}ms (handler: ${handlerPath})`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
const totalDuration = Date.now() - finishStart;
|
|
439
|
+
if (totalDuration > SLOW_THRESHOLD_MS) {
|
|
440
|
+
console.warn(
|
|
441
|
+
`netlify-recorder: finishRequest took ${totalDuration}ms total (upload: ${uploadDuration}ms, store: ${storeDuration}ms, handler: ${handlerPath})`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
420
444
|
return {
|
|
421
445
|
...response,
|
|
422
446
|
headers: {
|