822 行
25 KiB
JavaScript
822 行
25 KiB
JavaScript
/**
|
|
* 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)");
|
|
})();
|