@harness-fe/runtime 3.1.0 → 3.3.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/dist/capture.d.ts +50 -14
- package/dist/capture.js +198 -118
- package/dist/client.d.ts +7 -0
- package/dist/client.js +5 -1
- package/dist/commands.js +33 -0
- package/dist/recording.d.ts +16 -1
- package/dist/recording.js +15 -1
- package/package.json +3 -2
- package/src/capture.ts +221 -121
- package/src/client.ts +15 -1
- package/src/commands.ts +30 -0
- package/src/recording.test.ts +57 -0
- package/src/recording.ts +31 -1
- package/src/runtimeClient.e2e.test.ts +20 -2
- package/dist/fetchPatch.d.ts +0 -39
- package/dist/fetchPatch.js +0 -314
- package/dist/initiator.d.ts +0 -20
- package/dist/initiator.js +0 -34
- package/dist/storagePatch.d.ts +0 -23
- package/dist/storagePatch.js +0 -190
- package/dist/wsPatch.d.ts +0 -26
- package/dist/wsPatch.js +0 -172
- package/dist/xhrPatch.d.ts +0 -26
- package/dist/xhrPatch.js +0 -272
- package/src/fetchPatch.test.ts +0 -217
- package/src/fetchPatch.ts +0 -374
- package/src/initiator.ts +0 -40
- package/src/storagePatch.test.ts +0 -137
- package/src/storagePatch.ts +0 -213
- package/src/wsPatch.test.ts +0 -150
- package/src/wsPatch.ts +0 -198
- package/src/xhrPatch.test.ts +0 -191
- package/src/xhrPatch.ts +0 -317
package/dist/capture.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* CaptureStore — thin adapter on top of `@harness-fe/sandbox`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Sandbox does the actual browser-API patching (fetch / xhr / ws / storage /
|
|
5
|
+
* navigation / console / errors / globals / indexeddb) and exposes a unified
|
|
6
|
+
* observer + interceptor surface. This module only:
|
|
7
|
+
* 1. Installs sandbox with the runtime's `onEvent` callback wired in.
|
|
8
|
+
* 2. Adapts each `SandboxEvent` into the harness-fe protocol shape
|
|
9
|
+
* (`NetworkEntry` / `WsEntry` / `StorageEntry` / `ConsoleEntry` /
|
|
10
|
+
* `ErrorEntry`) so the daemon's existing ingestion + tail tools keep
|
|
11
|
+
* working unchanged.
|
|
12
|
+
* 3. Mirrors events into bounded `RingBuffer`s so the runtime's
|
|
13
|
+
* `console.tail` / `network.tail` / `ws.tail` / `storage.tail` MCP tool
|
|
14
|
+
* handlers have something to read.
|
|
15
|
+
*
|
|
16
|
+
* Identity / interceptor / reentry safety all live in sandbox.
|
|
6
17
|
*/
|
|
7
18
|
import { RingBuffer } from './buffer.js';
|
|
8
19
|
export declare class CaptureStore {
|
|
@@ -63,20 +74,45 @@ export declare class CaptureStore {
|
|
|
63
74
|
stack?: string | undefined;
|
|
64
75
|
} | undefined;
|
|
65
76
|
}>;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
readonly navigation: RingBuffer<{
|
|
78
|
+
ts: number;
|
|
79
|
+
kind: "replace" | "push" | "pop" | "hash" | "assign";
|
|
80
|
+
url?: string | undefined;
|
|
81
|
+
state?: unknown;
|
|
82
|
+
replace?: boolean | undefined;
|
|
83
|
+
initiator?: {
|
|
84
|
+
stack?: string | undefined;
|
|
85
|
+
} | undefined;
|
|
86
|
+
}>;
|
|
87
|
+
readonly globals: RingBuffer<{
|
|
88
|
+
ts: number;
|
|
89
|
+
op: "set" | "get" | "delete";
|
|
90
|
+
key: string;
|
|
91
|
+
value?: unknown;
|
|
92
|
+
previousValue?: unknown;
|
|
93
|
+
initiator?: {
|
|
94
|
+
stack?: string | undefined;
|
|
95
|
+
} | undefined;
|
|
96
|
+
}>;
|
|
97
|
+
readonly indexeddb: RingBuffer<{
|
|
98
|
+
ts: number;
|
|
99
|
+
op: "open" | "clear" | "get" | "delete" | "put" | "add" | "getAll" | "cursor";
|
|
100
|
+
db?: string | undefined;
|
|
101
|
+
version?: number | undefined;
|
|
102
|
+
store?: string | undefined;
|
|
103
|
+
key?: unknown;
|
|
104
|
+
value?: unknown;
|
|
105
|
+
success?: boolean | undefined;
|
|
106
|
+
error?: string | undefined;
|
|
107
|
+
initiator?: {
|
|
108
|
+
stack?: string | undefined;
|
|
109
|
+
} | undefined;
|
|
110
|
+
}>;
|
|
111
|
+
private handle?;
|
|
71
112
|
install(onEvent: (name: string, payload: unknown) => void, opts?: {
|
|
72
113
|
daemonUrl?: string;
|
|
73
114
|
}): void;
|
|
74
115
|
dispose(): void;
|
|
75
|
-
private
|
|
76
|
-
private installFetch;
|
|
77
|
-
private installXhr;
|
|
78
|
-
private installWs;
|
|
79
|
-
private installStorage;
|
|
80
|
-
private installErrors;
|
|
116
|
+
private adapt;
|
|
81
117
|
}
|
|
82
118
|
export declare function getCaptureStore(): CaptureStore;
|
package/dist/capture.js
CHANGED
|
@@ -1,134 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* CaptureStore — thin adapter on top of `@harness-fe/sandbox`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Sandbox does the actual browser-API patching (fetch / xhr / ws / storage /
|
|
5
|
+
* navigation / console / errors / globals / indexeddb) and exposes a unified
|
|
6
|
+
* observer + interceptor surface. This module only:
|
|
7
|
+
* 1. Installs sandbox with the runtime's `onEvent` callback wired in.
|
|
8
|
+
* 2. Adapts each `SandboxEvent` into the harness-fe protocol shape
|
|
9
|
+
* (`NetworkEntry` / `WsEntry` / `StorageEntry` / `ConsoleEntry` /
|
|
10
|
+
* `ErrorEntry`) so the daemon's existing ingestion + tail tools keep
|
|
11
|
+
* working unchanged.
|
|
12
|
+
* 3. Mirrors events into bounded `RingBuffer`s so the runtime's
|
|
13
|
+
* `console.tail` / `network.tail` / `ws.tail` / `storage.tail` MCP tool
|
|
14
|
+
* handlers have something to read.
|
|
15
|
+
*
|
|
16
|
+
* Identity / interceptor / reentry safety all live in sandbox.
|
|
6
17
|
*/
|
|
18
|
+
import { installSandbox, } from '@harness-fe/sandbox';
|
|
7
19
|
import { RingBuffer } from './buffer.js';
|
|
8
|
-
import { installFetchPatch } from './fetchPatch.js';
|
|
9
|
-
import { installXhrPatch } from './xhrPatch.js';
|
|
10
|
-
import { installWsPatch } from './wsPatch.js';
|
|
11
|
-
import { installStoragePatch } from './storagePatch.js';
|
|
12
20
|
const CONSOLE_CAP = 500;
|
|
13
21
|
const NETWORK_CAP = 200;
|
|
14
22
|
const ERROR_CAP = 200;
|
|
15
23
|
const WS_CAP = 200;
|
|
16
24
|
const STORAGE_CAP = 200;
|
|
25
|
+
const NAVIGATION_CAP = 100;
|
|
26
|
+
const GLOBALS_CAP = 200;
|
|
27
|
+
const INDEXEDDB_CAP = 200;
|
|
17
28
|
export class CaptureStore {
|
|
18
29
|
console = new RingBuffer(CONSOLE_CAP);
|
|
19
30
|
network = new RingBuffer(NETWORK_CAP);
|
|
20
31
|
errors = new RingBuffer(ERROR_CAP);
|
|
21
32
|
ws = new RingBuffer(WS_CAP);
|
|
22
33
|
storage = new RingBuffer(STORAGE_CAP);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
storageDispose;
|
|
34
|
+
navigation = new RingBuffer(NAVIGATION_CAP);
|
|
35
|
+
globals = new RingBuffer(GLOBALS_CAP);
|
|
36
|
+
indexeddb = new RingBuffer(INDEXEDDB_CAP);
|
|
37
|
+
handle;
|
|
28
38
|
install(onEvent, opts = {}) {
|
|
29
|
-
if (this.
|
|
39
|
+
if (this.handle)
|
|
30
40
|
return;
|
|
31
|
-
|
|
32
|
-
this.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.installStorage(onEvent);
|
|
37
|
-
this.installErrors(onEvent);
|
|
41
|
+
const selfUrls = opts.daemonUrl ? [opts.daemonUrl] : undefined;
|
|
42
|
+
this.handle = installSandbox({
|
|
43
|
+
selfUrls,
|
|
44
|
+
onEvent: (e) => this.adapt(e, onEvent),
|
|
45
|
+
});
|
|
38
46
|
}
|
|
39
47
|
dispose() {
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
this.xhrDispose?.();
|
|
43
|
-
this.xhrDispose = undefined;
|
|
44
|
-
this.wsDispose?.();
|
|
45
|
-
this.wsDispose = undefined;
|
|
46
|
-
this.storageDispose?.();
|
|
47
|
-
this.storageDispose = undefined;
|
|
48
|
-
this.installed = false;
|
|
48
|
+
this.handle?.dispose();
|
|
49
|
+
this.handle = undefined;
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const entry = {
|
|
56
|
-
ts: Date.now(),
|
|
57
|
-
level,
|
|
58
|
-
args: args.map(safeClone),
|
|
59
|
-
};
|
|
60
|
-
this.console.push(entry);
|
|
61
|
-
onEvent('console', entry);
|
|
62
|
-
original(...args);
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
installFetch(onEvent) {
|
|
67
|
-
this.fetchDispose = installFetchPatch({
|
|
68
|
-
onEntry: (entry) => {
|
|
51
|
+
adapt(e, onEvent) {
|
|
52
|
+
switch (e.source) {
|
|
53
|
+
case 'fetch':
|
|
54
|
+
case 'xhr': {
|
|
55
|
+
const entry = adaptFetchLike(e);
|
|
69
56
|
this.network.push(entry);
|
|
70
57
|
onEvent('network', entry);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.xhrDispose = installXhrPatch({
|
|
76
|
-
onEntry: (entry) => {
|
|
77
|
-
this.network.push(entry);
|
|
78
|
-
onEvent('network', entry);
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
installWs(onEvent, daemonUrl) {
|
|
83
|
-
// Add the daemon URL itself to the denylist so our own bridge
|
|
84
|
-
// connection isn't intercepted (otherwise every event we send
|
|
85
|
-
// would emit a `ws send` that loops back into the outbox).
|
|
86
|
-
const extra = [];
|
|
87
|
-
if (daemonUrl) {
|
|
88
|
-
const escaped = daemonUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
89
|
-
extra.push(new RegExp(`^${escaped}`));
|
|
90
|
-
}
|
|
91
|
-
this.wsDispose = installWsPatch({
|
|
92
|
-
denylist: extra.length > 0 ? [/\/__hfe\//, /sockjs-node/, ...extra] : undefined,
|
|
93
|
-
onEntry: (entry) => {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
case 'ws': {
|
|
61
|
+
const entry = adaptWs(e);
|
|
94
62
|
this.ws.push(entry);
|
|
95
63
|
onEvent('ws', entry);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
case 'storage': {
|
|
67
|
+
const entry = adaptStorage(e);
|
|
68
|
+
if (entry) {
|
|
69
|
+
this.storage.push(entry);
|
|
70
|
+
onEvent('storage', entry);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
case 'console': {
|
|
75
|
+
const entry = adaptConsole(e);
|
|
76
|
+
this.console.push(entry);
|
|
77
|
+
onEvent('console', entry);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
case 'errors': {
|
|
81
|
+
const entry = adaptError(e);
|
|
82
|
+
this.errors.push(entry);
|
|
83
|
+
onEvent('error', entry);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
case 'navigation': {
|
|
87
|
+
const entry = adaptNavigation(e);
|
|
88
|
+
this.navigation.push(entry);
|
|
89
|
+
onEvent('navigation', entry);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
case 'globals': {
|
|
93
|
+
const entry = adaptGlobals(e);
|
|
94
|
+
this.globals.push(entry);
|
|
95
|
+
onEvent('globals', entry);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
case 'indexeddb': {
|
|
99
|
+
const entry = adaptIndexedDb(e);
|
|
100
|
+
this.indexeddb.push(entry);
|
|
101
|
+
onEvent('indexeddb', entry);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
132
105
|
}
|
|
133
106
|
}
|
|
134
107
|
let captureStoreSingleton;
|
|
@@ -136,16 +109,123 @@ export function getCaptureStore() {
|
|
|
136
109
|
captureStoreSingleton ??= new CaptureStore();
|
|
137
110
|
return captureStoreSingleton;
|
|
138
111
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
112
|
+
// ────────────────────────────────────────────────────────────────────
|
|
113
|
+
// SandboxEvent → harness-fe protocol entry adapters
|
|
114
|
+
// ────────────────────────────────────────────────────────────────────
|
|
115
|
+
function adaptFetchLike(e) {
|
|
116
|
+
const d = e.data;
|
|
117
|
+
if (e.kind === 'req') {
|
|
118
|
+
const r = d;
|
|
119
|
+
return {
|
|
120
|
+
ts: e.ts,
|
|
121
|
+
id: r.id,
|
|
122
|
+
phase: 'req',
|
|
123
|
+
method: r.method,
|
|
124
|
+
url: r.url,
|
|
125
|
+
requestHeaders: r.headers,
|
|
126
|
+
requestBody: r.body,
|
|
127
|
+
requestBodyTruncated: r.bodyTruncated || undefined,
|
|
128
|
+
initiator: e.initiator,
|
|
129
|
+
};
|
|
149
130
|
}
|
|
150
|
-
|
|
131
|
+
const r = d;
|
|
132
|
+
return {
|
|
133
|
+
ts: e.ts,
|
|
134
|
+
id: r.id,
|
|
135
|
+
phase: 'res',
|
|
136
|
+
method: r.method,
|
|
137
|
+
url: r.url,
|
|
138
|
+
status: r.status,
|
|
139
|
+
durationMs: r.durationMs,
|
|
140
|
+
responseHeaders: r.headers,
|
|
141
|
+
responseBody: r.body,
|
|
142
|
+
responseBodyTruncated: r.bodyTruncated || undefined,
|
|
143
|
+
error: r.error,
|
|
144
|
+
initiator: e.initiator,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function adaptWs(e) {
|
|
148
|
+
const d = e.data;
|
|
149
|
+
return {
|
|
150
|
+
ts: e.ts,
|
|
151
|
+
id: d.id,
|
|
152
|
+
phase: d.phase,
|
|
153
|
+
url: d.url,
|
|
154
|
+
protocols: d.protocols,
|
|
155
|
+
payload: d.payload,
|
|
156
|
+
payloadTruncated: d.payloadTruncated,
|
|
157
|
+
code: d.code,
|
|
158
|
+
reason: d.reason,
|
|
159
|
+
wasClean: d.wasClean,
|
|
160
|
+
initiator: e.initiator,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function adaptStorage(e) {
|
|
164
|
+
const d = e.data;
|
|
165
|
+
// Sandbox emits a 'get' op; harness-fe protocol storage only models set/remove/clear.
|
|
166
|
+
if (d.op === 'get')
|
|
167
|
+
return null;
|
|
168
|
+
return {
|
|
169
|
+
ts: e.ts,
|
|
170
|
+
op: d.op,
|
|
171
|
+
which: d.which,
|
|
172
|
+
key: d.key,
|
|
173
|
+
value: d.value,
|
|
174
|
+
crossTab: d.crossTab,
|
|
175
|
+
initiator: e.initiator,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function adaptConsole(e) {
|
|
179
|
+
const d = e.data;
|
|
180
|
+
return {
|
|
181
|
+
ts: e.ts,
|
|
182
|
+
level: d.level,
|
|
183
|
+
args: d.args,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function adaptError(e) {
|
|
187
|
+
const d = e.data;
|
|
188
|
+
return {
|
|
189
|
+
ts: e.ts,
|
|
190
|
+
message: d.message,
|
|
191
|
+
stack: d.stack,
|
|
192
|
+
source: d.source,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function adaptNavigation(e) {
|
|
196
|
+
const d = e.data;
|
|
197
|
+
return {
|
|
198
|
+
ts: e.ts,
|
|
199
|
+
kind: d.kind,
|
|
200
|
+
url: d.url,
|
|
201
|
+
state: d.state,
|
|
202
|
+
replace: d.replace,
|
|
203
|
+
initiator: e.initiator,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function adaptGlobals(e) {
|
|
207
|
+
const d = e.data;
|
|
208
|
+
return {
|
|
209
|
+
ts: e.ts,
|
|
210
|
+
op: d.op,
|
|
211
|
+
key: d.key,
|
|
212
|
+
value: d.value,
|
|
213
|
+
previousValue: d.previousValue,
|
|
214
|
+
initiator: e.initiator,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function adaptIndexedDb(e) {
|
|
218
|
+
const d = e.data;
|
|
219
|
+
return {
|
|
220
|
+
ts: e.ts,
|
|
221
|
+
op: d.op,
|
|
222
|
+
db: d.db,
|
|
223
|
+
version: d.version,
|
|
224
|
+
store: d.store,
|
|
225
|
+
key: d.key,
|
|
226
|
+
value: d.value,
|
|
227
|
+
success: d.success,
|
|
228
|
+
error: d.error,
|
|
229
|
+
initiator: e.initiator,
|
|
230
|
+
};
|
|
151
231
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -28,6 +28,13 @@ export interface ClientOptions {
|
|
|
28
28
|
* by visitorId). Propagated by HarnessScript via window.__HARNESS_FE__.userId.
|
|
29
29
|
*/
|
|
30
30
|
userId?: string;
|
|
31
|
+
/**
|
|
32
|
+
* How often (in ms) rrweb should emit a fresh FullSnapshot baseline.
|
|
33
|
+
* Defaults to 30 minutes. Set to 0 to disable periodic baselines (the
|
|
34
|
+
* recorder still emits one at start() and one per ws reconnect).
|
|
35
|
+
* See {@link RrwebRecorderOptions.checkoutEveryNms} for the trade-off.
|
|
36
|
+
*/
|
|
37
|
+
rrwebCheckoutEveryNms?: number;
|
|
31
38
|
}
|
|
32
39
|
export { tryInheritFromParent } from './parent-inherit.js';
|
|
33
40
|
export type { ParentInheritance } from './parent-inherit.js';
|
package/dist/client.js
CHANGED
|
@@ -90,7 +90,10 @@ export class RuntimeClient {
|
|
|
90
90
|
}
|
|
91
91
|
pageLoadSent = false;
|
|
92
92
|
ctx = { capture: getCaptureStore() };
|
|
93
|
-
|
|
93
|
+
// Initialized in constructor (parameter property `opts` isn't readable at
|
|
94
|
+
// class-field-initializer time — field initializers run before parameter
|
|
95
|
+
// property assignment).
|
|
96
|
+
recorder;
|
|
94
97
|
reconnectAttempts = 0;
|
|
95
98
|
closed = false;
|
|
96
99
|
static MAX_OUTBOX_FRAMES = 500;
|
|
@@ -109,6 +112,7 @@ export class RuntimeClient {
|
|
|
109
112
|
const inheritedVisitor = tryInheritVisitorFromParent();
|
|
110
113
|
this.visitorId = inheritedVisitor ?? getOrCreateVisitorId();
|
|
111
114
|
publishVisitorIdToWindow(this.visitorId);
|
|
115
|
+
this.recorder = new RrwebRecorder((chunk) => this.sendEvent(EVENT_NAME.RRWEB, chunk), { checkoutEveryNms: opts.rrwebCheckoutEveryNms });
|
|
112
116
|
}
|
|
113
117
|
start() {
|
|
114
118
|
const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
package/dist/commands.js
CHANGED
|
@@ -382,6 +382,39 @@ export const commandHandlers = {
|
|
|
382
382
|
return JSON.stringify({ op: e.op, which: e.which, key: e.key, value: e.value });
|
|
383
383
|
}) };
|
|
384
384
|
},
|
|
385
|
+
[COMMAND.NAVIGATION_TAIL]: async (raw, ctx) => {
|
|
386
|
+
const args = raw;
|
|
387
|
+
const all = ctx.capture.navigation.tail(args.n ?? 20);
|
|
388
|
+
return { entries: filterTail(all, args, (e) => {
|
|
389
|
+
if (args.kind && e.kind !== args.kind)
|
|
390
|
+
return undefined;
|
|
391
|
+
return JSON.stringify({ kind: e.kind, url: e.url, replace: e.replace });
|
|
392
|
+
}) };
|
|
393
|
+
},
|
|
394
|
+
[COMMAND.GLOBALS_TAIL]: async (raw, ctx) => {
|
|
395
|
+
const args = raw;
|
|
396
|
+
const all = ctx.capture.globals.tail(args.n ?? 20);
|
|
397
|
+
return { entries: filterTail(all, args, (e) => {
|
|
398
|
+
if (args.op && e.op !== args.op)
|
|
399
|
+
return undefined;
|
|
400
|
+
if (args.key && e.key !== args.key)
|
|
401
|
+
return undefined;
|
|
402
|
+
return JSON.stringify({ op: e.op, key: e.key, value: e.value });
|
|
403
|
+
}) };
|
|
404
|
+
},
|
|
405
|
+
[COMMAND.INDEXEDDB_TAIL]: async (raw, ctx) => {
|
|
406
|
+
const args = raw;
|
|
407
|
+
const all = ctx.capture.indexeddb.tail(args.n ?? 20);
|
|
408
|
+
return { entries: filterTail(all, args, (e) => {
|
|
409
|
+
if (args.op && e.op !== args.op)
|
|
410
|
+
return undefined;
|
|
411
|
+
if (args.store && e.store !== args.store)
|
|
412
|
+
return undefined;
|
|
413
|
+
if (args.db && e.db !== args.db)
|
|
414
|
+
return undefined;
|
|
415
|
+
return JSON.stringify({ op: e.op, store: e.store, key: e.key });
|
|
416
|
+
}) };
|
|
417
|
+
},
|
|
385
418
|
};
|
|
386
419
|
/**
|
|
387
420
|
* Apply caller-supplied filtering to a tail() result. `pickHaystack` returns
|
package/dist/recording.d.ts
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import type { RrwebChunkPayload } from '@harness-fe/protocol';
|
|
2
2
|
export { RRWEB_FULL_SNAPSHOT_TYPE, chunkHasFullSnapshot } from './rrweb-types.js';
|
|
3
|
+
export interface RrwebRecorderOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Force rrweb to emit a fresh FullSnapshot every N milliseconds. Caps how
|
|
6
|
+
* stale the most recent baseline can be, so window replays mid-session
|
|
7
|
+
* don't have to roll forward from a baseline that's potentially hours old.
|
|
8
|
+
*
|
|
9
|
+
* Set to `0` (or a negative number) to disable periodic baselines and
|
|
10
|
+
* rely solely on the start() baseline + reconnect baselines. Useful for
|
|
11
|
+
* extremely bandwidth-constrained deployments.
|
|
12
|
+
*
|
|
13
|
+
* @default 30 * 60 * 1000 (30 minutes)
|
|
14
|
+
*/
|
|
15
|
+
checkoutEveryNms?: number;
|
|
16
|
+
}
|
|
3
17
|
export declare class RrwebRecorder {
|
|
4
18
|
private readonly onChunk;
|
|
19
|
+
private readonly opts;
|
|
5
20
|
private stopRecording?;
|
|
6
21
|
private flushTimer?;
|
|
7
22
|
private chunkSeq;
|
|
8
23
|
private buffer;
|
|
9
|
-
constructor(onChunk: (chunk: RrwebChunkPayload) => void);
|
|
24
|
+
constructor(onChunk: (chunk: RrwebChunkPayload) => void, opts?: RrwebRecorderOptions);
|
|
10
25
|
start(): void;
|
|
11
26
|
stop(): void;
|
|
12
27
|
/**
|
package/dist/recording.js
CHANGED
|
@@ -2,24 +2,38 @@ import { record } from 'rrweb';
|
|
|
2
2
|
export { RRWEB_FULL_SNAPSHOT_TYPE, chunkHasFullSnapshot } from './rrweb-types.js';
|
|
3
3
|
const FLUSH_MS = 5_000;
|
|
4
4
|
const MAX_EVENTS = 200;
|
|
5
|
+
// Default periodic-baseline cadence. Long-running sessions otherwise rely on
|
|
6
|
+
// a single FullSnapshot at start() + one per ws reconnect, which makes
|
|
7
|
+
// mid-session window replays expensive (rrweb has to roll forward all
|
|
8
|
+
// incremental events back to the original baseline) and leaves a window of
|
|
9
|
+
// vulnerability if the original baseline is ever evicted from the outbox.
|
|
10
|
+
// 30 min is a deliberate middle ground: ~16 baselines per 8h session at
|
|
11
|
+
// ~500KB each ≈ 8MB extra storage, which is acceptable for a dev tool.
|
|
12
|
+
const DEFAULT_CHECKOUT_EVERY_MS = 30 * 60 * 1000;
|
|
5
13
|
export class RrwebRecorder {
|
|
6
14
|
onChunk;
|
|
15
|
+
opts;
|
|
7
16
|
stopRecording;
|
|
8
17
|
flushTimer;
|
|
9
18
|
chunkSeq = 0;
|
|
10
19
|
buffer = [];
|
|
11
|
-
constructor(onChunk) {
|
|
20
|
+
constructor(onChunk, opts = {}) {
|
|
12
21
|
this.onChunk = onChunk;
|
|
22
|
+
this.opts = opts;
|
|
13
23
|
}
|
|
14
24
|
start() {
|
|
15
25
|
if (this.stopRecording)
|
|
16
26
|
return;
|
|
27
|
+
const checkoutEveryNms = this.opts.checkoutEveryNms ?? DEFAULT_CHECKOUT_EVERY_MS;
|
|
28
|
+
// rrweb interprets `checkoutEveryNms` falsy / undefined as "off".
|
|
29
|
+
// Pass undefined when disabled so we get the native off-path.
|
|
17
30
|
this.stopRecording = record({
|
|
18
31
|
emit: (event) => this.push(event),
|
|
19
32
|
inlineImages: false,
|
|
20
33
|
recordCanvas: false,
|
|
21
34
|
collectFonts: false,
|
|
22
35
|
maskAllInputs: false,
|
|
36
|
+
checkoutEveryNms: checkoutEveryNms > 0 ? checkoutEveryNms : undefined,
|
|
23
37
|
});
|
|
24
38
|
this.flushTimer = window.setInterval(() => this.flush(), FLUSH_MS);
|
|
25
39
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-fe/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Browser-side SDK injected into the dev page. Connects to the MCP server via WebSocket and executes commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@zumer/snapdom": "^2.12.0",
|
|
32
32
|
"rrweb": "2.0.0-alpha.4",
|
|
33
|
-
"@harness-fe/protocol": "3.
|
|
33
|
+
"@harness-fe/protocol": "3.2.0",
|
|
34
|
+
"@harness-fe/sandbox": "^3.2.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"happy-dom": "^20.9.0",
|