Arquitectura y estructura del proyecto
Este SDK proporciona capacidades completas de monitoreo frontend, incluyendo procesameinto en Web Workers, garantía de fiabilidad de datos y supervisión multidimensional. La arquitectura se divide en módulos específicos para manejar la recolección, procesamiento y reporte de métricas de manera eficiente.
// Estructura de directorios propuesta
frontend-monitor-sdk/
├── src/
│ ├── kernel/ // Núcleo del SDK
│ │ ├── main.js // Punto de entrada principal
│ │ ├── settings.js // Gestión de configuración
│ │ └── workers/ // Módulos de Web Workers
│ ├── collectors/ // Módulos de recolección de datos
│ │ ├── base-collector.js // Clase base para recolectores
│ │ ├── errors.js // Supervisión de errores
│ │ ├── metrics.js // Métricas de rendimiento
│ │ ├── interactions.js // Interacciones de usuario
│ │ ├── views.js // Monitoreo de visualizaciones
│ │ └── assets.js // Carga de recursos
│ ├── dispatchers/ // Módulos de envío de datos
│ │ ├── dispatcher.js // Estrategia de reporte
│ │ └── persistence.js // Almacenamiento local
│ ├── helpers/ // Funciones de utilidad
│ │ ├── anonymizer.js // Anonimización de datos
│ │ ├── connectivity.js // Detección de red
│ │ └── dom-utils.js // Manipulación del DOM
│ └── typings/ // Definiciones de tipos
└── build/ // Salida de compilación
Implementación del módulo principle
El punto de entrada principal inicializa los componentes esenciales y gestiona el ciclo de vida del SDK.
import { SettingsManager } from './settings.js';
import { WorkerController } from './workers/controller.js';
import { startErrorCollecting } from '../collectors/errors.js';
import { startMetricsCollecting } from '../collectors/metrics.js';
import { startInteractionCollecting } from '../collectors/interactions.js';
import { startViewCollecting } from '../collectors/views.js';
import { DataDispatcher } from '../dispatchers/dispatcher.js';
class ProductionMonitor {
constructor() {
this.settings = new SettingsManager();
this.workerController = null;
this.dispatcher = null;
this.active = false;
}
async initialize(userOptions = {}) {
if (this.active) return;
try {
await this.settings.applyOptions(userOptions);
this.workerController = new WorkerController(this.settings);
await this.workerController.setup();
this.dispatcher = new DataDispatcher(this.workerController, this.settings);
this.registerCollectors();
this.setupUnloadHandlers();
this.active = true;
console.log('Production Monitor SDK activado');
} catch (err) {
console.error('Fallo al activar el SDK:', err);
}
}
registerCollectors() {
const { workerController, settings } = this;
startErrorCollecting(workerController, settings);
startMetricsCollecting(workerController, settings);
startInteractionCollecting(workerController, settings);
if (settings.get('trackViews')) {
startViewCollecting(workerController, settings);
}
}
setupUnloadHandlers() {
window.addEventListener('beforeunload', () => {
this.dispatcher.immediateSend();
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.dispatcher.sendPendingData();
}
});
}
logEvent(eventName, payload) {
if (!this.active) return;
this.workerController.postMessage({
category: 'USER_EVENT',
content: {
eventName,
...payload,
time: Date.now()
}
});
}
identifyUser(userData) {
if (!this.active) return;
this.workerController.postMessage({
category: 'IDENTIFY_USER',
content: userData
});
}
}
const monitor = new ProductionMonitor();
if (globalThis.__MONITOR_OPTIONS__) {
monitor.initialize(globalThis.__MONITOR_OPTIONS__);
}
export { monitor as default };
Gestión de configuración
El sistema de configuración maneja valores predteerminados y permite actualizaciones dinámicas.
export class SettingsManager {
constructor() {
this.defaults = {
endpoint: '',
projectId: '',
sdkVersion: '1.2.0',
sampling: 0.15,
errorCapture: true,
metricsCapture: true,
interactionCapture: true,
viewCapture: false,
resourceCapture: true,
bufferLimit: 1500,
batchSend: 60,
cycleInterval: 25000,
attempts: 4,
backoff: 1200,
anonymization: {
active: true,
fields: ['phoneNumber', 'nationalId', 'emailAddress']
},
routing: {
active: true,
delay: 800
}
};
this.current = { ...this.defaults };
}
async applyOptions(userOptions) {
this.current = { ...this.defaults, ...userOptions };
try {
const remote = await this.loadRemoteSettings();
this.current = { ...this.current, ...remote };
} catch (e) {
console.warn('Configuración remota no disponible:', e);
}
return this.current;
}
async loadRemoteSettings() {
const { projectId, endpoint } = this.current;
if (!projectId || !endpoint) return {};
const res = await fetch(`${endpoint}/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId, version: this.current.sdkVersion })
});
if (!res.ok) throw new Error('Fallo en carga de configuración');
return await res.json();
}
get(key) {
return key ? this.current[key] : this.current;
}
update(key, value) {
this.current[key] = value;
}
}
Control de Web Workers
El controlador de Workers maneja la comunicación y proporciona una alternativa de fallback en el hilo principal.
import { DataProcessor } from './processor.js';
export class WorkerController {
constructor(settings) {
this.settings = settings;
this.worker = null;
this.dataQueue = [];
this.ready = false;
}
async setup() {
if (typeof Worker === 'undefined') {
this.processor = new DataProcessor(this.settings);
await this.processor.setup();
} else {
this.worker = new Worker('./worker-bundle.js');
this.configureMessaging();
}
this.ready = true;
}
configureMessaging() {
this.worker.onmessage = (evt) => {
const { action, payload } = evt.data;
if (action === 'QUEUE_SYNC') {
this.dataQueue = payload.queue;
} else if (action === 'SEND_STATUS') {
this.handleSendStatus(payload);
}
};
}
postMessage(msg) {
if (!this.ready) {
console.warn('Controlador no inicializado');
return;
}
if (this.worker) {
this.worker.postMessage(msg);
} else {
this.processOnMainThread(msg);
}
}
async processOnMainThread(msg) {
try {
const processed = await this.processor.handle(msg);
if (processed) {
this.dataQueue.push(processed);
if (this.dataQueue.length >= this.settings.get('batchSend')) {
await this.flushData();
}
}
} catch (e) {
console.error('Error en procesamiento principal:', e);
}
}
async flushData() {
if (this.worker) {
this.worker.postMessage({ action: 'FLUSH' });
} else {
await this.processor.send(this.dataQueue);
this.dataQueue = [];
}
}
handleSendStatus(info) {
const { success, failed } = info;
if (!success && failed > 0) {
console.warn(`Envío fallido, ${failed} registros pendientes`);
}
}
}
Lógica principal del Worker
El Worker gestiona cola de datos, almacenamiento local y envíos programados.
importScripts('./processor.js', '../storage/indexdb-storage.js');
let queue = [];
let sending = false;
let processor;
let storage;
self.onmessage = async (evt) => {
const { action, content } = evt.data;
switch (action) {
case 'INIT':
await initialize(content.settings);
break;
case 'USER_EVENT':
case 'ERROR':
case 'METRIC':
case 'INTERACTION':
case 'VIEW':
await processEntry(action, content);
break;
case 'FLUSH':
await flushQueue();
break;
case 'IDENTIFY_USER':
setUserContext(content);
break;
}
};
async function initialize(settings) {
processor = new DataProcessor(settings);
storage = new IndexDBStorage(settings);
const stored = await storage.retrieveQueue();
queue = [...queue, ...stored];
self.postMessage({
action: 'QUEUE_SYNC',
payload: { queue: queue.length }
});
setInterval(() => {
if (queue.length > 0) {
flushQueue();
}
}, settings.cycleInterval || 25000);
}
async function processEntry(category, data) {
try {
const entry = await processor.transform({
category,
content: data,
timestamp: Date.now()
});
if (entry) {
queue.push(entry);
if (queue.length > (processor.settings.bufferLimit || 1500)) {
await persistQueue();
queue = [];
}
self.postMessage({
action: 'QUEUE_SYNC',
payload: { queue: queue.length }
});
}
} catch (e) {
console.error('Fallo en procesamiento:', e);
}
}
async function flushQueue() {
if (sending || queue.length === 0) return;
sending = true;
try {
const success = await processor.send(queue);
self.postMessage({
action: 'SEND_STATUS',
payload: {
success,
failed: success ? 0 : queue.length
}
});
if (success) {
queue = [];
await storage.clear();
} else {
await persistQueue();
}
} catch (e) {
console.error('Fallo en envío:', e);
await persistQueue();
} finally {
sending = false;
}
}
async function persistQueue() {
if (queue.length > 0) {
await storage.saveQueue(queue);
}
}
function setUserContext(userData) {
processor.setUserContext(userData);
}
Recolector de errores
Este módulo captura excepciones JavaScript, errores de recursos y fallos en frameworks.
import { registerLog } from './base-collector.js';
export function startErrorCollecting(controller, settings) {
if (!settings.get('errorCapture')) return;
window.addEventListener('error', (evt) => {
logError('RUNTIME', {
text: evt.message,
file: evt.filename,
line: evt.lineno,
column: evt.colno,
error: serializeError(evt.error)
}, controller);
});
window.addEventListener('unhandledrejection', (evt) => {
logError('PROMISE', {
text: evt.reason?.message || 'Rechazo no manejado',
details: serializeError(evt.reason)
}, controller);
});
window.addEventListener('error', (evt) => {
const target = evt.target;
if (target !== window && target.nodeType === 1) {
logError('ASSET', {
tag: target.tagName,
source: target.src || target.href,
identifier: target.id,
classes: target.className
}, controller);
}
}, true);
replaceConsoleError(controller);
}
function logError(kind, errorInfo, controller) {
registerLog(controller, {
category: 'ERROR',
content: {
errorKind: kind,
...errorInfo,
pageUrl: window.location.href,
agent: navigator.userAgent,
time: Date.now()
}
});
}
function serializeError(err) {
if (!err) return null;
return {
name: err.name,
message: err.message,
stack: err.stack
};
}
function replaceConsoleError(controller) {
const originalError = console.error;
console.error = function(...args) {
registerLog(controller, {
category: 'ERROR',
content: {
errorKind: 'CONSOLE',
messages: args.map(a =>
typeof a === 'object' ? serializeError(a) : String(a)
),
time: Date.now()
}
});
originalError.apply(console, args);
};
}
export function setupVueErrorHandling(Vue, controller) {
Vue.config.errorHandler = (err, vm, info) => {
registerLog(controller, {
category: 'ERROR',
content: {
errorKind: 'VUE',
message: err.message,
stack: err.stack,
component: vm?.$options?.name,
hook: info,
time: Date.now()
}
});
};
}
export class ReactErrorCatcher extends React.Component {
constructor(props) {
super(props);
this.state = { error: false };
}
static getDerivedStateFromError() {
return { error: true };
}
componentDidCatch(error, info) {
registerLog(this.props.controller, {
category: 'ERROR',
content: {
errorKind: 'REACT',
message: error.message,
stack: error.stack,
componentStack: info.componentStack,
time: Date.now()
}
});
}
render() {
if (this.state.error) {
return this.props.fallback || <div>Error detectado.</div>;
}
return this.props.children;
}
}
Recolector de métricas de rendimiento
Supervisa indicadores clave de rendimiento, recursos cargados y tareas largas.
import { registerLog } from './base-collector.js';
export function startMetricsCollecting(controller, settings) {
if (!settings.get('metricsCapture')) return;
collectWebVitals(controller);
observeResources(controller);
if (settings.get('routing.active')) {
monitorRouting(controller, settings);
}
trackLongTasks(controller);
}
function collectWebVitals(controller) {
if (typeof getCLS === 'function') {
getCLS(reportVital('CLS', controller));
getFID(reportVital('FID', controller));
getFCP(reportVital('FCP', controller));
getLCP(reportVital('LCP', controller));
getTTFB(reportVital('TTFB', controller));
} else {
manualPerformanceCalculation(controller);
}
}
function reportVital(name, controller) {
return (vital) => {
registerLog(controller, {
category: 'METRIC',
content: {
metricKind: 'WEB_VITAL',
name,
value: vital.value,
score: vital.rating,
change: vital.delta,
time: Date.now()
}
});
};
}
function manualPerformanceCalculation(controller) {
window.addEventListener('load', () => {
setTimeout(() => {
const timing = performance.timing;
const navigationEntry = performance.getEntriesByType('navigation')[0];
const data = {
redirect: timing.redirectEnd - timing.redirectStart,
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
ttfb: timing.responseStart - timing.requestStart,
response: timing.responseEnd - timing.responseStart,
domParse: timing.domComplete - timing.domInteractive,
domReady: timing.domContentLoadedEventEnd - timing.navigationStart,
fullLoad: timing.loadEventEnd - timing.navigationStart,
fp: getFirstPaintTime(),
fcp: getFirstContentfulPaintTime(),
lcp: getLargestContentfulPaintTime()
};
registerLog(controller, {
category: 'METRIC',
content: {
metricKind: 'PAGE_LOAD',
...data,
time: Date.now()
}
});
}, 0);
});
}
function observeResources(controller) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'resource') {
registerLog(controller, {
category: 'METRIC',
content: {
metricKind: 'RESOURCE',
url: entry.name,
duration: entry.duration,
size: entry.transferSize,
type: classifyResource(entry.name),
time: Date.now()
}
});
}
});
});
observer.observe({ entryTypes: ['resource'] });
}
function classifyResource(url) {
if (url.includes('.css')) return 'stylesheet';
if (url.includes('.js')) return 'script';
if (url.match(/\.(jpg|jpeg|png|gif|webp|svg)/i)) return 'image';
if (url.includes('/api/') || url.includes('/graphql')) return 'request';
return 'other';
}
function trackLongTasks(controller) {
if (!PerformanceObserver) return;
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 50) {
registerLog(controller, {
category: 'METRIC',
content: {
metricKind: 'LONG_TASK',
duration: entry.duration,
start: entry.startTime,
attribution: entry.attribution,
time: Date.now()
}
});
}
});
});
try {
observer.observe({ entryTypes: ['longtask'] });
} catch (e) {
// Navegador no soporta longtask
}
}
function monitorRouting(controller, settings) {
let currentPath = getCurrentPath();
let routeStart = Date.now();
const onRouteChange = () => {
const newPath = getCurrentPath();
if (newPath === currentPath) return;
const stayDuration = Date.now() - routeStart;
registerLog(controller, {
category: 'METRIC',
content: {
metricKind: 'ROUTE_STAY',
from: currentPath,
to: newPath,
duration: stayDuration,
time: Date.now()
}
});
currentPath = newPath;
routeStart = Date.now();
setTimeout(() => {
captureRouteMetrics(newPath, controller);
}, settings.get('routing.delay'));
};
window.addEventListener('hashchange', onRouteChange);
window.addEventListener('popstate', onRouteChange);
extendHistoryMethods(onRouteChange);
}
function getCurrentPath() {
return window.location.pathname + window.location.search + window.location.hash;
}
function extendHistoryMethods(callback) {
const push = history.pushState;
const replace = history.replaceState;
history.pushState = function(...args) {
push.apply(this, args);
callback();
};
history.replaceState = function(...args) {
replace.apply(this, args);
callback();
};
}
function captureRouteMetrics(path, controller) {
const metrics = {
route: path,
fp: getFirstPaintTime(),
fcp: getFirstContentfulPaintTime(),
ready: Date.now() - performance.timing.navigationStart
};
registerLog(controller, {
category: 'METRIC',
content: {
metricKind: 'ROUTE_LOAD',
...metrics,
time: Date.now()
}
});
}
Recolector de interacciones de usuario
Captura eventos de clic, tiempo de permanencia, desplazamiento y cambios de visibilidad.
import { registerLog } from './base-collector.js';
export function startInteractionCollecting(controller, settings) {
if (!settings.get('interactionCapture')) return;
trackClicks(controller);
trackStayDuration(controller);
trackScrolling(controller);
trackVisibilityChanges(controller);
}
function trackClicks(controller) {
document.addEventListener('click', (evt) => {
const target = evt.target;
const clickInfo = {
posX: evt.clientX,
posY: evt.clientY,
element: {
tag: target.tagName,
id: target.id,
classes: target.className,
text: extractText(target),
path: buildSelectorPath(target)
},
time: Date.now()
};
// Control de muestreo
if (Math.random() < 0.1) { // 10% de muestreo
registerLog(controller, {
category: 'INTERACTION',
content: {
action: 'CLICK',
...clickInfo
}
});
}
}, { capture: true });
}
function trackStayDuration(controller) {
let pageStart = Date.now();
let visibleStart = Date.now();
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
const visibleDuration = Date.now() - visibleStart;
registerLog(controller, {
category: 'INTERACTION',
content: {
action: 'VISIBLE_DURATION',
duration: visibleDuration,
time: Date.now()
}
});
} else {
visibleStart = Date.now();
}
});
window.addEventListener('beforeunload', () => {
const totalDuration = Date.now() - pageStart;
registerLog(controller, {
category: 'INTERACTION',
content: {
action: 'PAGE_DURATION',
duration: totalDuration,
time: Date.now()
}
});
});
}
function trackScrolling(controller) {
let depthRecorded = false;
window.addEventListener('scroll', throttle(() => {
const depth = calculateScrollDepth();
if (depth > 0.8 && !depthRecorded) {
registerLog(controller, {
category: 'INTERACTION',
content: {
action: 'SCROLL_DEPTH',
depth,
time: Date.now()
}
});
depthRecorded = true;
}
}, 1000));
}
function calculateScrollDepth() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
const clientHeight = window.innerHeight;
return scrollTop / (scrollHeight - clientHeight);
}
function trackVisibilityChanges(controller) {
let changes = 0;
document.addEventListener('visibilitychange', () => {
changes++;
registerLog(controller, {
category: 'INTERACTION',
content: {
action: 'VISIBILITY_CHANGE',
state: document.visibilityState,
count: changes,
time: Date.now()
}
});
});
}
// Funciones de utilidad
function extractText(element) {
return element.textContent?.trim().slice(0, 50) || '';
}
function buildSelectorPath(element) {
const parts = [];
while (element && element.nodeType === Node.ELEMENT_NODE) {
let selector = element.nodeName.toLowerCase();
if (element.id) {
selector += `#${element.id}`;
parts.unshift(selector);
break;
} else {
let sibling = element;
let nth = 1;
while (sibling = sibling.previousElementSibling) {
if (sibling.nodeName.toLowerCase() === selector) nth++;
}
if (nth !== 1) selector += `:nth-of-type(${nth})`;
}
parts.unshift(selector);
element = element.parentNode;
}
return parts.join(' > ');
}
function throttle(func, delay) {
let timeoutId;
let lastRun = 0;
return function(...args) {
const now = Date.now();
if (now - lastRun > delay) {
func.apply(this, args);
lastRun = now;
} else {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
lastRun = Date.now();
}, delay - (now - lastRun));
}
};
}
Herramientas de anonimización y cumplimiento
Provee funcionalidades para proteger datos sensibles y cumplir con regulaciones de privacidad.
// Enmascaramiento de información sensible
export const maskValue = (value, category = 'auto') => {
if (typeof value !== 'string') return value;
const patterns = {
phone: /(\d{3})\d{4}(\d{4})/,
id: /(\d{6})\d{8}(\w{4})/,
email: /(\w{3})(\w+)(@\w+\.\w+)/,
card: /(\d{4})\d{8,12}(\d{4})/,
auto: /\b(\d{3})\d{4}(\d{4})\b|\b(\d{6})\d{8}(\w{4})\b|\b(\w{3})(\w+)(@\w+\.\w+)\b/
};
const regex = patterns[category] || patterns.auto;
return value.replace(regex, (match, ...groups) => {
if (category === 'phone') {
return `${groups[0]}****${groups[1]}`;
} else if (category === 'id') {
return `${groups[0]}********${groups[1]}`;
} else if (category === 'email') {
return `${groups[0]}***${groups[2]}`;
} else if (category === 'card') {
return `${groups[0]}${'*'.repeat(match.length - 8)}${groups[1]}`;
}
// Auto-detección
if (match.length === 11 && /^1[3-9]\d{9}$/.test(match)) {
return `${match.slice(0, 3)}****${match.slice(7)}`;
} else if (match.length === 18 && /^\d{17}[\dXx]$/.test(match)) {
return `${match.slice(0, 6)}********${match.slice(14)}`;
} else if (match.includes('@')) {
const [local, domain] = match.split('@');
return `${local.slice(0, 3)}***@${domain}`;
}
return match;
});
};
// Enmascaramiento profundo de objetos
export const deepMask = (obj, sensitiveKeys = []) => {
if (typeof obj !== 'object' || obj === null) {
return typeof obj === 'string' ? maskValue(obj) : obj;
}
if (Array.isArray(obj)) {
return obj.map(item => deepMask(item, sensitiveKeys));
}
const result = {};
const defaultKeys = ['phone', 'mobile', 'idCard', 'identity', 'email', 'card', 'password'];
const allSensitive = [...defaultKeys, ...sensitiveKeys];
for (const [key, value] of Object.entries(obj)) {
const lowerKey = key.toLowerCase();
const isSensitive = allSensitive.some(k =>
lowerKey.includes(k.toLowerCase())
);
if (isSensitive && typeof value === 'string') {
result[key] = maskValue(value);
} else {
result[key] = deepMask(value, sensitiveKeys);
}
}
return result;
};
// Gestión de consentimiento de privacidad
export class PrivacyManager {
constructor() {
this.consent = false;
this.required = false;
this.checkRequirements();
}
checkRequirements() {
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const euZones = [
'Europe/', 'CET', 'EET', 'WET', 'BST', 'CEST', 'EEST', 'WEST'
];
this.required = euZones.some(tz => zone.includes(tz));
if (this.required) {
this.displayConsentBanner();
} else {
this.consent = true;
}
}
displayConsentBanner() {
console.log('Se requiere consentimiento para su región');
setTimeout(() => {
this.grantConsent();
}, 2000);
}
grantConsent() {
this.consent = true;
if (this.onConsent) {
this.onConsent();
}
}
revokeConsent() {
this.consent = false;
this.clearStoredData();
}
clearStoredData() {
if (window.indexedDB) {
const req = indexedDB.deleteDatabase('MONITOR_DATA_DB');
req.onsuccess = () => {
console.log('Datos de monitoreo eliminados por cumplimiento');
};
}
localStorage.removeItem('monitor_user_id');
sessionStorage.removeItem('monitor_session');
}
setConsentHandler(handler) {
this.onConsent = handler;
}
}
Despachador de datos
Maneja el envío de datos con reintentos, adaptación a red y almacenamiento persistente.
import { StorageHandler } from './persistence.js';
import { NetworkMonitor } from '../helpers/connectivity.js';
export class DataDispatcher {
constructor(controller, settings) {
this.controller = controller;
this.settings = settings;
this.storage = new StorageHandler(settings);
this.network = new NetworkMonitor();
this.online = navigator.onLine;
this.sending = false;
this.setupNetworkListeners();
}
setupNetworkListeners() {
window.addEventListener('online', () => {
this.online = true;
this.sendPendingData();
});
window.addEventListener('offline', () => {
this.online = false;
});
this.network.onQualityChange((connection) => {
this.adjustStrategy(connection);
});
}
adjustStrategy(connection) {
const { effectiveType, downlink, rtt } = connection;
if (effectiveType === '4g' && downlink > 5) {
this.settings.update('batchSend', 60);
this.settings.update('cycleInterval', 25000);
} else if (effectiveType === '3g' || downlink < 1) {
this.settings.update('batchSend', 15);
this.settings.update('cycleInterval', 45000);
} else {
this.settings.update('batchSend', 8);
this.settings.update('cycleInterval', 90000);
}
}
async sendPendingData() {
if (!this.online || this.sending) return;
try {
this.sending = true;
const pending = await this.storage.getPending();
if (pending.length > 0) {
await this.transmitBatch(pending);
await this.storage.clearPending();
}
} catch (e) {
console.error('Fallo en envío pendiente:', e);
} finally {
this.sending = false;
}
}
async transmitBatch(data, attempt = 0) {
if (data.length === 0) return true;
const maxAttempts = this.settings.get('attempts');
const endpoint = this.settings.get('endpoint');
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Project-Id': this.settings.get('projectId'),
'X-SDK-Version': this.settings.get('sdkVersion')
},
body: JSON.stringify({
events: data,
timestamp: Date.now(),
session: this.getSessionIdentifier()
}),
keepalive: true
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
console.log(`Enviados ${data.length} registros`);
return true;
} catch (e) {
console.error(`Fallo en envío (intento ${attempt + 1}):`, e);
if (attempt < maxAttempts) {
const delay = this.settings.get('backoff') * Math.pow(2, attempt);
await new Promise(r => setTimeout(r, delay));
return this.transmitBatch(data, attempt + 1);
} else {
await this.storage.savePending(data);
return false;
}
}
}
async sendPendingData() {
if (this.controller) {
this.controller.flushData();
} else {
await this.sendPendingData();
}
}
async immediateSend() {
if (!navigator.sendBeacon) {
await this.sendPendingData();
return;
}
const pending = await this.storage.getPending();
if (pending.length > 0) {
const payload = JSON.stringify({
events: pending,
timestamp: Date.now(),
session: this.getSessionIdentifier()
});
navigator.sendBeacon(this.settings.get('endpoint'), payload);
}
}
getSessionIdentifier() {
let id = sessionStorage.getItem('monitor_session_id');
if (!id) {
id = createUniqueId();
sessionStorage.setItem('monitor_session_id', id);
}
return id;
}
}
function createUniqueId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
Ejemplos de integración y uso
Implementación básica en HTML y ejemplos para frameworks populares.
<!-- Implementación HTML básica -->
<html>
<head>
<title>Aplicación de ejemplo</title>
<script>
globalThis.__MONITOR_OPTIONS__ = {
projectId: 'ejemplo-123',
endpoint: 'https://monitor.example.com/collect',
sampling: 0.1,
viewCapture: true
};
</script>
<script src="https://cdn.example.com/monitor-sdk.js"></script>
</head>
<body>
<div data-track-view="banner-principal">Contenido</div>
<script>
monitor.initialize({
projectId: 'ejemplo-123',
endpoint: 'https://monitor.example.com/collect'
});
monitor.identifyUser({
userId: 'user-789',
displayName: 'usuario_demo'
});
monitor.logEvent('COMPRA_REALIZADA', {
itemId: 'item-456',
valor: 149.99,
moneda: 'EUR'
});
</script>
</body>
</html>
// Integración con React
import React from 'react';
import { ReactErrorCatcher } from './monitor-sdk';
function App() {
return (
<ReactErrorCatcher
controller={monitor.workerController}
fallback={<div>Error inesperado</div>}
>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/producto" element={<Product />} />
</Routes>
</Router>
</ReactErrorCatcher>
);
}
function ProductCard({ item }) {
const ref = React.useRef();
React.useEffect(() => {
if (ref.current) {
monitor.logEvent('IMPRESION_PRODUCTO', {
itemId: item.id,
seccion: 'lista_productos'
});
}
}, [item.id]);
return (
<div ref={ref} className="product-card">
<img src={item.imagen} alt={item.nombre} />
<h3>{item.nombre}</h3>
<p>{item.precio}</p>
</div>
);
}
// Integración con Vue
import Vue from 'vue';
import { setupVueErrorHandling } from './monitor-sdk';
setupVueErrorHandling(Vue, monitor.workerController);
export default {
name: 'UserProfile',
methods: {
trackProfile() {
monitor.logEvent('PERFIL_VISTO', {
userId: this.user.id,
seccion: 'resumen'
});
}
},
mounted() {
this.trackProfile();
}
};
Configuración de compilación
Configuración para empaquetar el SDK como una biblioteca universal.
// webpack.config.js
module.exports = {
entry: './src/kernel/main.js',
output: {
filename: 'monitor-sdk.js',
path: path.resolve(__dirname, 'build'),
library: 'monitor',
libraryTarget: 'umd'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
optimization: {
minimize: true
}
};
El SDK incluye capacidades avanzadas como procesamiento en Web Workers, almacenamiento local con IndexedDB, adaptación dinámica a condiciones de red, anonimización automática de datos sensibles, monitoreo completo de errores, rendimiento e interacciones de usuario, y soporte para aplicaciones SPA con frameworks populares como React y Vue.