debug-collector.js 25 KB

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