@replayio-app-building/netlify-recorder 0.4.0 → 0.6.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 +10 -23
- package/dist/index.d.ts +28 -3
- package/dist/index.js +56 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ The Netlify Recorder app (`https://netlify-recorder-bm4wmw.netlify.app`) provide
|
|
|
28
28
|
|
|
29
29
|
| Variable | Description | How to set |
|
|
30
30
|
|---|---|---|
|
|
31
|
-
| `
|
|
31
|
+
| `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 |
|
|
32
32
|
| `COMMIT_SHA` | The git commit hash of the deployed code | Set in your deploy script via `git rev-parse HEAD` |
|
|
33
33
|
| `BRANCH_NAME` | The git branch of the deployed code | Set in your deploy script via `git rev-parse --abbrev-ref HEAD` |
|
|
34
34
|
|
|
@@ -59,12 +59,7 @@ import type { Handler } from "@netlify/functions";
|
|
|
59
59
|
const RECORDER_URL = "https://netlify-recorder-bm4wmw.netlify.app";
|
|
60
60
|
|
|
61
61
|
const handler: Handler = async (event) => {
|
|
62
|
-
const reqContext = startRequest(
|
|
63
|
-
method: event.httpMethod,
|
|
64
|
-
url: event.path,
|
|
65
|
-
headers: event.headers as Record<string, string>,
|
|
66
|
-
body: event.body ?? undefined,
|
|
67
|
-
});
|
|
62
|
+
const reqContext = startRequest(event);
|
|
68
63
|
|
|
69
64
|
try {
|
|
70
65
|
const result = await myBusinessLogic();
|
|
@@ -143,7 +138,7 @@ If you need full control, you can manage your own blob storage, database, and re
|
|
|
143
138
|
|
|
144
139
|
### 1. Set required environment variables
|
|
145
140
|
|
|
146
|
-
Same as Option A — you must set `
|
|
141
|
+
Same as Option A — you must set `REPLAY_REPOSITORY_URL`, `COMMIT_SHA`, and `BRANCH_NAME` on your Netlify site. See the table in Option A, Step 1 above.
|
|
147
142
|
|
|
148
143
|
### 2. Wrap your Netlify function
|
|
149
144
|
|
|
@@ -154,12 +149,7 @@ import { startRequest, finishRequest } from "@replayio-app-building/netlify-reco
|
|
|
154
149
|
import type { Handler } from "@netlify/functions";
|
|
155
150
|
|
|
156
151
|
const handler: Handler = async (event) => {
|
|
157
|
-
const reqContext = startRequest(
|
|
158
|
-
method: event.httpMethod,
|
|
159
|
-
url: event.path,
|
|
160
|
-
headers: event.headers as Record<string, string>,
|
|
161
|
-
body: event.body ?? undefined,
|
|
162
|
-
});
|
|
152
|
+
const reqContext = startRequest(event);
|
|
163
153
|
|
|
164
154
|
try {
|
|
165
155
|
const result = await myBusinessLogic();
|
|
@@ -300,15 +290,12 @@ Self-hosted recording requires these environment variables:
|
|
|
300
290
|
|
|
301
291
|
## API Reference
|
|
302
292
|
|
|
303
|
-
### `startRequest(
|
|
293
|
+
### `startRequest(event): RequestContext`
|
|
304
294
|
|
|
305
|
-
Begins capturing a Netlify handler execution. Patches `globalThis.fetch` and `process.env` to record all outbound network calls and environment variable reads.
|
|
295
|
+
Begins capturing a Netlify handler execution. Accepts the raw Netlify event object and extracts all request metadata internally (method, path, headers, body, query string parameters, etc.). Patches `globalThis.fetch` and `process.env` to record all outbound network calls and environment variable reads.
|
|
306
296
|
|
|
307
297
|
**Parameters:**
|
|
308
|
-
- `
|
|
309
|
-
- `requestInfo.url` — Request URL/path
|
|
310
|
-
- `requestInfo.headers` — Request headers
|
|
311
|
-
- `requestInfo.body` — Optional request body
|
|
298
|
+
- `event` — The Netlify handler event object (passed directly)
|
|
312
299
|
|
|
313
300
|
**Returns:** A `RequestContext` object to pass to `finishRequest`.
|
|
314
301
|
|
|
@@ -316,7 +303,7 @@ Begins capturing a Netlify handler execution. Patches `globalThis.fetch` and `pr
|
|
|
316
303
|
|
|
317
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.
|
|
318
305
|
|
|
319
|
-
**Requires** the following environment variables (or equivalent options overrides): `COMMIT_SHA`, `BRANCH_NAME`, `
|
|
306
|
+
**Requires** the following environment variables (or equivalent options overrides): `COMMIT_SHA`, `BRANCH_NAME`, `REPLAY_REPOSITORY_URL`. Throws if any are missing.
|
|
320
307
|
|
|
321
308
|
**Parameters:**
|
|
322
309
|
- `requestContext` — The context returned by `startRequest`
|
|
@@ -325,7 +312,7 @@ Finalizes the request capture. Restores original `fetch` and `process.env`, seri
|
|
|
325
312
|
- `options.handlerPath` — Path to the handler file (used for recording metadata)
|
|
326
313
|
- `options.commitSha` — Override `COMMIT_SHA` env var
|
|
327
314
|
- `options.branchName` — Override `BRANCH_NAME` env var
|
|
328
|
-
- `options.repositoryUrl` — Override `
|
|
315
|
+
- `options.repositoryUrl` — Override `REPLAY_REPOSITORY_URL` env var
|
|
329
316
|
|
|
330
317
|
### `remoteCallbacks(serviceUrl): FinishRequestCallbacks`
|
|
331
318
|
|
|
@@ -364,7 +351,7 @@ These must be set on your Netlify site. Your deploy script should resolve them f
|
|
|
364
351
|
|---|---|---|
|
|
365
352
|
| `COMMIT_SHA` | Git commit hash of the deployed code | `git rev-parse HEAD` |
|
|
366
353
|
| `BRANCH_NAME` | Git branch of the deployed code | `git rev-parse --abbrev-ref HEAD` |
|
|
367
|
-
| `
|
|
354
|
+
| `REPLAY_REPOSITORY_URL` | Git repository URL (no embedded credentials) | `git remote get-url origin` (strip tokens) |
|
|
368
355
|
|
|
369
356
|
### Required for self-hosted recording (Option B)
|
|
370
357
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized request metadata stored in the blob.
|
|
3
|
+
* Populated internally by startRequest from the Netlify event.
|
|
4
|
+
*/
|
|
1
5
|
interface RequestInfo {
|
|
2
6
|
method: string;
|
|
3
7
|
url: string;
|
|
4
8
|
headers: Record<string, string>;
|
|
5
9
|
body?: string;
|
|
10
|
+
queryStringParameters?: Record<string, string | undefined> | null;
|
|
11
|
+
multiValueQueryStringParameters?: Record<string, string[] | undefined> | null;
|
|
12
|
+
rawUrl?: string;
|
|
13
|
+
rawQuery?: string;
|
|
14
|
+
isBase64Encoded?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Input type for startRequest. Mirrors the Netlify HandlerEvent shape so
|
|
18
|
+
* handlers can simply pass the event object directly.
|
|
19
|
+
*/
|
|
20
|
+
interface NetlifyEvent {
|
|
21
|
+
httpMethod: string;
|
|
22
|
+
path: string;
|
|
23
|
+
headers: Record<string, string | undefined>;
|
|
24
|
+
body?: string | null;
|
|
25
|
+
queryStringParameters?: Record<string, string | undefined> | null;
|
|
26
|
+
multiValueQueryStringParameters?: Record<string, string[] | undefined> | null;
|
|
27
|
+
rawUrl?: string;
|
|
28
|
+
rawQuery?: string;
|
|
29
|
+
isBase64Encoded?: boolean;
|
|
6
30
|
}
|
|
7
31
|
interface RequestContext {
|
|
8
32
|
requestInfo: RequestInfo;
|
|
@@ -92,6 +116,7 @@ interface EnsureRecordingOptions {
|
|
|
92
116
|
|
|
93
117
|
/**
|
|
94
118
|
* Called at the beginning of a Netlify handler execution.
|
|
119
|
+
* Accepts the raw Netlify event and extracts all request metadata internally.
|
|
95
120
|
* Installs interceptors on globalThis.fetch and process.env to capture
|
|
96
121
|
* outbound network calls and environment variable reads made by the handler.
|
|
97
122
|
* Returns a request context used by finishRequest.
|
|
@@ -101,7 +126,7 @@ interface EnsureRecordingOptions {
|
|
|
101
126
|
* capture interceptors to avoid overwriting the replay layer (which would
|
|
102
127
|
* also crash on Node v16 where Headers/Response don't exist).
|
|
103
128
|
*/
|
|
104
|
-
declare function startRequest(
|
|
129
|
+
declare function startRequest(event: NetlifyEvent): RequestContext;
|
|
105
130
|
|
|
106
131
|
interface HandlerResponse {
|
|
107
132
|
statusCode: number;
|
|
@@ -114,7 +139,7 @@ interface FinishRequestOptions {
|
|
|
114
139
|
commitSha?: string;
|
|
115
140
|
/** Override BRANCH_NAME env var. */
|
|
116
141
|
branchName?: string;
|
|
117
|
-
/** Override
|
|
142
|
+
/** Override REPLAY_REPOSITORY_URL env var. */
|
|
118
143
|
repositoryUrl?: string;
|
|
119
144
|
}
|
|
120
145
|
/**
|
|
@@ -227,4 +252,4 @@ interface SpawnRecordingContainerOptions {
|
|
|
227
252
|
*/
|
|
228
253
|
declare function spawnRecordingContainer(options: SpawnRecordingContainerOptions): Promise<string>;
|
|
229
254
|
|
|
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 };
|
|
255
|
+
export { type BlobData, type CapturedData, type ContainerInfraConfig, type EnsureRecordingOptions, type EnvRead, type FinishRequestCallbacks, type FinishRequestOptions, type NetlifyEvent, type NetworkCall, type RequestContext, type RequestInfo, type SpawnRecordingContainerOptions, createRequestRecording, ensureRequestRecording, finishRequest, readInfraConfigFromEnv, redactBlobData, remoteCallbacks, spawnRecordingContainer, startRequest };
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
8
|
// src/interceptors/network.ts
|
|
9
9
|
function installNetworkInterceptor(mode, calls) {
|
|
10
10
|
const originalFetch = globalThis.fetch;
|
|
11
|
+
let replayCallIndex = 0;
|
|
11
12
|
if (mode === "capture") {
|
|
12
13
|
const captureFetch = async (input, init) => {
|
|
13
14
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
@@ -39,14 +40,17 @@ function installNetworkInterceptor(mode, calls) {
|
|
|
39
40
|
};
|
|
40
41
|
globalThis.fetch = captureFetch;
|
|
41
42
|
} else {
|
|
42
|
-
let callIndex = 0;
|
|
43
43
|
const replayFetch = async () => {
|
|
44
|
-
const
|
|
44
|
+
const idx = replayCallIndex++;
|
|
45
|
+
const call = calls[idx];
|
|
45
46
|
if (!call) {
|
|
46
47
|
throw new Error(
|
|
47
48
|
`No more recorded network calls to replay (exhausted ${calls.length} calls)`
|
|
48
49
|
);
|
|
49
50
|
}
|
|
51
|
+
console.log(
|
|
52
|
+
` [network-replay] Consumed call ${idx + 1}/${calls.length}: ${call.method} ${call.url} => ${call.responseStatus}`
|
|
53
|
+
);
|
|
50
54
|
const body = call.responseBody ?? "";
|
|
51
55
|
const status = call.responseStatus;
|
|
52
56
|
return {
|
|
@@ -82,6 +86,12 @@ function installNetworkInterceptor(mode, calls) {
|
|
|
82
86
|
return {
|
|
83
87
|
restore() {
|
|
84
88
|
globalThis.fetch = originalFetch;
|
|
89
|
+
},
|
|
90
|
+
consumedCount() {
|
|
91
|
+
return replayCallIndex;
|
|
92
|
+
},
|
|
93
|
+
totalCount() {
|
|
94
|
+
return calls.length;
|
|
85
95
|
}
|
|
86
96
|
};
|
|
87
97
|
}
|
|
@@ -121,7 +131,24 @@ function installEnvironmentInterceptor(mode, reads) {
|
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
// src/startRequest.ts
|
|
124
|
-
function startRequest(
|
|
134
|
+
function startRequest(event) {
|
|
135
|
+
const headers = {};
|
|
136
|
+
for (const [key, value] of Object.entries(event.headers)) {
|
|
137
|
+
if (value !== void 0) {
|
|
138
|
+
headers[key] = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const requestInfo = {
|
|
142
|
+
method: event.httpMethod,
|
|
143
|
+
url: event.path,
|
|
144
|
+
headers,
|
|
145
|
+
body: event.body ?? void 0,
|
|
146
|
+
queryStringParameters: event.queryStringParameters ?? null,
|
|
147
|
+
multiValueQueryStringParameters: event.multiValueQueryStringParameters ?? null,
|
|
148
|
+
rawUrl: event.rawUrl,
|
|
149
|
+
rawQuery: event.rawQuery,
|
|
150
|
+
isBase64Encoded: event.isBase64Encoded
|
|
151
|
+
};
|
|
125
152
|
const capturedData = { networkCalls: [], envReads: [] };
|
|
126
153
|
const isReplay = globalThis.__REPLAY_RECORDING_MODE__ === true;
|
|
127
154
|
let cleanup;
|
|
@@ -180,6 +207,7 @@ var ENV_ALLOW_LIST = /* @__PURE__ */ new Set([
|
|
|
180
207
|
"DEPLOY_ID",
|
|
181
208
|
"DEPLOY_URL",
|
|
182
209
|
"REPOSITORY_URL",
|
|
210
|
+
"REPLAY_REPOSITORY_URL",
|
|
183
211
|
"APP_REPOSITORY_URL",
|
|
184
212
|
"BRANCH",
|
|
185
213
|
"HEAD",
|
|
@@ -277,11 +305,11 @@ async function finishRequest(requestContext, callbacks, response, options) {
|
|
|
277
305
|
}
|
|
278
306
|
const rawCommitSha = options?.commitSha ?? process.env.COMMIT_SHA;
|
|
279
307
|
const rawBranchName = options?.branchName ?? process.env.BRANCH_NAME;
|
|
280
|
-
const rawRepositoryUrl = options?.repositoryUrl ?? process.env.
|
|
308
|
+
const rawRepositoryUrl = options?.repositoryUrl ?? process.env.REPLAY_REPOSITORY_URL;
|
|
281
309
|
const missing = [];
|
|
282
310
|
if (!rawCommitSha) missing.push("COMMIT_SHA");
|
|
283
311
|
if (!rawBranchName) missing.push("BRANCH_NAME");
|
|
284
|
-
if (!rawRepositoryUrl) missing.push("
|
|
312
|
+
if (!rawRepositoryUrl) missing.push("REPLAY_REPOSITORY_URL");
|
|
285
313
|
if (missing.length > 0) {
|
|
286
314
|
throw new Error(
|
|
287
315
|
`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.`
|
|
@@ -653,9 +681,31 @@ async function createRequestRecording(blobUrlOrData, handlerPath, requestInfo) {
|
|
|
653
681
|
httpMethod: requestInfo.method,
|
|
654
682
|
path: requestInfo.url,
|
|
655
683
|
headers: requestInfo.headers,
|
|
656
|
-
body: requestInfo.body ?? null
|
|
684
|
+
body: requestInfo.body ?? null,
|
|
685
|
+
queryStringParameters: requestInfo.queryStringParameters ?? null,
|
|
686
|
+
multiValueQueryStringParameters: requestInfo.multiValueQueryStringParameters ?? null,
|
|
687
|
+
rawUrl: requestInfo.rawUrl ?? requestInfo.url,
|
|
688
|
+
rawQuery: requestInfo.rawQuery ?? "",
|
|
689
|
+
isBase64Encoded: requestInfo.isBase64Encoded ?? false
|
|
657
690
|
});
|
|
658
691
|
} finally {
|
|
692
|
+
const consumed = networkHandle.consumedCount();
|
|
693
|
+
const total = networkHandle.totalCount();
|
|
694
|
+
if (consumed < total) {
|
|
695
|
+
console.error(
|
|
696
|
+
`ERROR: Not all recorded network calls were consumed during replay. Consumed ${consumed} of ${total} calls. ${total - consumed} call(s) were never replayed.`
|
|
697
|
+
);
|
|
698
|
+
for (let i = consumed; i < total; i++) {
|
|
699
|
+
const call = blobData.capturedData.networkCalls[i];
|
|
700
|
+
if (call) {
|
|
701
|
+
console.error(
|
|
702
|
+
` Unconsumed call ${i + 1}/${total}: ${call.method} ${call.url} => ${call.responseStatus}`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
console.log(` [network-replay] All ${total} recorded network call(s) were consumed.`);
|
|
708
|
+
}
|
|
659
709
|
globalThis.__REPLAY_RECORDING_MODE__ = false;
|
|
660
710
|
networkHandle.restore();
|
|
661
711
|
envHandle.restore();
|