@replayio-app-building/netlify-recorder 0.8.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 +104 -64
- package/dist/index.d.ts +139 -14
- package/dist/index.js +40 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,44 +46,69 @@ 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
|
+
|
|
51
|
+
**v1 handler** (Netlify Functions v1 — `event` with `httpMethod`, `path`, etc.):
|
|
50
52
|
|
|
51
53
|
```typescript
|
|
52
54
|
import {
|
|
53
|
-
|
|
54
|
-
finishRequest,
|
|
55
|
+
createRecordingRequestHandler,
|
|
55
56
|
remoteCallbacks,
|
|
56
57
|
} from "@replayio-app-building/netlify-recorder";
|
|
57
|
-
import type { Handler } from "@netlify/functions";
|
|
58
58
|
|
|
59
59
|
const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
|
|
60
60
|
|
|
61
|
-
const handler
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
try {
|
|
61
|
+
const handler = createRecordingRequestHandler(
|
|
62
|
+
async (event) => {
|
|
65
63
|
const result = await myBusinessLogic();
|
|
66
64
|
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
);
|
|
77
|
-
} catch (err) {
|
|
78
|
-
reqContext.cleanup();
|
|
79
|
-
throw err;
|
|
65
|
+
return {
|
|
66
|
+
statusCode: 200,
|
|
67
|
+
headers: { "Content-Type": "application/json" },
|
|
68
|
+
body: JSON.stringify(result),
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
callbacks: remoteCallbacks(RECORDER_URL),
|
|
73
|
+
handlerPath: "netlify/functions/my-handler",
|
|
80
74
|
}
|
|
81
|
-
|
|
75
|
+
);
|
|
82
76
|
|
|
83
77
|
export { handler };
|
|
84
78
|
```
|
|
85
79
|
|
|
86
|
-
|
|
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
|
+
|
|
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.
|
|
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.
|
|
87
112
|
|
|
88
113
|
### 2. Create recordings
|
|
89
114
|
|
|
@@ -142,50 +167,43 @@ Same as Option A — you must set `REPLAY_REPOSITORY_URL`, `COMMIT_SHA`, and `BR
|
|
|
142
167
|
|
|
143
168
|
### 2. Wrap your Netlify function
|
|
144
169
|
|
|
145
|
-
Use `
|
|
170
|
+
Use `createRecordingRequestHandler` with custom callbacks that write to your own storage and database:
|
|
146
171
|
|
|
147
172
|
```typescript
|
|
148
|
-
import {
|
|
149
|
-
import type { Handler } from "@netlify/functions";
|
|
150
|
-
|
|
151
|
-
const handler: Handler = async (event) => {
|
|
152
|
-
const reqContext = startRequest(event);
|
|
173
|
+
import { createRecordingRequestHandler } from "@replayio-app-building/netlify-recorder";
|
|
153
174
|
|
|
154
|
-
|
|
175
|
+
const handler = createRecordingRequestHandler(
|
|
176
|
+
async (event) => {
|
|
155
177
|
const result = await myBusinessLogic();
|
|
156
178
|
|
|
157
|
-
return
|
|
158
|
-
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
return {
|
|
180
|
+
statusCode: 200,
|
|
181
|
+
headers: { "Content-Type": "application/json" },
|
|
182
|
+
body: JSON.stringify(result),
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
callbacks: {
|
|
187
|
+
uploadBlob: async (data) => {
|
|
188
|
+
// Upload the JSON string to your blob storage (S3, R2, etc.)
|
|
189
|
+
const res = await fetch("https://storage.example.com/upload", {
|
|
190
|
+
method: "PUT",
|
|
191
|
+
body: data,
|
|
192
|
+
});
|
|
193
|
+
const { url } = await res.json();
|
|
194
|
+
return url;
|
|
195
|
+
},
|
|
196
|
+
storeRequestData: async ({ blobUrl, commitSha, branchName, repositoryUrl, handlerPath }) => {
|
|
197
|
+
const [row] = await sql`
|
|
198
|
+
INSERT INTO requests (blob_url, commit_sha, branch_name, repository_url, handler_path, status)
|
|
199
|
+
VALUES (${blobUrl}, ${commitSha}, ${branchName}, ${repositoryUrl}, ${handlerPath}, 'captured')
|
|
200
|
+
RETURNING id
|
|
201
|
+
`;
|
|
202
|
+
return row.id;
|
|
177
203
|
},
|
|
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;
|
|
204
|
+
},
|
|
187
205
|
}
|
|
188
|
-
|
|
206
|
+
);
|
|
189
207
|
|
|
190
208
|
export { handler };
|
|
191
209
|
```
|
|
@@ -290,18 +308,40 @@ Self-hosted recording requires these environment variables:
|
|
|
290
308
|
|
|
291
309
|
## API Reference
|
|
292
310
|
|
|
311
|
+
### `createRecordingRequestHandler(handler, options): Handler`
|
|
312
|
+
|
|
313
|
+
Wraps a Netlify handler function with automatic request recording. This is the recommended way to integrate — it handles `startRequest`/`finishRequest` and error cleanup internally.
|
|
314
|
+
|
|
315
|
+
**Parameters:**
|
|
316
|
+
- `handler` — Your async handler function `(event, context?) => Promise<{ statusCode, headers?, body? }>`
|
|
317
|
+
- `options.callbacks` — Either `remoteCallbacks(serviceUrl)` or custom `{ uploadBlob, storeRequestData }` callbacks
|
|
318
|
+
- `options.handlerPath` — Path to the handler file (used for recording metadata)
|
|
319
|
+
- `options.commitSha` — Override `COMMIT_SHA` env var
|
|
320
|
+
- `options.branchName` — Override `BRANCH_NAME` env var
|
|
321
|
+
- `options.repositoryUrl` — Override `REPLAY_REPOSITORY_URL` env var
|
|
322
|
+
|
|
323
|
+
**Returns:** A wrapped handler function with the same signature.
|
|
324
|
+
|
|
293
325
|
### `startRequest(event): RequestContext`
|
|
294
326
|
|
|
295
|
-
Begins capturing a Netlify handler execution.
|
|
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.
|
|
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.
|
|
296
332
|
|
|
297
333
|
**Parameters:**
|
|
298
|
-
- `event` —
|
|
334
|
+
- `event` — A v1 `NetlifyEvent` or v2 `Request` object (passed directly — no need to clone)
|
|
299
335
|
|
|
300
336
|
**Returns:** A `RequestContext` object to pass to `finishRequest`.
|
|
301
337
|
|
|
302
338
|
### `finishRequest(requestContext, callbacks, response, options?): Promise<HandlerResponse>`
|
|
303
339
|
|
|
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.
|
|
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.
|
|
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.
|
|
305
345
|
|
|
306
346
|
**Requires** the following environment variables (or equivalent options overrides): `COMMIT_SHA`, `BRANCH_NAME`, `REPLAY_REPOSITORY_URL`. Throws if any are missing.
|
|
307
347
|
|
|
@@ -362,6 +402,6 @@ These must be set on your Netlify site. Your deploy script should resolve them f
|
|
|
362
402
|
|
|
363
403
|
## How It Works
|
|
364
404
|
|
|
365
|
-
1. **Capture phase** (`startRequest` / `finishRequest`): When a Netlify function handles a request,
|
|
405
|
+
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
406
|
|
|
367
407
|
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
|
@@ -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;
|
|
@@ -57,7 +78,7 @@ interface EnvRead {
|
|
|
57
78
|
value: string | undefined;
|
|
58
79
|
timestamp: number;
|
|
59
80
|
}
|
|
60
|
-
interface HandlerResponse {
|
|
81
|
+
interface HandlerResponse$1 {
|
|
61
82
|
statusCode: number;
|
|
62
83
|
body?: string;
|
|
63
84
|
}
|
|
@@ -69,7 +90,7 @@ interface BlobData {
|
|
|
69
90
|
startTime: number;
|
|
70
91
|
endTime: number;
|
|
71
92
|
/** The response returned to the client, used to detect replay mismatches. */
|
|
72
|
-
handlerResponse?: HandlerResponse;
|
|
93
|
+
handlerResponse?: HandlerResponse$1;
|
|
73
94
|
}
|
|
74
95
|
interface FinishRequestCallbacks {
|
|
75
96
|
/** Uploads serialized captured data and returns the blob URL. */
|
|
@@ -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,9 +206,85 @@ 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
|
|
|
220
|
+
interface HandlerResponse {
|
|
221
|
+
statusCode: number;
|
|
222
|
+
headers?: Record<string, string>;
|
|
223
|
+
body?: string;
|
|
224
|
+
}
|
|
225
|
+
interface CreateRecordingRequestHandlerOptions extends FinishRequestOptions {
|
|
226
|
+
callbacks: FinishRequestCallbacks;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Wraps a Netlify handler function with request recording.
|
|
230
|
+
*
|
|
231
|
+
* Automatically calls `startRequest` before the handler and `finishRequest`
|
|
232
|
+
* after, capturing all outbound network calls and environment variable reads.
|
|
233
|
+
* On error, interceptors are cleaned up and the error is re-thrown.
|
|
234
|
+
*
|
|
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)
|
|
243
|
+
* ```typescript
|
|
244
|
+
* import { createRecordingRequestHandler, remoteCallbacks } from "@replayio-app-building/netlify-recorder";
|
|
245
|
+
*
|
|
246
|
+
* const handler = createRecordingRequestHandler(
|
|
247
|
+
* async (event) => {
|
|
248
|
+
* const result = await myBusinessLogic(event.body);
|
|
249
|
+
* return {
|
|
250
|
+
* statusCode: 200,
|
|
251
|
+
* headers: { "Content-Type": "application/json" },
|
|
252
|
+
* body: JSON.stringify(result),
|
|
253
|
+
* };
|
|
254
|
+
* },
|
|
255
|
+
* {
|
|
256
|
+
* callbacks: remoteCallbacks("https://netlify-recorder-bm4wmw.netlify.app"),
|
|
257
|
+
* handlerPath: "netlify/functions/my-handler",
|
|
258
|
+
* }
|
|
259
|
+
* );
|
|
260
|
+
*
|
|
261
|
+
* export { handler };
|
|
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
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
declare function createRecordingRequestHandler(handler: (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>, options: CreateRecordingRequestHandlerOptions): (event: NetlifyEvent | NetlifyV2Request, context?: unknown) => Promise<HandlerResponse>;
|
|
287
|
+
|
|
163
288
|
/**
|
|
164
289
|
* Creates `FinishRequestCallbacks` that send captured data to a remote
|
|
165
290
|
* Netlify Recorder service. This removes the need for the consuming app
|
|
@@ -222,9 +347,9 @@ interface RecordingResult {
|
|
|
222
347
|
/** Details about the mismatch, if any. */
|
|
223
348
|
mismatchDetails?: string;
|
|
224
349
|
/** The response produced during replay. */
|
|
225
|
-
replayResponse?: HandlerResponse;
|
|
350
|
+
replayResponse?: HandlerResponse$1;
|
|
226
351
|
/** The original response captured in the blob. */
|
|
227
|
-
capturedResponse?: HandlerResponse;
|
|
352
|
+
capturedResponse?: HandlerResponse$1;
|
|
228
353
|
/** Whether some recorded network calls were not consumed during replay. */
|
|
229
354
|
unconsumedNetworkCalls: boolean;
|
|
230
355
|
/** Details about unconsumed network calls, if any. */
|
|
@@ -276,4 +401,4 @@ interface SpawnRecordingContainerOptions {
|
|
|
276
401
|
*/
|
|
277
402
|
declare function spawnRecordingContainer(options: SpawnRecordingContainerOptions): Promise<string>;
|
|
278
403
|
|
|
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 };
|
|
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: {
|
|
@@ -426,6 +450,20 @@ async function finishRequest(requestContext, callbacks, response, options) {
|
|
|
426
450
|
};
|
|
427
451
|
}
|
|
428
452
|
|
|
453
|
+
// src/createRecordingRequestHandler.ts
|
|
454
|
+
function createRecordingRequestHandler(handler, options) {
|
|
455
|
+
return async (event, context) => {
|
|
456
|
+
const reqContext = startRequest(event);
|
|
457
|
+
try {
|
|
458
|
+
const response = await handler(event, context);
|
|
459
|
+
return await finishRequest(reqContext, options.callbacks, response, options);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
reqContext.cleanup();
|
|
462
|
+
throw err;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
429
467
|
// src/remoteCallbacks.ts
|
|
430
468
|
function remoteCallbacks(serviceUrl) {
|
|
431
469
|
const base = serviceUrl.replace(/\/+$/, "");
|
|
@@ -994,6 +1032,7 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
|
|
|
994
1032
|
return result;
|
|
995
1033
|
}
|
|
996
1034
|
export {
|
|
1035
|
+
createRecordingRequestHandler,
|
|
997
1036
|
createRequestRecording,
|
|
998
1037
|
ensureRequestRecording,
|
|
999
1038
|
finishRequest,
|