@ajaltech/monitrace
Version:
Librería de monitoreo de errores frontend creada por Ajal Technology
365 lines (358 loc) • 10.2 kB
JavaScript
// src/http/axios.ts
import axios from "axios";
// src/storage/local.ts
var STORAGE_KEY = "monitrace-errors";
var MAX_ERRORS = 100;
function getStoredErrors() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
} catch {
return [];
}
}
function saveErrorLocally(error) {
try {
const existing = getStoredErrors();
if (existing.length >= MAX_ERRORS) {
existing.shift();
}
existing.push(error);
localStorage.setItem(STORAGE_KEY, JSON.stringify(existing));
} catch {
console.warn("[Monitrace] Error al guardar en localStorage.");
}
}
function overwriteStoredErrors(errors) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(errors));
} catch {
console.warn("[Monitrace] Error al escribir errores pendientes.");
}
}
// src/storage/sender.ts
var pendingErrors = /* @__PURE__ */ new Map();
var DEBOUNCE_DELAY_MS = 10 * 1e3;
function getErrorKey(payload) {
return `${payload.type}|${payload.message}|${payload.stack}`;
}
function sendError(error, config2) {
if (!isMonitraceInitialized()) {
console.warn("[Monitrace] No inicializado. No se puede enviar error.");
return;
}
const enrichedError = {
...error,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
userContext: getCurrentUserContext?.() || void 0,
appName: config2.appName,
version: config2.version
};
const key = getErrorKey(enrichedError);
let pending = pendingErrors.get(key);
if (pending) {
pending.count += 1;
clearTimeout(pending.timeoutId);
pending.timeoutId = window.setTimeout(() => {
flushError(key, config2);
}, DEBOUNCE_DELAY_MS);
} else {
pending = {
payload: { ...enrichedError },
count: 1,
timeoutId: window.setTimeout(() => {
flushError(key, config2);
}, DEBOUNCE_DELAY_MS)
};
pendingErrors.set(key, pending);
}
}
function flushError(key, config2) {
const pending = pendingErrors.get(key);
if (!pending) return;
if (pending.count > 1) {
pending.payload.count = pending.count;
}
console.info("[Monitrace] Enviando error (con debounce):", pending.payload);
try {
fetch(config2.endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(pending.payload),
credentials: "omit"
}).catch(() => {
if (config2.localFallback) {
saveErrorLocally(pending.payload);
console.info("[Monitrace] Error guardado localmente por fallo en el env\xEDo.");
}
});
} catch {
if (config2.localFallback) {
saveErrorLocally(pending.payload);
console.info("[Monitrace] Error guardado localmente por excepci\xF3n de red.");
}
}
pendingErrors.delete(key);
}
function flushAllPendingErrors(config2) {
console.info("[Monitrace] Flushing all pending errors (manual flush).");
pendingErrors.forEach((pending, key) => {
clearTimeout(pending.timeoutId);
if (pending.count > 1) {
pending.payload.count = pending.count;
}
try {
fetch(config2.endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(pending.payload),
credentials: "omit"
}).catch(() => {
if (config2.localFallback) {
saveErrorLocally(pending.payload);
console.info("[Monitrace] Error guardado localmente por fallo en el env\xEDo (manual flush).");
}
});
} catch {
if (config2.localFallback) {
saveErrorLocally(pending.payload);
console.info("[Monitrace] Error guardado localmente por excepci\xF3n de red (manual flush).");
}
}
});
pendingErrors.clear();
}
function flushStoredErrors(config2) {
const stored = getStoredErrors();
if (!stored.length) return;
const remaining = [];
let sendCount = 0;
stored.forEach((error) => {
try {
fetch(config2.endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(error),
credentials: "omit"
}).then((res) => {
if (!res.ok) {
remaining.push(error);
} else {
sendCount++;
}
}).catch(() => {
remaining.push(error);
});
} catch {
remaining.push(error);
}
});
setTimeout(() => {
overwriteStoredErrors(remaining);
if (sendCount > 0) {
console.info(`[Monitrace] Reenviados correctamente ${sendCount} errores desde el almacenamiento local.`);
}
}, 2e3);
}
function startErrorFlusher(config2, interval = 5 * 60 * 1e3) {
setInterval(() => flushStoredErrors(config2), interval);
}
// src/http/axios.ts
function setupAxiosInterceptor(config2) {
axios.interceptors.response.use(
(response) => response,
(error) => {
const payload = {
message: error.message,
stack: error.stack || "",
route: window.location.href,
type: "http"
};
sendError(payload, config2);
return Promise.reject(error);
}
);
console.info("[Monitrace] Interceptor de Axios instalado.");
}
// src/http/fetch.ts
var originalFetch;
function setupFetchInterceptor(config2) {
if (!window.fetch) {
console.warn("[Monitrace] Fetch no est\xE1 disponible en este entorno.");
return;
}
if (!originalFetch) {
originalFetch = window.fetch;
}
window.fetch = async (...args) => {
try {
const response = await originalFetch(...args);
if (!response.ok) {
sendError({
message: `HTTP error: ${response.status} ${response.statusText}`,
stack: "",
route: window.location.href,
type: "http"
}, config2);
}
return response;
} catch (error) {
sendError({
message: error.message || "Error desconocido en fetch",
stack: error.stack || "",
route: window.location.href,
type: "http"
}, config2);
throw error;
}
};
console.info("[Monitrace] Interceptor de fetch instalado.");
}
// src/http/xhr.ts
var isPatched = false;
function setupXHRInterceptor(config2) {
if (isPatched) return;
const OriginalXHR = window.XMLHttpRequest;
class MonitraceXHR extends OriginalXHR {
constructor() {
super(...arguments);
this._url = "";
}
open(method, url, ...args) {
this._url = url;
return super.open(method, url, ...args);
}
send(body) {
this.addEventListener("error", () => {
sendError({
message: `XHR error to ${this._url}`,
stack: "",
route: window.location.href,
type: "http"
}, config2);
});
this.addEventListener("abort", () => {
sendError({
message: `XHR aborted: ${this._url}`,
stack: "",
route: window.location.href,
type: "http"
}, config2);
});
this.addEventListener("load", () => {
if (this.status >= 400) {
sendError({
message: `XHR failed: ${this.status} ${this.statusText}`,
stack: "",
route: window.location.href,
type: "http"
}, config2);
}
});
super.send(body);
}
}
window.XMLHttpRequest = MonitraceXHR;
isPatched = true;
console.info("[Monitrace] Interceptor de XHR instalado.");
}
// src/core.ts
var config = null;
var isInitialized = false;
var currentUserContext = null;
function initMonitrace(userConfig) {
if (!userConfig.endpoint) {
console.warn("[Monitrace] \u274C No se ha configurado el endpoint del backend. Abortando inicializaci\xF3n.");
return;
}
if (!userConfig.appName) {
console.warn("[Monitrace] \u274C Falta el nombre de la aplicaci\xF3n (appName). Abortando inicializaci\xF3n.");
return;
}
if (!["axios", "fetch", "xhr"].includes(userConfig.client)) {
console.warn(`[Monitrace] \u274C Tipo de cliente HTTP inv\xE1lido: "${userConfig.client}". Use "axios", "fetch" o "xhr".`);
return;
}
if (isInitialized) {
console.warn("[Monitrace] Ya fue inicializado. Ignorando reinicializaci\xF3n.");
return;
}
config = userConfig;
if (config.userContext) {
currentUserContext = config.userContext;
}
if (config.localFallback) {
startErrorFlusher(config, 10 * 60 * 1e3);
}
isInitialized = true;
window.addEventListener("error", (e) => {
sendError({
message: e.message,
stack: e.error?.stack || "",
route: window.location.href,
type: "global"
}, config);
});
window.onerror = function(message, source, lineno, colno, error) {
sendError({
message,
stack: error?.stack || "",
route: window.location.href,
type: "global-onerror"
}, config);
};
window.addEventListener("unhandledrejection", (e) => {
const reason = e.reason instanceof Error ? { message: e.reason.message, stack: e.reason.stack } : { message: String(e.reason), stack: "" };
sendError({
message: reason.message,
stack: reason.stack,
route: window.location.href,
type: "promise"
}, config);
});
switch (config.client) {
case "axios":
setupAxiosInterceptor(config);
break;
case "fetch":
setupFetchInterceptor(config);
break;
case "xhr":
setupXHRInterceptor(config);
break;
}
console.info(`[Monitrace] \u2705 Inicializado correctamente para "${config.appName}".`);
}
function isContextFunction(fn) {
return typeof fn === "function";
}
function setUserContext(contextOrFn) {
if (isContextFunction(contextOrFn)) {
currentUserContext = contextOrFn;
} else {
currentUserContext = () => contextOrFn;
}
}
function getCurrentUserContext() {
return currentUserContext ? currentUserContext() : null;
}
function isMonitraceInitialized() {
return isInitialized;
}
function getMonitraceStatus() {
return {
isInitialized,
hasEndpoint: !!config?.endpoint,
hasAppName: !!config?.appName
};
}
export {
flushAllPendingErrors,
flushStoredErrors,
getCurrentUserContext,
getMonitraceStatus,
initMonitrace,
isMonitraceInitialized,
sendError,
setUserContext,
startErrorFlusher
};