| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821 |
- /**
- * Manus Debug Collector (agent-friendly)
- *
- * Captures:
- * 1) Console logs
- * 2) Network requests (fetch + XHR)
- * 3) User interactions (semantic uiEvents: click/type/submit/nav/scroll/etc.)
- *
- * Data is periodically sent to /__manus__/logs
- * Note: uiEvents are mirrored to sessionEvents for sessionReplay.log
- */
- (function () {
- "use strict";
- // Prevent double initialization
- if (window.__MANUS_DEBUG_COLLECTOR__) return;
- // ==========================================================================
- // Configuration
- // ==========================================================================
- const CONFIG = {
- reportEndpoint: "/__manus__/logs",
- bufferSize: {
- console: 500,
- network: 200,
- // semantic, agent-friendly UI events
- ui: 500,
- },
- reportInterval: 2000,
- sensitiveFields: [
- "password",
- "token",
- "secret",
- "key",
- "authorization",
- "cookie",
- "session",
- ],
- maxBodyLength: 10240,
- // UI event logging privacy policy:
- // - inputs matching sensitiveFields or type=password are masked by default
- // - non-sensitive inputs log up to 200 chars
- uiInputMaxLen: 200,
- uiTextMaxLen: 80,
- // Scroll throttling: minimum ms between scroll events
- scrollThrottleMs: 500,
- };
- // ==========================================================================
- // Storage
- // ==========================================================================
- const store = {
- consoleLogs: [],
- networkRequests: [],
- uiEvents: [],
- lastReportTime: Date.now(),
- lastScrollTime: 0,
- };
- // ==========================================================================
- // Utility Functions
- // ==========================================================================
- function sanitizeValue(value, depth) {
- if (depth === void 0) depth = 0;
- if (depth > 5) return "[Max Depth]";
- if (value === null) return null;
- if (value === undefined) return undefined;
- if (typeof value === "string") {
- return value.length > 1000 ? value.slice(0, 1000) + "...[truncated]" : value;
- }
- if (typeof value !== "object") return value;
- if (Array.isArray(value)) {
- return value.slice(0, 100).map(function (v) {
- return sanitizeValue(v, depth + 1);
- });
- }
- var sanitized = {};
- for (var k in value) {
- if (Object.prototype.hasOwnProperty.call(value, k)) {
- var isSensitive = CONFIG.sensitiveFields.some(function (f) {
- return k.toLowerCase().indexOf(f) !== -1;
- });
- if (isSensitive) {
- sanitized[k] = "[REDACTED]";
- } else {
- sanitized[k] = sanitizeValue(value[k], depth + 1);
- }
- }
- }
- return sanitized;
- }
- function formatArg(arg) {
- try {
- if (arg instanceof Error) {
- return { type: "Error", message: arg.message, stack: arg.stack };
- }
- if (typeof arg === "object") return sanitizeValue(arg);
- return String(arg);
- } catch (e) {
- return "[Unserializable]";
- }
- }
- function formatArgs(args) {
- var result = [];
- for (var i = 0; i < args.length; i++) result.push(formatArg(args[i]));
- return result;
- }
- function pruneBuffer(buffer, maxSize) {
- if (buffer.length > maxSize) buffer.splice(0, buffer.length - maxSize);
- }
- function tryParseJson(str) {
- if (typeof str !== "string") return str;
- try {
- return JSON.parse(str);
- } catch (e) {
- return str;
- }
- }
- // ==========================================================================
- // Semantic UI Event Logging (agent-friendly)
- // ==========================================================================
- function shouldIgnoreTarget(target) {
- try {
- if (!target || !(target instanceof Element)) return false;
- return !!target.closest(".manus-no-record");
- } catch (e) {
- return false;
- }
- }
- function compactText(s, maxLen) {
- try {
- var t = (s || "").trim().replace(/\s+/g, " ");
- if (!t) return "";
- return t.length > maxLen ? t.slice(0, maxLen) + "…" : t;
- } catch (e) {
- return "";
- }
- }
- function elText(el) {
- try {
- var t = el.innerText || el.textContent || "";
- return compactText(t, CONFIG.uiTextMaxLen);
- } catch (e) {
- return "";
- }
- }
- function describeElement(el) {
- if (!el || !(el instanceof Element)) return null;
- var getAttr = function (name) {
- return el.getAttribute(name);
- };
- var tag = el.tagName ? el.tagName.toLowerCase() : null;
- var id = el.id || null;
- var name = getAttr("name") || null;
- var role = getAttr("role") || null;
- var ariaLabel = getAttr("aria-label") || null;
- var dataLoc = getAttr("data-loc") || null;
- var testId =
- getAttr("data-testid") ||
- getAttr("data-test-id") ||
- getAttr("data-test") ||
- null;
- var type = tag === "input" ? (getAttr("type") || "text") : null;
- var href = tag === "a" ? getAttr("href") || null : null;
- // a small, stable hint for agents (avoid building full CSS paths)
- var selectorHint = null;
- if (testId) selectorHint = '[data-testid="' + testId + '"]';
- else if (dataLoc) selectorHint = '[data-loc="' + dataLoc + '"]';
- else if (id) selectorHint = "#" + id;
- else selectorHint = tag || "unknown";
- return {
- tag: tag,
- id: id,
- name: name,
- type: type,
- role: role,
- ariaLabel: ariaLabel,
- testId: testId,
- dataLoc: dataLoc,
- href: href,
- text: elText(el),
- selectorHint: selectorHint,
- };
- }
- function isSensitiveField(el) {
- if (!el || !(el instanceof Element)) return false;
- var tag = el.tagName ? el.tagName.toLowerCase() : "";
- if (tag !== "input" && tag !== "textarea") return false;
- var type = (el.getAttribute("type") || "").toLowerCase();
- if (type === "password") return true;
- var name = (el.getAttribute("name") || "").toLowerCase();
- var id = (el.id || "").toLowerCase();
- return CONFIG.sensitiveFields.some(function (f) {
- return name.indexOf(f) !== -1 || id.indexOf(f) !== -1;
- });
- }
- function getInputValueSafe(el) {
- if (!el || !(el instanceof Element)) return null;
- var tag = el.tagName ? el.tagName.toLowerCase() : "";
- if (tag !== "input" && tag !== "textarea" && tag !== "select") return null;
- var v = "";
- try {
- v = el.value != null ? String(el.value) : "";
- } catch (e) {
- v = "";
- }
- if (isSensitiveField(el)) return { masked: true, length: v.length };
- if (v.length > CONFIG.uiInputMaxLen) v = v.slice(0, CONFIG.uiInputMaxLen) + "…";
- return v;
- }
- function logUiEvent(kind, payload) {
- var entry = {
- timestamp: Date.now(),
- kind: kind,
- url: location.href,
- viewport: { width: window.innerWidth, height: window.innerHeight },
- payload: sanitizeValue(payload),
- };
- store.uiEvents.push(entry);
- pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui);
- }
- function installUiEventListeners() {
- // Clicks
- document.addEventListener(
- "click",
- function (e) {
- var t = e.target;
- if (shouldIgnoreTarget(t)) return;
- logUiEvent("click", {
- target: describeElement(t),
- x: e.clientX,
- y: e.clientY,
- });
- },
- true
- );
- // Typing "commit" events
- document.addEventListener(
- "change",
- function (e) {
- var t = e.target;
- if (shouldIgnoreTarget(t)) return;
- logUiEvent("change", {
- target: describeElement(t),
- value: getInputValueSafe(t),
- });
- },
- true
- );
- document.addEventListener(
- "focusin",
- function (e) {
- var t = e.target;
- if (shouldIgnoreTarget(t)) return;
- logUiEvent("focusin", { target: describeElement(t) });
- },
- true
- );
- document.addEventListener(
- "focusout",
- function (e) {
- var t = e.target;
- if (shouldIgnoreTarget(t)) return;
- logUiEvent("focusout", {
- target: describeElement(t),
- value: getInputValueSafe(t),
- });
- },
- true
- );
- // Enter/Escape are useful for form flows & modals
- document.addEventListener(
- "keydown",
- function (e) {
- if (e.key !== "Enter" && e.key !== "Escape") return;
- var t = e.target;
- if (shouldIgnoreTarget(t)) return;
- logUiEvent("keydown", { key: e.key, target: describeElement(t) });
- },
- true
- );
- // Form submissions
- document.addEventListener(
- "submit",
- function (e) {
- var t = e.target;
- if (shouldIgnoreTarget(t)) return;
- logUiEvent("submit", { target: describeElement(t) });
- },
- true
- );
- // Throttled scroll events
- window.addEventListener(
- "scroll",
- function () {
- var now = Date.now();
- if (now - store.lastScrollTime < CONFIG.scrollThrottleMs) return;
- store.lastScrollTime = now;
- logUiEvent("scroll", {
- scrollX: window.scrollX,
- scrollY: window.scrollY,
- documentHeight: document.documentElement.scrollHeight,
- viewportHeight: window.innerHeight,
- });
- },
- { passive: true }
- );
- // Navigation tracking for SPAs
- function nav(reason) {
- logUiEvent("navigate", { reason: reason });
- }
- var origPush = history.pushState;
- history.pushState = function () {
- origPush.apply(this, arguments);
- nav("pushState");
- };
- var origReplace = history.replaceState;
- history.replaceState = function () {
- origReplace.apply(this, arguments);
- nav("replaceState");
- };
- window.addEventListener("popstate", function () {
- nav("popstate");
- });
- window.addEventListener("hashchange", function () {
- nav("hashchange");
- });
- }
- // ==========================================================================
- // Console Interception
- // ==========================================================================
- var originalConsole = {
- log: console.log.bind(console),
- debug: console.debug.bind(console),
- info: console.info.bind(console),
- warn: console.warn.bind(console),
- error: console.error.bind(console),
- };
- ["log", "debug", "info", "warn", "error"].forEach(function (method) {
- console[method] = function () {
- var args = Array.prototype.slice.call(arguments);
- var entry = {
- timestamp: Date.now(),
- level: method.toUpperCase(),
- args: formatArgs(args),
- stack: method === "error" ? new Error().stack : null,
- };
- store.consoleLogs.push(entry);
- pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
- originalConsole[method].apply(console, args);
- };
- });
- window.addEventListener("error", function (event) {
- store.consoleLogs.push({
- timestamp: Date.now(),
- level: "ERROR",
- args: [
- {
- type: "UncaughtError",
- message: event.message,
- filename: event.filename,
- lineno: event.lineno,
- colno: event.colno,
- stack: event.error ? event.error.stack : null,
- },
- ],
- stack: event.error ? event.error.stack : null,
- });
- pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
- // Mark an error moment in UI event stream for agents
- logUiEvent("error", {
- message: event.message,
- filename: event.filename,
- lineno: event.lineno,
- colno: event.colno,
- });
- });
- window.addEventListener("unhandledrejection", function (event) {
- var reason = event.reason;
- store.consoleLogs.push({
- timestamp: Date.now(),
- level: "ERROR",
- args: [
- {
- type: "UnhandledRejection",
- reason: reason && reason.message ? reason.message : String(reason),
- stack: reason && reason.stack ? reason.stack : null,
- },
- ],
- stack: reason && reason.stack ? reason.stack : null,
- });
- pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
- logUiEvent("unhandledrejection", {
- reason: reason && reason.message ? reason.message : String(reason),
- });
- });
- // ==========================================================================
- // Fetch Interception
- // ==========================================================================
- var originalFetch = window.fetch.bind(window);
- window.fetch = function (input, init) {
- init = init || {};
- var startTime = Date.now();
- // Handle string, Request object, or URL object
- var url = typeof input === "string"
- ? input
- : (input && (input.url || input.href || String(input))) || "";
- var method = init.method || (input && input.method) || "GET";
- // Don't intercept internal requests
- if (url.indexOf("/__manus__/") === 0) {
- return originalFetch(input, init);
- }
- // Safely parse headers (avoid breaking if headers format is invalid)
- var requestHeaders = {};
- try {
- if (init.headers) {
- requestHeaders = Object.fromEntries(new Headers(init.headers).entries());
- }
- } catch (e) {
- requestHeaders = { _parseError: true };
- }
- var entry = {
- timestamp: startTime,
- type: "fetch",
- method: method.toUpperCase(),
- url: url,
- request: {
- headers: requestHeaders,
- body: init.body ? sanitizeValue(tryParseJson(init.body)) : null,
- },
- response: null,
- duration: null,
- error: null,
- };
- return originalFetch(input, init)
- .then(function (response) {
- entry.duration = Date.now() - startTime;
- var contentType = (response.headers.get("content-type") || "").toLowerCase();
- var contentLength = response.headers.get("content-length");
- entry.response = {
- status: response.status,
- statusText: response.statusText,
- headers: Object.fromEntries(response.headers.entries()),
- body: null,
- };
- // Semantic network hint for agents on failures (sync, no need to wait for body)
- if (response.status >= 400) {
- logUiEvent("network_error", {
- kind: "fetch",
- method: entry.method,
- url: entry.url,
- status: response.status,
- statusText: response.statusText,
- });
- }
- // Skip body capture for streaming responses (SSE, etc.) to avoid memory leaks
- var isStreaming = contentType.indexOf("text/event-stream") !== -1 ||
- contentType.indexOf("application/stream") !== -1 ||
- contentType.indexOf("application/x-ndjson") !== -1;
- if (isStreaming) {
- entry.response.body = "[Streaming response - not captured]";
- store.networkRequests.push(entry);
- pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
- return response;
- }
- // Skip body capture for large responses to avoid memory issues
- if (contentLength && parseInt(contentLength, 10) > CONFIG.maxBodyLength) {
- entry.response.body = "[Response too large: " + contentLength + " bytes]";
- store.networkRequests.push(entry);
- pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
- return response;
- }
- // Skip body capture for binary content types
- var isBinary = contentType.indexOf("image/") !== -1 ||
- contentType.indexOf("video/") !== -1 ||
- contentType.indexOf("audio/") !== -1 ||
- contentType.indexOf("application/octet-stream") !== -1 ||
- contentType.indexOf("application/pdf") !== -1 ||
- contentType.indexOf("application/zip") !== -1;
- if (isBinary) {
- entry.response.body = "[Binary content: " + contentType + "]";
- store.networkRequests.push(entry);
- pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
- return response;
- }
- // For text responses, clone and read body in background
- var clonedResponse = response.clone();
- // Async: read body in background, don't block the response
- clonedResponse
- .text()
- .then(function (text) {
- if (text.length <= CONFIG.maxBodyLength) {
- entry.response.body = sanitizeValue(tryParseJson(text));
- } else {
- entry.response.body = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
- }
- })
- .catch(function () {
- entry.response.body = "[Unable to read body]";
- })
- .finally(function () {
- store.networkRequests.push(entry);
- pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
- });
- // Return response immediately, don't wait for body reading
- return response;
- })
- .catch(function (error) {
- entry.duration = Date.now() - startTime;
- entry.error = { message: error.message, stack: error.stack };
- store.networkRequests.push(entry);
- pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
- logUiEvent("network_error", {
- kind: "fetch",
- method: entry.method,
- url: entry.url,
- message: error.message,
- });
- throw error;
- });
- };
- // ==========================================================================
- // XHR Interception
- // ==========================================================================
- var originalXHROpen = XMLHttpRequest.prototype.open;
- var originalXHRSend = XMLHttpRequest.prototype.send;
- XMLHttpRequest.prototype.open = function (method, url) {
- this._manusData = {
- method: (method || "GET").toUpperCase(),
- url: url,
- startTime: null,
- };
- return originalXHROpen.apply(this, arguments);
- };
- XMLHttpRequest.prototype.send = function (body) {
- var xhr = this;
- if (
- xhr._manusData &&
- xhr._manusData.url &&
- xhr._manusData.url.indexOf("/__manus__/") !== 0
- ) {
- xhr._manusData.startTime = Date.now();
- xhr._manusData.requestBody = body ? sanitizeValue(tryParseJson(body)) : null;
- xhr.addEventListener("load", function () {
- var contentType = (xhr.getResponseHeader("content-type") || "").toLowerCase();
- var responseBody = null;
- // Skip body capture for streaming responses
- var isStreaming = contentType.indexOf("text/event-stream") !== -1 ||
- contentType.indexOf("application/stream") !== -1 ||
- contentType.indexOf("application/x-ndjson") !== -1;
- // Skip body capture for binary content types
- var isBinary = contentType.indexOf("image/") !== -1 ||
- contentType.indexOf("video/") !== -1 ||
- contentType.indexOf("audio/") !== -1 ||
- contentType.indexOf("application/octet-stream") !== -1 ||
- contentType.indexOf("application/pdf") !== -1 ||
- contentType.indexOf("application/zip") !== -1;
- if (isStreaming) {
- responseBody = "[Streaming response - not captured]";
- } else if (isBinary) {
- responseBody = "[Binary content: " + contentType + "]";
- } else {
- // Safe to read responseText for text responses
- try {
- var text = xhr.responseText || "";
- if (text.length > CONFIG.maxBodyLength) {
- responseBody = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
- } else {
- responseBody = sanitizeValue(tryParseJson(text));
- }
- } catch (e) {
- // responseText may throw for non-text responses
- responseBody = "[Unable to read response: " + e.message + "]";
- }
- }
- var entry = {
- timestamp: xhr._manusData.startTime,
- type: "xhr",
- method: xhr._manusData.method,
- url: xhr._manusData.url,
- request: { body: xhr._manusData.requestBody },
- response: {
- status: xhr.status,
- statusText: xhr.statusText,
- body: responseBody,
- },
- duration: Date.now() - xhr._manusData.startTime,
- error: null,
- };
- store.networkRequests.push(entry);
- pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
- if (entry.response && entry.response.status >= 400) {
- logUiEvent("network_error", {
- kind: "xhr",
- method: entry.method,
- url: entry.url,
- status: entry.response.status,
- statusText: entry.response.statusText,
- });
- }
- });
- xhr.addEventListener("error", function () {
- var entry = {
- timestamp: xhr._manusData.startTime,
- type: "xhr",
- method: xhr._manusData.method,
- url: xhr._manusData.url,
- request: { body: xhr._manusData.requestBody },
- response: null,
- duration: Date.now() - xhr._manusData.startTime,
- error: { message: "Network error" },
- };
- store.networkRequests.push(entry);
- pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
- logUiEvent("network_error", {
- kind: "xhr",
- method: entry.method,
- url: entry.url,
- message: "Network error",
- });
- });
- }
- return originalXHRSend.apply(this, arguments);
- };
- // ==========================================================================
- // Data Reporting
- // ==========================================================================
- function reportLogs() {
- var consoleLogs = store.consoleLogs.splice(0);
- var networkRequests = store.networkRequests.splice(0);
- var uiEvents = store.uiEvents.splice(0);
- // Skip if no new data
- if (
- consoleLogs.length === 0 &&
- networkRequests.length === 0 &&
- uiEvents.length === 0
- ) {
- return Promise.resolve();
- }
- var payload = {
- timestamp: Date.now(),
- consoleLogs: consoleLogs,
- networkRequests: networkRequests,
- // Mirror uiEvents to sessionEvents for sessionReplay.log
- sessionEvents: uiEvents,
- // agent-friendly semantic events
- uiEvents: uiEvents,
- };
- return originalFetch(CONFIG.reportEndpoint, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(payload),
- }).catch(function () {
- // Put data back on failure (but respect limits)
- store.consoleLogs = consoleLogs.concat(store.consoleLogs);
- store.networkRequests = networkRequests.concat(store.networkRequests);
- store.uiEvents = uiEvents.concat(store.uiEvents);
- pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console);
- pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
- pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui);
- });
- }
- // Periodic reporting
- setInterval(reportLogs, CONFIG.reportInterval);
- // Report on page unload
- window.addEventListener("beforeunload", function () {
- var consoleLogs = store.consoleLogs;
- var networkRequests = store.networkRequests;
- var uiEvents = store.uiEvents;
- if (
- consoleLogs.length === 0 &&
- networkRequests.length === 0 &&
- uiEvents.length === 0
- ) {
- return;
- }
- var payload = {
- timestamp: Date.now(),
- consoleLogs: consoleLogs,
- networkRequests: networkRequests,
- // Mirror uiEvents to sessionEvents for sessionReplay.log
- sessionEvents: uiEvents,
- uiEvents: uiEvents,
- };
- if (navigator.sendBeacon) {
- var payloadStr = JSON.stringify(payload);
- // sendBeacon has ~64KB limit, truncate if too large
- var MAX_BEACON_SIZE = 60000; // Leave some margin
- if (payloadStr.length > MAX_BEACON_SIZE) {
- // Prioritize: keep recent events, drop older logs
- var truncatedPayload = {
- timestamp: Date.now(),
- consoleLogs: consoleLogs.slice(-50),
- networkRequests: networkRequests.slice(-20),
- sessionEvents: uiEvents.slice(-100),
- uiEvents: uiEvents.slice(-100),
- _truncated: true,
- };
- payloadStr = JSON.stringify(truncatedPayload);
- }
- navigator.sendBeacon(CONFIG.reportEndpoint, payloadStr);
- }
- });
- // ==========================================================================
- // Initialization
- // ==========================================================================
- // Install semantic UI listeners ASAP
- try {
- installUiEventListeners();
- } catch (e) {
- console.warn("[Manus] Failed to install UI listeners:", e);
- }
- // Mark as initialized
- window.__MANUS_DEBUG_COLLECTOR__ = {
- version: "2.0-no-rrweb",
- store: store,
- forceReport: reportLogs,
- };
- console.debug("[Manus] Debug collector initialized (no rrweb, UI events only)");
- })();
|