web-terminal-agent 1.0.0 → 1.1.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 +29 -49
- package/index.js +92 -20
- package/package.json +6 -4
- package/public/__manus__/debug-collector.js +821 -0
- package/public/assets/index-BwC69ZBZ.js +515 -0
- package/public/assets/index-CqQn1g4I.css +1 -0
- package/public/images/connection-orb.png +0 -0
- package/public/images/crt-background.png +0 -0
- package/public/images/terminal-frame.png +0 -0
- package/public/index.html +81 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manus Debug Collector (agent-friendly)
|
|
3
|
+
*
|
|
4
|
+
* Captures:
|
|
5
|
+
* 1) Console logs
|
|
6
|
+
* 2) Network requests (fetch + XHR)
|
|
7
|
+
* 3) User interactions (semantic uiEvents: click/type/submit/nav/scroll/etc.)
|
|
8
|
+
*
|
|
9
|
+
* Data is periodically sent to /__manus__/logs
|
|
10
|
+
* Note: uiEvents are mirrored to sessionEvents for sessionReplay.log
|
|
11
|
+
*/
|
|
12
|
+
(function () {
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
// Prevent double initialization
|
|
16
|
+
if (window.__MANUS_DEBUG_COLLECTOR__) return;
|
|
17
|
+
|
|
18
|
+
// ==========================================================================
|
|
19
|
+
// Configuration
|
|
20
|
+
// ==========================================================================
|
|
21
|
+
const CONFIG = {
|
|
22
|
+
reportEndpoint: "/__manus__/logs",
|
|
23
|
+
bufferSize: {
|
|
24
|
+
console: 500,
|
|
25
|
+
network: 200,
|
|
26
|
+
// semantic, agent-friendly UI events
|
|
27
|
+
ui: 500,
|
|
28
|
+
},
|
|
29
|
+
reportInterval: 2000,
|
|
30
|
+
sensitiveFields: [
|
|
31
|
+
"password",
|
|
32
|
+
"token",
|
|
33
|
+
"secret",
|
|
34
|
+
"key",
|
|
35
|
+
"authorization",
|
|
36
|
+
"cookie",
|
|
37
|
+
"session",
|
|
38
|
+
],
|
|
39
|
+
maxBodyLength: 10240,
|
|
40
|
+
// UI event logging privacy policy:
|
|
41
|
+
// - inputs matching sensitiveFields or type=password are masked by default
|
|
42
|
+
// - non-sensitive inputs log up to 200 chars
|
|
43
|
+
uiInputMaxLen: 200,
|
|
44
|
+
uiTextMaxLen: 80,
|
|
45
|
+
// Scroll throttling: minimum ms between scroll events
|
|
46
|
+
scrollThrottleMs: 500,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ==========================================================================
|
|
50
|
+
// Storage
|
|
51
|
+
// ==========================================================================
|
|
52
|
+
const store = {
|
|
53
|
+
consoleLogs: [],
|
|
54
|
+
networkRequests: [],
|
|
55
|
+
uiEvents: [],
|
|
56
|
+
lastReportTime: Date.now(),
|
|
57
|
+
lastScrollTime: 0,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ==========================================================================
|
|
61
|
+
// Utility Functions
|
|
62
|
+
// ==========================================================================
|
|
63
|
+
|
|
64
|
+
function sanitizeValue(value, depth) {
|
|
65
|
+
if (depth === void 0) depth = 0;
|
|
66
|
+
if (depth > 5) return "[Max Depth]";
|
|
67
|
+
if (value === null) return null;
|
|
68
|
+
if (value === undefined) return undefined;
|
|
69
|
+
|
|
70
|
+
if (typeof value === "string") {
|
|
71
|
+
return value.length > 1000 ? value.slice(0, 1000) + "...[truncated]" : value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof value !== "object") return value;
|
|
75
|
+
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
return value.slice(0, 100).map(function (v) {
|
|
78
|
+
return sanitizeValue(v, depth + 1);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
var sanitized = {};
|
|
83
|
+
for (var k in value) {
|
|
84
|
+
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
|
85
|
+
var isSensitive = CONFIG.sensitiveFields.some(function (f) {
|
|
86
|
+
return k.toLowerCase().indexOf(f) !== -1;
|
|
87
|
+
});
|
|
88
|
+
if (isSensitive) {
|
|
89
|
+
sanitized[k] = "[REDACTED]";
|
|
90
|
+
} else {
|
|
91
|
+
sanitized[k] = sanitizeValue(value[k], depth + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return sanitized;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatArg(arg) {
|
|
99
|
+
try {
|
|
100
|
+
if (arg instanceof Error) {
|
|
101
|
+
return { type: "Error", message: arg.message, stack: arg.stack };
|
|
102
|
+
}
|
|
103
|
+
if (typeof arg === "object") return sanitizeValue(arg);
|
|
104
|
+
return String(arg);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return "[Unserializable]";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatArgs(args) {
|
|
111
|
+
var result = [];
|
|
112
|
+
for (var i = 0; i < args.length; i++) result.push(formatArg(args[i]));
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function pruneBuffer(buffer, maxSize) {
|
|
117
|
+
if (buffer.length > maxSize) buffer.splice(0, buffer.length - maxSize);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function tryParseJson(str) {
|
|
121
|
+
if (typeof str !== "string") return str;
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(str);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return str;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ==========================================================================
|
|
130
|
+
// Semantic UI Event Logging (agent-friendly)
|
|
131
|
+
// ==========================================================================
|
|
132
|
+
|
|
133
|
+
function shouldIgnoreTarget(target) {
|
|
134
|
+
try {
|
|
135
|
+
if (!target || !(target instanceof Element)) return false;
|
|
136
|
+
return !!target.closest(".manus-no-record");
|
|
137
|
+
} catch (e) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function compactText(s, maxLen) {
|
|
143
|
+
try {
|
|
144
|
+
var t = (s || "").trim().replace(/\s+/g, " ");
|
|
145
|
+
if (!t) return "";
|
|
146
|
+
return t.length > maxLen ? t.slice(0, maxLen) + "…" : t;
|
|
147
|
+
} catch (e) {
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function elText(el) {
|
|
153
|
+
try {
|
|
154
|
+
var t = el.innerText || el.textContent || "";
|
|
155
|
+
return compactText(t, CONFIG.uiTextMaxLen);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function describeElement(el) {
|
|
162
|
+
if (!el || !(el instanceof Element)) return null;
|
|
163
|
+
|
|
164
|
+
var getAttr = function (name) {
|
|
165
|
+
return el.getAttribute(name);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
var tag = el.tagName ? el.tagName.toLowerCase() : null;
|
|
169
|
+
var id = el.id || null;
|
|
170
|
+
var name = getAttr("name") || null;
|
|
171
|
+
var role = getAttr("role") || null;
|
|
172
|
+
var ariaLabel = getAttr("aria-label") || null;
|
|
173
|
+
|
|
174
|
+
var dataLoc = getAttr("data-loc") || null;
|
|
175
|
+
var testId =
|
|
176
|
+
getAttr("data-testid") ||
|
|
177
|
+
getAttr("data-test-id") ||
|
|
178
|
+
getAttr("data-test") ||
|
|
179
|
+
null;
|
|
180
|
+
|
|
181
|
+
var type = tag === "input" ? (getAttr("type") || "text") : null;
|
|
182
|
+
var href = tag === "a" ? getAttr("href") || null : null;
|
|
183
|
+
|
|
184
|
+
// a small, stable hint for agents (avoid building full CSS paths)
|
|
185
|
+
var selectorHint = null;
|
|
186
|
+
if (testId) selectorHint = '[data-testid="' + testId + '"]';
|
|
187
|
+
else if (dataLoc) selectorHint = '[data-loc="' + dataLoc + '"]';
|
|
188
|
+
else if (id) selectorHint = "#" + id;
|
|
189
|
+
else selectorHint = tag || "unknown";
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
tag: tag,
|
|
193
|
+
id: id,
|
|
194
|
+
name: name,
|
|
195
|
+
type: type,
|
|
196
|
+
role: role,
|
|
197
|
+
ariaLabel: ariaLabel,
|
|
198
|
+
testId: testId,
|
|
199
|
+
dataLoc: dataLoc,
|
|
200
|
+
href: href,
|
|
201
|
+
text: elText(el),
|
|
202
|
+
selectorHint: selectorHint,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isSensitiveField(el) {
|
|
207
|
+
if (!el || !(el instanceof Element)) return false;
|
|
208
|
+
var tag = el.tagName ? el.tagName.toLowerCase() : "";
|
|
209
|
+
if (tag !== "input" && tag !== "textarea") return false;
|
|
210
|
+
|
|
211
|
+
var type = (el.getAttribute("type") || "").toLowerCase();
|
|
212
|
+
if (type === "password") return true;
|
|
213
|
+
|
|
214
|
+
var name = (el.getAttribute("name") || "").toLowerCase();
|
|
215
|
+
var id = (el.id || "").toLowerCase();
|
|
216
|
+
|
|
217
|
+
return CONFIG.sensitiveFields.some(function (f) {
|
|
218
|
+
return name.indexOf(f) !== -1 || id.indexOf(f) !== -1;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getInputValueSafe(el) {
|
|
223
|
+
if (!el || !(el instanceof Element)) return null;
|
|
224
|
+
var tag = el.tagName ? el.tagName.toLowerCase() : "";
|
|
225
|
+
if (tag !== "input" && tag !== "textarea" && tag !== "select") return null;
|
|
226
|
+
|
|
227
|
+
var v = "";
|
|
228
|
+
try {
|
|
229
|
+
v = el.value != null ? String(el.value) : "";
|
|
230
|
+
} catch (e) {
|
|
231
|
+
v = "";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (isSensitiveField(el)) return { masked: true, length: v.length };
|
|
235
|
+
|
|
236
|
+
if (v.length > CONFIG.uiInputMaxLen) v = v.slice(0, CONFIG.uiInputMaxLen) + "…";
|
|
237
|
+
return v;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function logUiEvent(kind, payload) {
|
|
241
|
+
var entry = {
|
|
242
|
+
timestamp: Date.now(),
|
|
243
|
+
kind: kind,
|
|
244
|
+
url: location.href,
|
|
245
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
246
|
+
payload: sanitizeValue(payload),
|
|
247
|
+
};
|
|
248
|
+
store.uiEvents.push(entry);
|
|
249
|
+
pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function installUiEventListeners() {
|
|
253
|
+
// Clicks
|
|
254
|
+
document.addEventListener(
|
|
255
|
+
"click",
|
|
256
|
+
function (e) {
|
|
257
|
+
var t = e.target;
|
|
258
|
+
if (shouldIgnoreTarget(t)) return;
|
|
259
|
+
logUiEvent("click", {
|
|
260
|
+
target: describeElement(t),
|
|
261
|
+
x: e.clientX,
|
|
262
|
+
y: e.clientY,
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
true
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Typing "commit" events
|
|
269
|
+
document.addEventListener(
|
|
270
|
+
"change",
|
|
271
|
+
function (e) {
|
|
272
|
+
var t = e.target;
|
|
273
|
+
if (shouldIgnoreTarget(t)) return;
|
|
274
|
+
logUiEvent("change", {
|
|
275
|
+
target: describeElement(t),
|
|
276
|
+
value: getInputValueSafe(t),
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
true
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
document.addEventListener(
|
|
283
|
+
"focusin",
|
|
284
|
+
function (e) {
|
|
285
|
+
var t = e.target;
|
|
286
|
+
if (shouldIgnoreTarget(t)) return;
|
|
287
|
+
logUiEvent("focusin", { target: describeElement(t) });
|
|
288
|
+
},
|
|
289
|
+
true
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
document.addEventListener(
|
|
293
|
+
"focusout",
|
|
294
|
+
function (e) {
|
|
295
|
+
var t = e.target;
|
|
296
|
+
if (shouldIgnoreTarget(t)) return;
|
|
297
|
+
logUiEvent("focusout", {
|
|
298
|
+
target: describeElement(t),
|
|
299
|
+
value: getInputValueSafe(t),
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
true
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Enter/Escape are useful for form flows & modals
|
|
306
|
+
document.addEventListener(
|
|
307
|
+
"keydown",
|
|
308
|
+
function (e) {
|
|
309
|
+
if (e.key !== "Enter" && e.key !== "Escape") return;
|
|
310
|
+
var t = e.target;
|
|
311
|
+
if (shouldIgnoreTarget(t)) return;
|
|
312
|
+
logUiEvent("keydown", { key: e.key, target: describeElement(t) });
|
|
313
|
+
},
|
|
314
|
+
true
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Form submissions
|
|
318
|
+
document.addEventListener(
|
|
319
|
+
"submit",
|
|
320
|
+
function (e) {
|
|
321
|
+
var t = e.target;
|
|
322
|
+
if (shouldIgnoreTarget(t)) return;
|
|
323
|
+
logUiEvent("submit", { target: describeElement(t) });
|
|
324
|
+
},
|
|
325
|
+
true
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Throttled scroll events
|
|
329
|
+
window.addEventListener(
|
|
330
|
+
"scroll",
|
|
331
|
+
function () {
|
|
332
|
+
var now = Date.now();
|
|
333
|
+
if (now - store.lastScrollTime < CONFIG.scrollThrottleMs) return;
|
|
334
|
+
store.lastScrollTime = now;
|
|
335
|
+
|
|
336
|
+
logUiEvent("scroll", {
|
|
337
|
+
scrollX: window.scrollX,
|
|
338
|
+
scrollY: window.scrollY,
|
|
339
|
+
documentHeight: document.documentElement.scrollHeight,
|
|
340
|
+
viewportHeight: window.innerHeight,
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
{ passive: true }
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Navigation tracking for SPAs
|
|
347
|
+
function nav(reason) {
|
|
348
|
+
logUiEvent("navigate", { reason: reason });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
var origPush = history.pushState;
|
|
352
|
+
history.pushState = function () {
|
|
353
|
+
origPush.apply(this, arguments);
|
|
354
|
+
nav("pushState");
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
var origReplace = history.replaceState;
|
|
358
|
+
history.replaceState = function () {
|
|
359
|
+
origReplace.apply(this, arguments);
|
|
360
|
+
nav("replaceState");
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
window.addEventListener("popstate", function () {
|
|
364
|
+
nav("popstate");
|
|
365
|
+
});
|
|
366
|
+
window.addEventListener("hashchange", function () {
|
|
367
|
+
nav("hashchange");
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ==========================================================================
|
|
372
|
+
// Console Interception
|
|
373
|
+
// ==========================================================================
|
|
374
|
+
|
|
375
|
+
var originalConsole = {
|
|
376
|
+
log: console.log.bind(console),
|
|
377
|
+
debug: console.debug.bind(console),
|
|
378
|
+
info: console.info.bind(console),
|
|
379
|
+
warn: console.warn.bind(console),
|
|
380
|
+
error: console.error.bind(console),
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
["log", "debug", "info", "warn", "error"].forEach(function (method) {
|
|
384
|
+
console[method] = function () {
|
|
385
|
+
var args = Array.prototype.slice.call(arguments);
|
|
386
|
+
|
|
387
|
+
var entry = {
|
|
388
|
+
timestamp: Date.now(),
|
|
389
|
+
level: method.toUpperCase(),
|
|
390
|
+
args: formatArgs(args),
|
|
391
|
+
stack: method === "error" ? new Error().stack : null,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
store.consoleLogs.push(entry);
|
|
395
|
+
pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
|
|
396
|
+
|
|
397
|
+
originalConsole[method].apply(console, args);
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
window.addEventListener("error", function (event) {
|
|
402
|
+
store.consoleLogs.push({
|
|
403
|
+
timestamp: Date.now(),
|
|
404
|
+
level: "ERROR",
|
|
405
|
+
args: [
|
|
406
|
+
{
|
|
407
|
+
type: "UncaughtError",
|
|
408
|
+
message: event.message,
|
|
409
|
+
filename: event.filename,
|
|
410
|
+
lineno: event.lineno,
|
|
411
|
+
colno: event.colno,
|
|
412
|
+
stack: event.error ? event.error.stack : null,
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
stack: event.error ? event.error.stack : null,
|
|
416
|
+
});
|
|
417
|
+
pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
|
|
418
|
+
|
|
419
|
+
// Mark an error moment in UI event stream for agents
|
|
420
|
+
logUiEvent("error", {
|
|
421
|
+
message: event.message,
|
|
422
|
+
filename: event.filename,
|
|
423
|
+
lineno: event.lineno,
|
|
424
|
+
colno: event.colno,
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
window.addEventListener("unhandledrejection", function (event) {
|
|
429
|
+
var reason = event.reason;
|
|
430
|
+
store.consoleLogs.push({
|
|
431
|
+
timestamp: Date.now(),
|
|
432
|
+
level: "ERROR",
|
|
433
|
+
args: [
|
|
434
|
+
{
|
|
435
|
+
type: "UnhandledRejection",
|
|
436
|
+
reason: reason && reason.message ? reason.message : String(reason),
|
|
437
|
+
stack: reason && reason.stack ? reason.stack : null,
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
stack: reason && reason.stack ? reason.stack : null,
|
|
441
|
+
});
|
|
442
|
+
pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
|
|
443
|
+
|
|
444
|
+
logUiEvent("unhandledrejection", {
|
|
445
|
+
reason: reason && reason.message ? reason.message : String(reason),
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// ==========================================================================
|
|
450
|
+
// Fetch Interception
|
|
451
|
+
// ==========================================================================
|
|
452
|
+
|
|
453
|
+
var originalFetch = window.fetch.bind(window);
|
|
454
|
+
|
|
455
|
+
window.fetch = function (input, init) {
|
|
456
|
+
init = init || {};
|
|
457
|
+
var startTime = Date.now();
|
|
458
|
+
// Handle string, Request object, or URL object
|
|
459
|
+
var url = typeof input === "string"
|
|
460
|
+
? input
|
|
461
|
+
: (input && (input.url || input.href || String(input))) || "";
|
|
462
|
+
var method = init.method || (input && input.method) || "GET";
|
|
463
|
+
|
|
464
|
+
// Don't intercept internal requests
|
|
465
|
+
if (url.indexOf("/__manus__/") === 0) {
|
|
466
|
+
return originalFetch(input, init);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Safely parse headers (avoid breaking if headers format is invalid)
|
|
470
|
+
var requestHeaders = {};
|
|
471
|
+
try {
|
|
472
|
+
if (init.headers) {
|
|
473
|
+
requestHeaders = Object.fromEntries(new Headers(init.headers).entries());
|
|
474
|
+
}
|
|
475
|
+
} catch (e) {
|
|
476
|
+
requestHeaders = { _parseError: true };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
var entry = {
|
|
480
|
+
timestamp: startTime,
|
|
481
|
+
type: "fetch",
|
|
482
|
+
method: method.toUpperCase(),
|
|
483
|
+
url: url,
|
|
484
|
+
request: {
|
|
485
|
+
headers: requestHeaders,
|
|
486
|
+
body: init.body ? sanitizeValue(tryParseJson(init.body)) : null,
|
|
487
|
+
},
|
|
488
|
+
response: null,
|
|
489
|
+
duration: null,
|
|
490
|
+
error: null,
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
return originalFetch(input, init)
|
|
494
|
+
.then(function (response) {
|
|
495
|
+
entry.duration = Date.now() - startTime;
|
|
496
|
+
|
|
497
|
+
var contentType = (response.headers.get("content-type") || "").toLowerCase();
|
|
498
|
+
var contentLength = response.headers.get("content-length");
|
|
499
|
+
|
|
500
|
+
entry.response = {
|
|
501
|
+
status: response.status,
|
|
502
|
+
statusText: response.statusText,
|
|
503
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
504
|
+
body: null,
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Semantic network hint for agents on failures (sync, no need to wait for body)
|
|
508
|
+
if (response.status >= 400) {
|
|
509
|
+
logUiEvent("network_error", {
|
|
510
|
+
kind: "fetch",
|
|
511
|
+
method: entry.method,
|
|
512
|
+
url: entry.url,
|
|
513
|
+
status: response.status,
|
|
514
|
+
statusText: response.statusText,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Skip body capture for streaming responses (SSE, etc.) to avoid memory leaks
|
|
519
|
+
var isStreaming = contentType.indexOf("text/event-stream") !== -1 ||
|
|
520
|
+
contentType.indexOf("application/stream") !== -1 ||
|
|
521
|
+
contentType.indexOf("application/x-ndjson") !== -1;
|
|
522
|
+
if (isStreaming) {
|
|
523
|
+
entry.response.body = "[Streaming response - not captured]";
|
|
524
|
+
store.networkRequests.push(entry);
|
|
525
|
+
pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
|
|
526
|
+
return response;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Skip body capture for large responses to avoid memory issues
|
|
530
|
+
if (contentLength && parseInt(contentLength, 10) > CONFIG.maxBodyLength) {
|
|
531
|
+
entry.response.body = "[Response too large: " + contentLength + " bytes]";
|
|
532
|
+
store.networkRequests.push(entry);
|
|
533
|
+
pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
|
|
534
|
+
return response;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Skip body capture for binary content types
|
|
538
|
+
var isBinary = contentType.indexOf("image/") !== -1 ||
|
|
539
|
+
contentType.indexOf("video/") !== -1 ||
|
|
540
|
+
contentType.indexOf("audio/") !== -1 ||
|
|
541
|
+
contentType.indexOf("application/octet-stream") !== -1 ||
|
|
542
|
+
contentType.indexOf("application/pdf") !== -1 ||
|
|
543
|
+
contentType.indexOf("application/zip") !== -1;
|
|
544
|
+
if (isBinary) {
|
|
545
|
+
entry.response.body = "[Binary content: " + contentType + "]";
|
|
546
|
+
store.networkRequests.push(entry);
|
|
547
|
+
pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
|
|
548
|
+
return response;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// For text responses, clone and read body in background
|
|
552
|
+
var clonedResponse = response.clone();
|
|
553
|
+
|
|
554
|
+
// Async: read body in background, don't block the response
|
|
555
|
+
clonedResponse
|
|
556
|
+
.text()
|
|
557
|
+
.then(function (text) {
|
|
558
|
+
if (text.length <= CONFIG.maxBodyLength) {
|
|
559
|
+
entry.response.body = sanitizeValue(tryParseJson(text));
|
|
560
|
+
} else {
|
|
561
|
+
entry.response.body = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
.catch(function () {
|
|
565
|
+
entry.response.body = "[Unable to read body]";
|
|
566
|
+
})
|
|
567
|
+
.finally(function () {
|
|
568
|
+
store.networkRequests.push(entry);
|
|
569
|
+
pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Return response immediately, don't wait for body reading
|
|
573
|
+
return response;
|
|
574
|
+
})
|
|
575
|
+
.catch(function (error) {
|
|
576
|
+
entry.duration = Date.now() - startTime;
|
|
577
|
+
entry.error = { message: error.message, stack: error.stack };
|
|
578
|
+
|
|
579
|
+
store.networkRequests.push(entry);
|
|
580
|
+
pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
|
|
581
|
+
|
|
582
|
+
logUiEvent("network_error", {
|
|
583
|
+
kind: "fetch",
|
|
584
|
+
method: entry.method,
|
|
585
|
+
url: entry.url,
|
|
586
|
+
message: error.message,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
throw error;
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// ==========================================================================
|
|
594
|
+
// XHR Interception
|
|
595
|
+
// ==========================================================================
|
|
596
|
+
|
|
597
|
+
var originalXHROpen = XMLHttpRequest.prototype.open;
|
|
598
|
+
var originalXHRSend = XMLHttpRequest.prototype.send;
|
|
599
|
+
|
|
600
|
+
XMLHttpRequest.prototype.open = function (method, url) {
|
|
601
|
+
this._manusData = {
|
|
602
|
+
method: (method || "GET").toUpperCase(),
|
|
603
|
+
url: url,
|
|
604
|
+
startTime: null,
|
|
605
|
+
};
|
|
606
|
+
return originalXHROpen.apply(this, arguments);
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
610
|
+
var xhr = this;
|
|
611
|
+
|
|
612
|
+
if (
|
|
613
|
+
xhr._manusData &&
|
|
614
|
+
xhr._manusData.url &&
|
|
615
|
+
xhr._manusData.url.indexOf("/__manus__/") !== 0
|
|
616
|
+
) {
|
|
617
|
+
xhr._manusData.startTime = Date.now();
|
|
618
|
+
xhr._manusData.requestBody = body ? sanitizeValue(tryParseJson(body)) : null;
|
|
619
|
+
|
|
620
|
+
xhr.addEventListener("load", function () {
|
|
621
|
+
var contentType = (xhr.getResponseHeader("content-type") || "").toLowerCase();
|
|
622
|
+
var responseBody = null;
|
|
623
|
+
|
|
624
|
+
// Skip body capture for streaming responses
|
|
625
|
+
var isStreaming = contentType.indexOf("text/event-stream") !== -1 ||
|
|
626
|
+
contentType.indexOf("application/stream") !== -1 ||
|
|
627
|
+
contentType.indexOf("application/x-ndjson") !== -1;
|
|
628
|
+
|
|
629
|
+
// Skip body capture for binary content types
|
|
630
|
+
var isBinary = contentType.indexOf("image/") !== -1 ||
|
|
631
|
+
contentType.indexOf("video/") !== -1 ||
|
|
632
|
+
contentType.indexOf("audio/") !== -1 ||
|
|
633
|
+
contentType.indexOf("application/octet-stream") !== -1 ||
|
|
634
|
+
contentType.indexOf("application/pdf") !== -1 ||
|
|
635
|
+
contentType.indexOf("application/zip") !== -1;
|
|
636
|
+
|
|
637
|
+
if (isStreaming) {
|
|
638
|
+
responseBody = "[Streaming response - not captured]";
|
|
639
|
+
} else if (isBinary) {
|
|
640
|
+
responseBody = "[Binary content: " + contentType + "]";
|
|
641
|
+
} else {
|
|
642
|
+
// Safe to read responseText for text responses
|
|
643
|
+
try {
|
|
644
|
+
var text = xhr.responseText || "";
|
|
645
|
+
if (text.length > CONFIG.maxBodyLength) {
|
|
646
|
+
responseBody = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
|
|
647
|
+
} else {
|
|
648
|
+
responseBody = sanitizeValue(tryParseJson(text));
|
|
649
|
+
}
|
|
650
|
+
} catch (e) {
|
|
651
|
+
// responseText may throw for non-text responses
|
|
652
|
+
responseBody = "[Unable to read response: " + e.message + "]";
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
var entry = {
|
|
657
|
+
timestamp: xhr._manusData.startTime,
|
|
658
|
+
type: "xhr",
|
|
659
|
+
method: xhr._manusData.method,
|
|
660
|
+
url: xhr._manusData.url,
|
|
661
|
+
request: { body: xhr._manusData.requestBody },
|
|
662
|
+
response: {
|
|
663
|
+
status: xhr.status,
|
|
664
|
+
statusText: xhr.statusText,
|
|
665
|
+
body: responseBody,
|
|
666
|
+
},
|
|
667
|
+
duration: Date.now() - xhr._manusData.startTime,
|
|
668
|
+
error: null,
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
store.networkRequests.push(entry);
|
|
672
|
+
pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
|
|
673
|
+
|
|
674
|
+
if (entry.response && entry.response.status >= 400) {
|
|
675
|
+
logUiEvent("network_error", {
|
|
676
|
+
kind: "xhr",
|
|
677
|
+
method: entry.method,
|
|
678
|
+
url: entry.url,
|
|
679
|
+
status: entry.response.status,
|
|
680
|
+
statusText: entry.response.statusText,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
xhr.addEventListener("error", function () {
|
|
686
|
+
var entry = {
|
|
687
|
+
timestamp: xhr._manusData.startTime,
|
|
688
|
+
type: "xhr",
|
|
689
|
+
method: xhr._manusData.method,
|
|
690
|
+
url: xhr._manusData.url,
|
|
691
|
+
request: { body: xhr._manusData.requestBody },
|
|
692
|
+
response: null,
|
|
693
|
+
duration: Date.now() - xhr._manusData.startTime,
|
|
694
|
+
error: { message: "Network error" },
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
store.networkRequests.push(entry);
|
|
698
|
+
pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
|
|
699
|
+
|
|
700
|
+
logUiEvent("network_error", {
|
|
701
|
+
kind: "xhr",
|
|
702
|
+
method: entry.method,
|
|
703
|
+
url: entry.url,
|
|
704
|
+
message: "Network error",
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return originalXHRSend.apply(this, arguments);
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// ==========================================================================
|
|
713
|
+
// Data Reporting
|
|
714
|
+
// ==========================================================================
|
|
715
|
+
|
|
716
|
+
function reportLogs() {
|
|
717
|
+
var consoleLogs = store.consoleLogs.splice(0);
|
|
718
|
+
var networkRequests = store.networkRequests.splice(0);
|
|
719
|
+
var uiEvents = store.uiEvents.splice(0);
|
|
720
|
+
|
|
721
|
+
// Skip if no new data
|
|
722
|
+
if (
|
|
723
|
+
consoleLogs.length === 0 &&
|
|
724
|
+
networkRequests.length === 0 &&
|
|
725
|
+
uiEvents.length === 0
|
|
726
|
+
) {
|
|
727
|
+
return Promise.resolve();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
var payload = {
|
|
731
|
+
timestamp: Date.now(),
|
|
732
|
+
consoleLogs: consoleLogs,
|
|
733
|
+
networkRequests: networkRequests,
|
|
734
|
+
// Mirror uiEvents to sessionEvents for sessionReplay.log
|
|
735
|
+
sessionEvents: uiEvents,
|
|
736
|
+
// agent-friendly semantic events
|
|
737
|
+
uiEvents: uiEvents,
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
return originalFetch(CONFIG.reportEndpoint, {
|
|
741
|
+
method: "POST",
|
|
742
|
+
headers: { "Content-Type": "application/json" },
|
|
743
|
+
body: JSON.stringify(payload),
|
|
744
|
+
}).catch(function () {
|
|
745
|
+
// Put data back on failure (but respect limits)
|
|
746
|
+
store.consoleLogs = consoleLogs.concat(store.consoleLogs);
|
|
747
|
+
store.networkRequests = networkRequests.concat(store.networkRequests);
|
|
748
|
+
store.uiEvents = uiEvents.concat(store.uiEvents);
|
|
749
|
+
|
|
750
|
+
pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
|
|
751
|
+
pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
|
|
752
|
+
pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Periodic reporting
|
|
757
|
+
setInterval(reportLogs, CONFIG.reportInterval);
|
|
758
|
+
|
|
759
|
+
// Report on page unload
|
|
760
|
+
window.addEventListener("beforeunload", function () {
|
|
761
|
+
var consoleLogs = store.consoleLogs;
|
|
762
|
+
var networkRequests = store.networkRequests;
|
|
763
|
+
var uiEvents = store.uiEvents;
|
|
764
|
+
|
|
765
|
+
if (
|
|
766
|
+
consoleLogs.length === 0 &&
|
|
767
|
+
networkRequests.length === 0 &&
|
|
768
|
+
uiEvents.length === 0
|
|
769
|
+
) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
var payload = {
|
|
774
|
+
timestamp: Date.now(),
|
|
775
|
+
consoleLogs: consoleLogs,
|
|
776
|
+
networkRequests: networkRequests,
|
|
777
|
+
// Mirror uiEvents to sessionEvents for sessionReplay.log
|
|
778
|
+
sessionEvents: uiEvents,
|
|
779
|
+
uiEvents: uiEvents,
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
if (navigator.sendBeacon) {
|
|
783
|
+
var payloadStr = JSON.stringify(payload);
|
|
784
|
+
// sendBeacon has ~64KB limit, truncate if too large
|
|
785
|
+
var MAX_BEACON_SIZE = 60000; // Leave some margin
|
|
786
|
+
if (payloadStr.length > MAX_BEACON_SIZE) {
|
|
787
|
+
// Prioritize: keep recent events, drop older logs
|
|
788
|
+
var truncatedPayload = {
|
|
789
|
+
timestamp: Date.now(),
|
|
790
|
+
consoleLogs: consoleLogs.slice(-50),
|
|
791
|
+
networkRequests: networkRequests.slice(-20),
|
|
792
|
+
sessionEvents: uiEvents.slice(-100),
|
|
793
|
+
uiEvents: uiEvents.slice(-100),
|
|
794
|
+
_truncated: true,
|
|
795
|
+
};
|
|
796
|
+
payloadStr = JSON.stringify(truncatedPayload);
|
|
797
|
+
}
|
|
798
|
+
navigator.sendBeacon(CONFIG.reportEndpoint, payloadStr);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// ==========================================================================
|
|
803
|
+
// Initialization
|
|
804
|
+
// ==========================================================================
|
|
805
|
+
|
|
806
|
+
// Install semantic UI listeners ASAP
|
|
807
|
+
try {
|
|
808
|
+
installUiEventListeners();
|
|
809
|
+
} catch (e) {
|
|
810
|
+
console.warn("[Manus] Failed to install UI listeners:", e);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Mark as initialized
|
|
814
|
+
window.__MANUS_DEBUG_COLLECTOR__ = {
|
|
815
|
+
version: "2.0-no-rrweb",
|
|
816
|
+
store: store,
|
|
817
|
+
forceReport: reportLogs,
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
console.debug("[Manus] Debug collector initialized (no rrweb, UI events only)");
|
|
821
|
+
})();
|