@harness-fe/runtime 3.0.1 → 3.2.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 +82 -11
- package/dist/capture.js +204 -85
- package/dist/client.js +2 -1
- package/dist/commands.js +178 -3
- package/package.json +3 -2
- package/src/capture.ts +233 -87
- package/src/client.ts +5 -1
- package/src/commands.ts +193 -6
- package/src/commandsFilter.test.ts +167 -0
- package/src/commandsNetwork.e2e.test.ts +146 -0
- package/src/runtimeClient.e2e.test.ts +264 -0
- package/dist/fetchPatch.d.ts +0 -39
- package/dist/fetchPatch.js +0 -311
- package/dist/xhrPatch.d.ts +0 -26
- package/dist/xhrPatch.js +0 -269
- package/src/fetchPatch.test.ts +0 -203
- package/src/fetchPatch.ts +0 -371
- package/src/xhrPatch.test.ts +0 -191
- package/src/xhrPatch.ts +0 -314
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 {
|
|
@@ -27,6 +38,9 @@ export declare class CaptureStore {
|
|
|
27
38
|
requestBodyTruncated?: boolean | undefined;
|
|
28
39
|
responseBodyTruncated?: boolean | undefined;
|
|
29
40
|
error?: string | undefined;
|
|
41
|
+
initiator?: {
|
|
42
|
+
stack?: string | undefined;
|
|
43
|
+
} | undefined;
|
|
30
44
|
}>;
|
|
31
45
|
readonly errors: RingBuffer<{
|
|
32
46
|
ts: number;
|
|
@@ -34,14 +48,71 @@ export declare class CaptureStore {
|
|
|
34
48
|
stack?: string | undefined;
|
|
35
49
|
source?: string | undefined;
|
|
36
50
|
}>;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
readonly ws: RingBuffer<{
|
|
52
|
+
ts: number;
|
|
53
|
+
id: string;
|
|
54
|
+
phase: "open" | "send" | "recv" | "close";
|
|
55
|
+
url: string;
|
|
56
|
+
protocols?: string[] | undefined;
|
|
57
|
+
payload?: unknown;
|
|
58
|
+
payloadTruncated?: boolean | undefined;
|
|
59
|
+
code?: number | undefined;
|
|
60
|
+
reason?: string | undefined;
|
|
61
|
+
wasClean?: boolean | undefined;
|
|
62
|
+
initiator?: {
|
|
63
|
+
stack?: string | undefined;
|
|
64
|
+
} | undefined;
|
|
65
|
+
}>;
|
|
66
|
+
readonly storage: RingBuffer<{
|
|
67
|
+
op: "set" | "remove" | "clear";
|
|
68
|
+
which: "local" | "session" | "cookie";
|
|
69
|
+
ts: number;
|
|
70
|
+
key?: string | undefined;
|
|
71
|
+
value?: string | undefined;
|
|
72
|
+
crossTab?: boolean | undefined;
|
|
73
|
+
initiator?: {
|
|
74
|
+
stack?: string | undefined;
|
|
75
|
+
} | undefined;
|
|
76
|
+
}>;
|
|
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?;
|
|
112
|
+
install(onEvent: (name: string, payload: unknown) => void, opts?: {
|
|
113
|
+
daemonUrl?: string;
|
|
114
|
+
}): void;
|
|
41
115
|
dispose(): void;
|
|
42
|
-
private
|
|
43
|
-
private installFetch;
|
|
44
|
-
private installXhr;
|
|
45
|
-
private installErrors;
|
|
116
|
+
private adapt;
|
|
46
117
|
}
|
|
47
118
|
export declare function getCaptureStore(): CaptureStore;
|
package/dist/capture.js
CHANGED
|
@@ -1,112 +1,231 @@
|
|
|
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
20
|
const CONSOLE_CAP = 500;
|
|
11
21
|
const NETWORK_CAP = 200;
|
|
12
22
|
const ERROR_CAP = 200;
|
|
23
|
+
const WS_CAP = 200;
|
|
24
|
+
const STORAGE_CAP = 200;
|
|
25
|
+
const NAVIGATION_CAP = 100;
|
|
26
|
+
const GLOBALS_CAP = 200;
|
|
27
|
+
const INDEXEDDB_CAP = 200;
|
|
13
28
|
export class CaptureStore {
|
|
14
29
|
console = new RingBuffer(CONSOLE_CAP);
|
|
15
30
|
network = new RingBuffer(NETWORK_CAP);
|
|
16
31
|
errors = new RingBuffer(ERROR_CAP);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
ws = new RingBuffer(WS_CAP);
|
|
33
|
+
storage = new RingBuffer(STORAGE_CAP);
|
|
34
|
+
navigation = new RingBuffer(NAVIGATION_CAP);
|
|
35
|
+
globals = new RingBuffer(GLOBALS_CAP);
|
|
36
|
+
indexeddb = new RingBuffer(INDEXEDDB_CAP);
|
|
37
|
+
handle;
|
|
38
|
+
install(onEvent, opts = {}) {
|
|
39
|
+
if (this.handle)
|
|
22
40
|
return;
|
|
23
|
-
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
41
|
+
const selfUrls = opts.daemonUrl ? [opts.daemonUrl] : undefined;
|
|
42
|
+
this.handle = installSandbox({
|
|
43
|
+
selfUrls,
|
|
44
|
+
onEvent: (e) => this.adapt(e, onEvent),
|
|
45
|
+
});
|
|
28
46
|
}
|
|
29
47
|
dispose() {
|
|
30
|
-
this.
|
|
31
|
-
this.
|
|
32
|
-
this.xhrDispose?.();
|
|
33
|
-
this.xhrDispose = undefined;
|
|
34
|
-
this.installed = false;
|
|
48
|
+
this.handle?.dispose();
|
|
49
|
+
this.handle = undefined;
|
|
35
50
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
adapt(e, onEvent) {
|
|
52
|
+
switch (e.source) {
|
|
53
|
+
case 'fetch':
|
|
54
|
+
case 'xhr': {
|
|
55
|
+
const entry = adaptFetchLike(e);
|
|
56
|
+
this.network.push(entry);
|
|
57
|
+
onEvent('network', entry);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
case 'ws': {
|
|
61
|
+
const entry = adaptWs(e);
|
|
62
|
+
this.ws.push(entry);
|
|
63
|
+
onEvent('ws', entry);
|
|
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);
|
|
46
76
|
this.console.push(entry);
|
|
47
77
|
onEvent('console', entry);
|
|
48
|
-
|
|
49
|
-
}
|
|
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
|
+
}
|
|
50
104
|
}
|
|
51
105
|
}
|
|
52
|
-
installFetch(onEvent) {
|
|
53
|
-
this.fetchDispose = installFetchPatch({
|
|
54
|
-
onEntry: (entry) => {
|
|
55
|
-
this.network.push(entry);
|
|
56
|
-
onEvent('network', entry);
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
installXhr(onEvent) {
|
|
61
|
-
this.xhrDispose = installXhrPatch({
|
|
62
|
-
onEntry: (entry) => {
|
|
63
|
-
this.network.push(entry);
|
|
64
|
-
onEvent('network', entry);
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
installErrors(onEvent) {
|
|
69
|
-
if (typeof window === 'undefined')
|
|
70
|
-
return;
|
|
71
|
-
window.addEventListener('error', (e) => {
|
|
72
|
-
const entry = {
|
|
73
|
-
ts: Date.now(),
|
|
74
|
-
message: e.message,
|
|
75
|
-
stack: e.error?.stack,
|
|
76
|
-
source: e.filename ? `${e.filename}:${e.lineno}:${e.colno}` : undefined,
|
|
77
|
-
};
|
|
78
|
-
this.errors.push(entry);
|
|
79
|
-
onEvent('error', entry);
|
|
80
|
-
});
|
|
81
|
-
window.addEventListener('unhandledrejection', (e) => {
|
|
82
|
-
const reason = e.reason;
|
|
83
|
-
const message = reason instanceof Error ? reason.message : String(reason ?? 'unhandled rejection');
|
|
84
|
-
const stack = reason instanceof Error ? reason.stack : undefined;
|
|
85
|
-
const entry = {
|
|
86
|
-
ts: Date.now(),
|
|
87
|
-
message: `Unhandled: ${message}`,
|
|
88
|
-
stack,
|
|
89
|
-
};
|
|
90
|
-
this.errors.push(entry);
|
|
91
|
-
onEvent('error', entry);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
106
|
}
|
|
95
107
|
let captureStoreSingleton;
|
|
96
108
|
export function getCaptureStore() {
|
|
97
109
|
captureStoreSingleton ??= new CaptureStore();
|
|
98
110
|
return captureStoreSingleton;
|
|
99
111
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
};
|
|
110
130
|
}
|
|
111
|
-
|
|
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
|
+
};
|
|
112
231
|
}
|
package/dist/client.js
CHANGED
|
@@ -111,7 +111,8 @@ export class RuntimeClient {
|
|
|
111
111
|
publishVisitorIdToWindow(this.visitorId);
|
|
112
112
|
}
|
|
113
113
|
start() {
|
|
114
|
-
this.
|
|
114
|
+
const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
|
115
|
+
this.ctx.capture.install((name, payload) => this.sendEvent(name, payload), { daemonUrl });
|
|
115
116
|
this.recorder.start();
|
|
116
117
|
this.connect();
|
|
117
118
|
}
|
package/dist/commands.js
CHANGED
|
@@ -276,17 +276,192 @@ export const commandHandlers = {
|
|
|
276
276
|
},
|
|
277
277
|
[COMMAND.CONSOLE_TAIL]: async (raw, ctx) => {
|
|
278
278
|
const args = raw;
|
|
279
|
-
|
|
279
|
+
const all = ctx.capture.console.tail(args.n ?? 20);
|
|
280
|
+
return { entries: filterTail(all, args, (e) => {
|
|
281
|
+
if (args.level && e.level !== args.level)
|
|
282
|
+
return undefined;
|
|
283
|
+
return JSON.stringify({ level: e.level, args: e.args });
|
|
284
|
+
}) };
|
|
280
285
|
},
|
|
281
286
|
[COMMAND.NETWORK_TAIL]: async (raw, ctx) => {
|
|
282
287
|
const args = raw;
|
|
283
|
-
|
|
288
|
+
const all = ctx.capture.network.tail(args.n ?? 20);
|
|
289
|
+
return { entries: filterTail(all, args, (e) => {
|
|
290
|
+
if (args.urlContains && !e.url.includes(args.urlContains))
|
|
291
|
+
return undefined;
|
|
292
|
+
if (args.method && e.method.toUpperCase() !== args.method.toUpperCase())
|
|
293
|
+
return undefined;
|
|
294
|
+
if (args.statusCode !== undefined && e.status !== args.statusCode)
|
|
295
|
+
return undefined;
|
|
296
|
+
return JSON.stringify({ url: e.url, method: e.method, requestBody: e.requestBody, responseBody: e.responseBody });
|
|
297
|
+
}) };
|
|
284
298
|
},
|
|
285
299
|
[COMMAND.ERRORS_TAIL]: async (raw, ctx) => {
|
|
286
300
|
const args = raw;
|
|
287
|
-
|
|
301
|
+
const all = ctx.capture.errors.tail(args.n ?? 20);
|
|
302
|
+
return { entries: filterTail(all, args, (e) => JSON.stringify({ message: e.message, stack: e.stack, source: e.source })) };
|
|
303
|
+
},
|
|
304
|
+
[COMMAND.WS_TAIL]: async (raw, ctx) => {
|
|
305
|
+
const args = raw;
|
|
306
|
+
const all = ctx.capture.ws.tail(args.n ?? 20);
|
|
307
|
+
return { entries: filterTail(all, args, (e) => {
|
|
308
|
+
if (args.phase && e.phase !== args.phase)
|
|
309
|
+
return undefined;
|
|
310
|
+
return JSON.stringify({ url: e.url, payload: e.payload, reason: e.reason });
|
|
311
|
+
}) };
|
|
312
|
+
},
|
|
313
|
+
[COMMAND.NETWORK_WAIT_FOR]: async (raw, ctx) => {
|
|
314
|
+
const args = raw;
|
|
315
|
+
const timeoutMs = args.timeoutMs ?? 10_000;
|
|
316
|
+
const deadline = Date.now() + timeoutMs;
|
|
317
|
+
const regex = args.urlRegex ? safeRegex(args.urlRegex) : undefined;
|
|
318
|
+
// Anchor on the existing buffer head so we only consider new requests
|
|
319
|
+
// (otherwise an old matching entry would resolve immediately).
|
|
320
|
+
const baselineLen = ctx.capture.network.size();
|
|
321
|
+
while (Date.now() < deadline) {
|
|
322
|
+
const all = ctx.capture.network.tail(500);
|
|
323
|
+
const newOnes = all.slice(Math.max(0, all.length - (ctx.capture.network.size() - baselineLen)));
|
|
324
|
+
for (const e of newOnes) {
|
|
325
|
+
if (args.urlContains && !e.url.includes(args.urlContains))
|
|
326
|
+
continue;
|
|
327
|
+
if (regex && !regex.test(e.url))
|
|
328
|
+
continue;
|
|
329
|
+
if (args.method && e.method.toUpperCase() !== args.method.toUpperCase())
|
|
330
|
+
continue;
|
|
331
|
+
if (args.statusCode !== undefined && e.status !== args.statusCode)
|
|
332
|
+
continue;
|
|
333
|
+
return { ok: true, entry: e, after: Date.now() };
|
|
334
|
+
}
|
|
335
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
336
|
+
}
|
|
337
|
+
throw new Error(`network.wait_for: no matching request within ${timeoutMs}ms`);
|
|
338
|
+
},
|
|
339
|
+
[COMMAND.NETWORK_WAIT_FOR_IDLE]: async (raw, ctx) => {
|
|
340
|
+
const args = raw;
|
|
341
|
+
const idleMs = args.idleMs ?? 500;
|
|
342
|
+
const timeoutMs = args.timeoutMs ?? 10_000;
|
|
343
|
+
const deadline = Date.now() + timeoutMs;
|
|
344
|
+
let lastSize = ctx.capture.network.size();
|
|
345
|
+
let stableSince = Date.now();
|
|
346
|
+
while (Date.now() < deadline) {
|
|
347
|
+
const currentSize = ctx.capture.network.size();
|
|
348
|
+
if (currentSize !== lastSize) {
|
|
349
|
+
lastSize = currentSize;
|
|
350
|
+
stableSince = Date.now();
|
|
351
|
+
}
|
|
352
|
+
else if (Date.now() - stableSince >= idleMs) {
|
|
353
|
+
return { ok: true, idleFor: Date.now() - stableSince, after: Date.now() };
|
|
354
|
+
}
|
|
355
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
356
|
+
}
|
|
357
|
+
throw new Error(`network.wait_for_idle: never quiet for ${idleMs}ms within ${timeoutMs}ms`);
|
|
358
|
+
},
|
|
359
|
+
[COMMAND.NETWORK_GET]: async (raw, ctx) => {
|
|
360
|
+
const args = raw;
|
|
361
|
+
// Return both req + res entries for this id (one or both may exist).
|
|
362
|
+
const all = ctx.capture.network.tail(200);
|
|
363
|
+
const matches = all.filter((e) => e.id === args.reqId);
|
|
364
|
+
return { entries: matches, found: matches.length > 0 };
|
|
365
|
+
},
|
|
366
|
+
[COMMAND.WS_GET]: async (raw, ctx) => {
|
|
367
|
+
const args = raw;
|
|
368
|
+
const all = ctx.capture.ws.tail(200);
|
|
369
|
+
const matches = all.filter((e) => e.id === args.wsId);
|
|
370
|
+
return { entries: matches, found: matches.length > 0 };
|
|
371
|
+
},
|
|
372
|
+
[COMMAND.STORAGE_TAIL]: async (raw, ctx) => {
|
|
373
|
+
const args = raw;
|
|
374
|
+
const all = ctx.capture.storage.tail(args.n ?? 20);
|
|
375
|
+
return { entries: filterTail(all, args, (e) => {
|
|
376
|
+
if (args.which && e.which !== args.which)
|
|
377
|
+
return undefined;
|
|
378
|
+
if (args.op && e.op !== args.op)
|
|
379
|
+
return undefined;
|
|
380
|
+
if (args.key && e.key !== args.key)
|
|
381
|
+
return undefined;
|
|
382
|
+
return JSON.stringify({ op: e.op, which: e.which, key: e.key, value: e.value });
|
|
383
|
+
}) };
|
|
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
|
+
}) };
|
|
288
417
|
},
|
|
289
418
|
};
|
|
419
|
+
/**
|
|
420
|
+
* Apply caller-supplied filtering to a tail() result. `pickHaystack` returns
|
|
421
|
+
* the string to match against (or `undefined` to drop the entry due to a
|
|
422
|
+
* type-specific narrow like `level` / `urlContains`). The shared `filter`
|
|
423
|
+
* string then runs as substring (default) or regex against the haystack.
|
|
424
|
+
*/
|
|
425
|
+
function safeRegex(source) {
|
|
426
|
+
try {
|
|
427
|
+
return new RegExp(source, 'i');
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function filterTail(items, args, pickHaystack) {
|
|
434
|
+
const filter = args.filter?.trim();
|
|
435
|
+
const useRegex = args.match === 'regex';
|
|
436
|
+
let regex;
|
|
437
|
+
if (filter && useRegex) {
|
|
438
|
+
try {
|
|
439
|
+
regex = new RegExp(filter, 'i');
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Invalid regex: fall back to substring match rather than throwing.
|
|
443
|
+
regex = undefined;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const out = [];
|
|
447
|
+
for (const item of items) {
|
|
448
|
+
const haystack = pickHaystack(item);
|
|
449
|
+
if (haystack === undefined)
|
|
450
|
+
continue;
|
|
451
|
+
if (filter) {
|
|
452
|
+
if (regex) {
|
|
453
|
+
if (!regex.test(haystack))
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
if (!haystack.toLowerCase().includes(filter.toLowerCase()))
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
out.push(item);
|
|
462
|
+
}
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
290
465
|
function truncate(s, n) {
|
|
291
466
|
if (s.length <= n)
|
|
292
467
|
return s;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-fe/runtime",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.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",
|