mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 03:02:03 +00:00
248 lines
7.1 KiB
TypeScript
248 lines
7.1 KiB
TypeScript
import type { FrontendErrorCollectorScriptOptions } from "./types.js";
|
|
|
|
const DEFAULT_REPORTER = "openhandoff-frontend";
|
|
|
|
export function createFrontendErrorCollectorScript(
|
|
options: FrontendErrorCollectorScriptOptions
|
|
): string {
|
|
const config = {
|
|
endpoint: options.endpoint,
|
|
reporter: options.reporter ?? DEFAULT_REPORTER,
|
|
includeConsoleErrors: options.includeConsoleErrors ?? true,
|
|
includeFetchErrors: options.includeFetchErrors ?? true,
|
|
};
|
|
|
|
return `(function () {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
if (window.__OPENHANDOFF_FRONTEND_ERROR_COLLECTOR__) {
|
|
return;
|
|
}
|
|
|
|
var config = ${JSON.stringify(config)};
|
|
var sharedContext = window.__OPENHANDOFF_FRONTEND_ERROR_CONTEXT__ || {};
|
|
window.__OPENHANDOFF_FRONTEND_ERROR_CONTEXT__ = sharedContext;
|
|
|
|
function now() {
|
|
return Date.now();
|
|
}
|
|
|
|
function clampText(input, maxLength) {
|
|
if (typeof input !== "string") {
|
|
return null;
|
|
}
|
|
var value = input.trim();
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
if (value.length <= maxLength) {
|
|
return value;
|
|
}
|
|
return value.slice(0, maxLength) + "...(truncated)";
|
|
}
|
|
|
|
function currentRoute() {
|
|
return location.pathname + location.search + location.hash;
|
|
}
|
|
|
|
function safeContext() {
|
|
var copy = {};
|
|
for (var key in sharedContext) {
|
|
if (!Object.prototype.hasOwnProperty.call(sharedContext, key)) {
|
|
continue;
|
|
}
|
|
var candidate = sharedContext[key];
|
|
if (
|
|
candidate === null ||
|
|
candidate === undefined ||
|
|
typeof candidate === "string" ||
|
|
typeof candidate === "number" ||
|
|
typeof candidate === "boolean"
|
|
) {
|
|
copy[key] = candidate;
|
|
}
|
|
}
|
|
copy.route = currentRoute();
|
|
return copy;
|
|
}
|
|
|
|
function stringifyUnknown(input) {
|
|
if (typeof input === "string") {
|
|
return input;
|
|
}
|
|
if (input instanceof Error) {
|
|
return input.stack || input.message || String(input);
|
|
}
|
|
try {
|
|
return JSON.stringify(input);
|
|
} catch {
|
|
return String(input);
|
|
}
|
|
}
|
|
|
|
var internalSendInFlight = false;
|
|
|
|
function send(eventPayload) {
|
|
var payload = {
|
|
kind: eventPayload.kind || "window-error",
|
|
message: clampText(eventPayload.message || "(no message)", 12000),
|
|
stack: clampText(eventPayload.stack, 12000),
|
|
source: clampText(eventPayload.source, 1024),
|
|
line: typeof eventPayload.line === "number" ? eventPayload.line : null,
|
|
column: typeof eventPayload.column === "number" ? eventPayload.column : null,
|
|
url: clampText(eventPayload.url || location.href, 2048),
|
|
timestamp: typeof eventPayload.timestamp === "number" ? eventPayload.timestamp : now(),
|
|
context: safeContext(),
|
|
extra: eventPayload.extra || {},
|
|
};
|
|
|
|
var body = JSON.stringify(payload);
|
|
|
|
if (navigator.sendBeacon && body.length < 60000) {
|
|
var blob = new Blob([body], { type: "application/json" });
|
|
navigator.sendBeacon(config.endpoint, blob);
|
|
return;
|
|
}
|
|
|
|
if (internalSendInFlight) {
|
|
return;
|
|
}
|
|
|
|
internalSendInFlight = true;
|
|
fetch(config.endpoint, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
credentials: "same-origin",
|
|
keepalive: true,
|
|
body: body,
|
|
}).catch(function () {
|
|
return;
|
|
}).finally(function () {
|
|
internalSendInFlight = false;
|
|
});
|
|
}
|
|
|
|
window.__OPENHANDOFF_FRONTEND_ERROR_COLLECTOR__ = {
|
|
setContext: function (nextContext) {
|
|
if (!nextContext || typeof nextContext !== "object") {
|
|
return;
|
|
}
|
|
for (var key in nextContext) {
|
|
if (!Object.prototype.hasOwnProperty.call(nextContext, key)) {
|
|
continue;
|
|
}
|
|
sharedContext[key] = nextContext[key];
|
|
}
|
|
},
|
|
};
|
|
|
|
if (config.includeConsoleErrors) {
|
|
var originalConsoleError = console.error.bind(console);
|
|
console.error = function () {
|
|
var message = "";
|
|
var values = [];
|
|
for (var index = 0; index < arguments.length; index += 1) {
|
|
values.push(stringifyUnknown(arguments[index]));
|
|
}
|
|
message = values.join(" ");
|
|
send({
|
|
kind: "console-error",
|
|
message: message || "console.error called",
|
|
timestamp: now(),
|
|
extra: { args: values.slice(0, 10) },
|
|
});
|
|
return originalConsoleError.apply(console, arguments);
|
|
};
|
|
}
|
|
|
|
window.addEventListener("error", function (event) {
|
|
var target = event.target;
|
|
var hasResourceTarget = target && target !== window && typeof target === "object";
|
|
if (hasResourceTarget) {
|
|
var url = null;
|
|
if ("src" in target && typeof target.src === "string") {
|
|
url = target.src;
|
|
} else if ("href" in target && typeof target.href === "string") {
|
|
url = target.href;
|
|
}
|
|
send({
|
|
kind: "resource-error",
|
|
message: "Resource failed to load",
|
|
source: event.filename || null,
|
|
line: typeof event.lineno === "number" ? event.lineno : null,
|
|
column: typeof event.colno === "number" ? event.colno : null,
|
|
url: url || location.href,
|
|
stack: null,
|
|
timestamp: now(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
var message = clampText(event.message, 12000) || "Unhandled window error";
|
|
var stack = event.error && event.error.stack ? String(event.error.stack) : null;
|
|
send({
|
|
kind: "window-error",
|
|
message: message,
|
|
stack: stack,
|
|
source: event.filename || null,
|
|
line: typeof event.lineno === "number" ? event.lineno : null,
|
|
column: typeof event.colno === "number" ? event.colno : null,
|
|
url: location.href,
|
|
timestamp: now(),
|
|
});
|
|
}, true);
|
|
|
|
window.addEventListener("unhandledrejection", function (event) {
|
|
var reason = event.reason;
|
|
var stack = reason && reason.stack ? String(reason.stack) : null;
|
|
send({
|
|
kind: "unhandled-rejection",
|
|
message: stringifyUnknown(reason),
|
|
stack: stack,
|
|
url: location.href,
|
|
timestamp: now(),
|
|
});
|
|
});
|
|
|
|
if (config.includeFetchErrors && typeof window.fetch === "function") {
|
|
var originalFetch = window.fetch.bind(window);
|
|
window.fetch = function () {
|
|
var args = arguments;
|
|
var requestUrl = null;
|
|
if (typeof args[0] === "string") {
|
|
requestUrl = args[0];
|
|
} else if (args[0] && typeof args[0].url === "string") {
|
|
requestUrl = args[0].url;
|
|
}
|
|
|
|
return originalFetch.apply(window, args).then(function (response) {
|
|
if (!response.ok && response.status >= 500) {
|
|
send({
|
|
kind: "fetch-response-error",
|
|
message: "Fetch returned HTTP " + response.status,
|
|
url: requestUrl || location.href,
|
|
timestamp: now(),
|
|
extra: {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
},
|
|
});
|
|
}
|
|
return response;
|
|
}).catch(function (error) {
|
|
send({
|
|
kind: "fetch-error",
|
|
message: stringifyUnknown(error),
|
|
stack: error && error.stack ? String(error.stack) : null,
|
|
url: requestUrl || location.href,
|
|
timestamp: now(),
|
|
});
|
|
throw error;
|
|
});
|
|
};
|
|
}
|
|
|
|
})();`;
|
|
}
|