UNPKG

@ajaltech/monitrace

Version:

Librería de monitoreo de errores frontend creada por Ajal Technology

410 lines (401 loc) 12.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { flushAllPendingErrors: () => flushAllPendingErrors, flushStoredErrors: () => flushStoredErrors, getCurrentUserContext: () => getCurrentUserContext, getMonitraceStatus: () => getMonitraceStatus, initMonitrace: () => initMonitrace, isMonitraceInitialized: () => isMonitraceInitialized, sendError: () => sendError, setUserContext: () => setUserContext, startErrorFlusher: () => startErrorFlusher }); module.exports = __toCommonJS(index_exports); // src/http/axios.ts var import_axios = __toESM(require("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) { import_axios.default.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 }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { flushAllPendingErrors, flushStoredErrors, getCurrentUserContext, getMonitraceStatus, initMonitrace, isMonitraceInitialized, sendError, setUserContext, startErrorFlusher });