Skip to main content
Glama
monitor.js98.2 kB
// Brummer Web Monitoring Script // This script is injected into proxied web pages to collect telemetry data (function() { 'use strict'; // Check if already initialized to prevent multiple injections if (window.__brummerInitialized) { return; } // Mark as initialized immediately window.__brummerInitialized = true; // Configuration const BRUMMER_CONFIG = { telemetryEndpoint: (function() { // Try to determine the proxy server URL // Default to localhost:8888 (standard Brummer proxy port) const proxyHost = window.__brummerProxyHost || 'localhost:8888'; return 'http://' + proxyHost + '/__brummer_telemetry__'; })(), websocketEndpoint: (function() { // WebSocket endpoint for real-time telemetry const proxyHost = window.__brummerProxyHost || 'localhost:8888'; return 'ws://' + proxyHost + '/__brummer_ws__'; })(), batchInterval: 2000, // Send data every 2 seconds maxBatchSize: 100, collectInteractionMetrics: true, collectPerformanceMetrics: true, collectMemoryMetrics: true, collectConsoleMetrics: false, processName: window.__brummerProcessName || 'unknown', debugMode: false, // Disable debug output to keep console clean debugLevel: 'minimal', // verbose, normal, minimal useWebSocket: true // Use WebSocket instead of HTTP }; // Debug utilities for visual output const DEBUG = { colors: { success: '#22c55e', warning: '#f59e0b', error: '#ef4444', info: '#3b82f6', debug: '#8b5cf6' }, icons: { success: '✅', warning: '⚠️', error: '❌', info: 'ℹ️', debug: '🐛', network: '🌐', timer: '⏱️', buffer: '📊', send: '📤', receive: '📥', user: '👤', performance: '🚀', memory: '🧠', console: '💬' }, formatTime: function(timestamp) { const now = Date.now(); const diff = now - timestamp; if (diff < 1000) return 'just now'; if (diff < 60000) return `${Math.floor(diff/1000)}s ago`; if (diff < 3600000) return `${Math.floor(diff/60000)}m ago`; return `${Math.floor(diff/3600000)}h ago`; }, formatDuration: function(ms) { if (ms < 100) return `${this.icons.timer} ${ms}ms`; if (ms < 1000) return `⏳ ${ms}ms`; if (ms < 5000) return `🐌 ${(ms/1000).toFixed(1)}s`; return `🚨 ${(ms/1000).toFixed(1)}s`; }, formatSize: function(bytes) { if (bytes < 1024) return `${bytes}B`; if (bytes < 1024 * 1024) return `${(bytes/1024).toFixed(1)}KB`; return `${(bytes/(1024*1024)).toFixed(1)}MB`; }, progressBar: function(current, max, width = 10) { const filled = Math.floor((current / max) * width); const empty = width - filled; const percentage = Math.floor((current / max) * 100); return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${percentage}% (${current}/${max})`; }, log: function(level, category, message, data = null) { // Silently add to debug timeline without console output if (window.__brummer && window.__brummer.debug && window.__brummer.debug.eventTimeline) { window.__brummer.debug.eventTimeline.push({ timestamp: Date.now(), level, category, message, data }); // Keep only last 50 entries if (window.__brummer.debug.eventTimeline.length > 50) { window.__brummer.debug.eventTimeline.shift(); } } }, status: function() { if (!window.__brummer) return; const stats = window.__brummer.stats; const buffer = telemetryBuffer; const now = Date.now(); // Only show status if explicitly called by user console.log('%c🔍 BRUMMER TELEMETRY DEBUG DASHBOARD', 'font-size: 18px; font-weight: bold; color: #3b82f6;'); console.log('═══════════════════════════════════════'); console.log(`${this.icons.buffer} Buffer: ${this.progressBar(buffer.length, BRUMMER_CONFIG.maxBatchSize)}`); console.log(`${this.icons.network} Endpoint: ${BRUMMER_CONFIG.telemetryEndpoint} ${stats.lastPingSuccess ? '🟢 ONLINE' : '🔴 OFFLINE'} (${stats.lastPingTime || 'unknown'})`); console.log(`${this.icons.send} Last Send: ${stats.lastSendTime ? this.formatTime(stats.lastSendTime) : 'never'} ${stats.lastSendSuccess ? '✅ SUCCESS' : '❌ FAILED'}`); console.log(`${this.icons.timer} Next Flush: ${batchTimer ? `in ${Math.ceil((BRUMMER_CONFIG.batchInterval - (now - stats.lastBatchStart))/1000)}s` : 'not scheduled'}`); console.log(`${this.icons.receive} Total Sent: ${stats.totalEvents} events (${this.formatSize(stats.totalBytes)})`); console.log(`${this.icons.error} Errors: ${stats.errorCount} failures`); console.log(`${this.icons.performance} Session: ${this.formatTime(pageMetadata.pageLoadTime)} (${this.formatDuration(now - pageMetadata.pageLoadTime)})`); } }; // Telemetry buffer const telemetryBuffer = []; let batchTimer = null; // WebSocket connection let websocket = null; let wsConnected = false; let wsReconnectAttempts = 0; const wsMaxReconnectAttempts = 5; const wsReconnectDelay = 2000; // Statistics tracking const stats = { totalEvents: 0, totalBytes: 0, errorCount: 0, lastSendTime: null, lastSendSuccess: null, lastBatchStart: null, lastPingTime: null, lastPingSuccess: null, eventCounts: { page_load: 0, user_interaction: 0, performance_metrics: 0, memory_usage: 0, console_output: 0, javascript_error: 0, resource_timing: 0 }, networkStats: { requestCount: 0, successCount: 0, failureCount: 0, totalLatency: 0 } }; // Page metadata const pageMetadata = { url: window.location.href, referrer: document.referrer, userAgent: navigator.userAgent, sessionId: generateSessionId(), pageLoadTime: Date.now(), cookies: document.cookie ? document.cookie.split(';').length : 0, localStorage: typeof(Storage) !== "undefined" && localStorage.length || 0, sessionStorage: typeof(Storage) !== "undefined" && sessionStorage.length || 0 }; // Global debug interface window.__brummer = { config: BRUMMER_CONFIG, stats: stats, metadata: pageMetadata, buffer: telemetryBuffer, debug: { eventTimeline: [], status: DEBUG.status.bind(DEBUG), flush: () => flushTelemetry(), ping: () => BRUMMER_CONFIG.useWebSocket ? pingWebSocket() : pingEndpoint(), clear: () => { telemetryBuffer.length = 0; DEBUG.log('info', 'debug', 'Buffer cleared manually'); }, ws: () => websocket, reconnect: () => connectWebSocket(), timeline: () => { console.log('%c⏰ TELEMETRY TIMELINE (last 30 events)', 'font-size: 16px; font-weight: bold; color: #8b5cf6;'); console.log('════════════════════════════════════'); window.__brummer.debug.eventTimeline.slice(-30).forEach(entry => { console.log(`${DEBUG.formatTime(entry.timestamp)} │ ${DEBUG.icons[entry.level]} ${DEBUG.icons[entry.category]} ${entry.message}`); }); }, headers: () => { console.log('%c📡 REQUEST HEADERS & CONTEXT', 'font-size: 16px; font-weight: bold; color: #3b82f6;'); console.log('════════════════════════════════════'); console.log(`🔐 Authorization: ${document.cookie.includes('auth') ? 'Present' : 'Not found'}`); console.log(`🍪 Cookies: ${pageMetadata.cookies} items`); console.log(`💾 Local Storage: ${pageMetadata.localStorage} items`); console.log(`💾 Session Storage: ${pageMetadata.sessionStorage} items`); console.log(`🌐 Origin: ${window.location.origin}`); console.log(`🔗 Referrer: ${pageMetadata.referrer || 'Direct'}`); console.log(`📱 User Agent: ${navigator.userAgent.substring(0, 80)}...`); } } }; // Custom Metrics API (similar to mcpTelemetry) window.brummerTelemetry = { // Track custom events track: function(eventName, data) { sendTelemetry({ type: 'custom_event', data: { eventName: eventName, data: data, timestamp: Date.now() } }); DEBUG.log('info', 'custom', `Custom event tracked: ${eventName}`, data); }, // Mark performance points mark: function(name) { if (window.performance && window.performance.mark) { performance.mark(name); sendTelemetry({ type: 'performance_mark', data: { name: name, timestamp: performance.now() } }); DEBUG.log('info', 'performance', `Performance mark: ${name}`); } }, // Measure between marks measure: function(name, startMark, endMark) { if (window.performance && window.performance.measure) { try { performance.measure(name, startMark, endMark); const entries = performance.getEntriesByName(name, 'measure'); const measure = entries[entries.length - 1]; if (measure) { sendTelemetry({ type: 'performance_measure', data: { name: name, duration: measure.duration, startTime: measure.startTime } }); DEBUG.log('info', 'performance', `Performance measure: ${name} = ${measure.duration.toFixed(2)}ms`); } } catch (e) { DEBUG.log('error', 'performance', `Failed to measure ${name}: ${e.message}`); } } }, // Log custom error error: function(message, details) { sendTelemetry({ type: 'custom_error', data: { message: message, details: details, stack: (new Error()).stack, timestamp: Date.now() } }); DEBUG.log('error', 'custom', `Custom error: ${message}`, details); }, // Track user actions action: function(action, target, metadata) { sendTelemetry({ type: 'user_action', data: { action: action, target: target, metadata: metadata, timestamp: Date.now() } }); DEBUG.log('info', 'user', `User action: ${action} on ${target}`, metadata); }, // Track feature usage feature: function(featureName, metadata) { sendTelemetry({ type: 'feature_usage', data: { feature: featureName, metadata: metadata, timestamp: Date.now() } }); DEBUG.log('info', 'custom', `Feature used: ${featureName}`, metadata); }, // Capture screenshot programmatically screenshot: function(options = {}) { const requestId = 'api_screenshot_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); const screenshotData = { requestId: requestId, url: window.location.href, viewport: { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio }, fullPage: options.fullPage !== false, // Default to true selector: options.selector || null, timestamp: Date.now() }; if (wsConnected && websocket && websocket.readyState === WebSocket.OPEN) { sendWebSocketCommand('screenshot_request', screenshotData); } else { sendTelemetry({ type: 'screenshot_request', data: screenshotData }); flushTelemetry(); } DEBUG.log('info', 'screenshot', `Screenshot requested via API with ID: ${requestId}`, options); return requestId; } }; // Alias for compatibility window.mcpTelemetry = window.brummerTelemetry; // Generate a unique session ID function generateSessionId() { return 'brummer_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } // Connect to WebSocket for real-time telemetry function connectWebSocket() { if (!BRUMMER_CONFIG.useWebSocket) return; if (websocket && (websocket.readyState === WebSocket.CONNECTING || websocket.readyState === WebSocket.OPEN)) { DEBUG.log('info', 'network', 'WebSocket already connected or connecting'); return; } DEBUG.log('info', 'network', `🔌 Connecting to WebSocket: ${BRUMMER_CONFIG.websocketEndpoint}`); try { websocket = new WebSocket(BRUMMER_CONFIG.websocketEndpoint); websocket.onopen = function(event) { wsConnected = true; wsReconnectAttempts = 0; stats.lastPingSuccess = true; stats.lastPingTime = DEBUG.formatTime(Date.now()); DEBUG.log('success', 'network', '🔗 WebSocket connected successfully', { readyState: websocket.readyState, url: BRUMMER_CONFIG.websocketEndpoint }); }; websocket.onmessage = function(event) { try { const message = JSON.parse(event.data); handleWebSocketMessage(message); } catch (error) { DEBUG.log('error', 'network', 'Failed to parse WebSocket message', { error: error.message, data: event.data }); } }; websocket.onclose = function(event) { wsConnected = false; stats.lastPingSuccess = false; DEBUG.log('warning', 'network', `WebSocket connection closed`, { code: event.code, reason: event.reason, wasClean: event.wasClean }); // Attempt to reconnect if not intentionally closed if (event.code !== 1000 && wsReconnectAttempts < wsMaxReconnectAttempts) { wsReconnectAttempts++; DEBUG.log('info', 'network', `Attempting reconnect ${wsReconnectAttempts}/${wsMaxReconnectAttempts} in ${wsReconnectDelay}ms`); setTimeout(connectWebSocket, wsReconnectDelay); } }; websocket.onerror = function(error) { wsConnected = false; stats.lastPingSuccess = false; stats.errorCount++; DEBUG.log('error', 'network', 'WebSocket error occurred', { error: error, readyState: websocket ? websocket.readyState : 'null' }); }; } catch (error) { DEBUG.log('error', 'network', 'Failed to create WebSocket connection', { error: error.message, endpoint: BRUMMER_CONFIG.websocketEndpoint }); } } // Handle incoming WebSocket messages function handleWebSocketMessage(message) { DEBUG.log('info', 'network', `📥 WebSocket message: ${message.type}`, message); switch (message.type) { case 'connected': DEBUG.log('success', 'network', '🎉 WebSocket welcome received', message.data); break; case 'command_response': DEBUG.log('info', 'debug', `Command response: ${JSON.stringify(message.data).substring(0, 200)}...`); break; case 'telemetry': DEBUG.log('info', 'network', 'Telemetry broadcast received from another client'); break; case 'command': // Handle REPL commands if (message.data && message.data.action === 'repl') { handleREPLCommand(message.data); } break; case 'screenshot_response': // Handle screenshot response handleScreenshotResponse(message.data); break; default: DEBUG.log('info', 'network', `Unknown message type: ${message.type}`); } } // Handle REPL command from server function handleREPLCommand(data) { const { code, responseId, sessionId } = data; DEBUG.log('info', 'repl', `📝 Executing REPL command: ${code.substring(0, 100)}...`, { responseId, sessionId, codeLength: code.length }); // Execute the code and capture result let result; let error = null; try { // Create a function that returns the result of the code // This allows us to handle both expressions and statements const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; const fn = new AsyncFunction('return (' + code + ')'); // Execute the function and get the result Promise.resolve(fn()).then(res => { result = res; sendREPLResponse(responseId, result, null); }).catch(err => { // If it fails as an expression, try as statements try { const fn2 = new AsyncFunction(code); Promise.resolve(fn2()).then(res => { result = res; sendREPLResponse(responseId, result, null); }).catch(err2 => { error = err2.toString(); sendREPLResponse(responseId, null, error); }); } catch (err2) { error = err2.toString(); sendREPLResponse(responseId, null, error); } }); } catch (err) { // Try executing as statements if expression fails try { const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; const fn = new AsyncFunction(code); Promise.resolve(fn()).then(res => { result = res; sendREPLResponse(responseId, result, null); }).catch(err2 => { error = err2.toString(); sendREPLResponse(responseId, null, error); }); } catch (err2) { error = err2.toString(); sendREPLResponse(responseId, null, error); } } } // Handle screenshot response from server function handleScreenshotResponse(data) { const { requestId, success, error, filename, dataUrl } = data; DEBUG.log('info', 'screenshot', `Screenshot response received`, { requestId, success, error, filename }); // Clear timeout if exists if (window.__brummerScreenshotTimeout) { clearTimeout(window.__brummerScreenshotTimeout); window.__brummerScreenshotTimeout = null; } // Update UI const statusEl = document.getElementById('__brummer_screenshot_status'); const btnEl = document.getElementById('__brummer_screenshot_btn'); if (statusEl && btnEl) { btnEl.disabled = false; btnEl.textContent = '📸 Capture Screenshot'; if (success) { statusEl.textContent = `✅ Screenshot saved: ${filename || 'screenshot.png'}`; statusEl.style.color = '#22c55e'; // If we have a data URL, offer to download it if (dataUrl) { const link = document.createElement('a'); link.href = dataUrl; link.download = filename || 'screenshot.png'; link.click(); } } else { statusEl.textContent = `❌ Screenshot failed: ${error || 'Unknown error'}`; statusEl.style.color = '#ef4444'; } // Clear status after 5 seconds setTimeout(() => { if (statusEl) { statusEl.textContent = ''; } }, 5000); } } // Send REPL response back to server function sendREPLResponse(responseId, result, error) { const responseData = { responseId: responseId, result: result !== undefined ? result : null, error: error }; // Try to stringify the result safely try { // Handle circular references and functions const seen = new WeakSet(); responseData.result = JSON.parse(JSON.stringify(result, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular]'; } seen.add(value); } if (typeof value === 'function') { return value.toString(); } return value; })); } catch (e) { // If serialization fails, convert to string responseData.result = String(result); } DEBUG.log('info', 'repl', `📤 Sending REPL response`, { responseId, hasResult: result !== null && result !== undefined, hasError: error !== null }); sendWebSocketCommand('repl_response', responseData); } // Send command via WebSocket function sendWebSocketCommand(type, data = {}) { if (!websocket || websocket.readyState !== WebSocket.OPEN) { DEBUG.log('error', 'network', 'WebSocket not connected, cannot send command'); return false; } const message = { type: type, data: data, timestamp: Date.now() }; try { websocket.send(JSON.stringify(message)); DEBUG.log('info', 'network', `📤 Sent WebSocket command: ${type}`, data); return true; } catch (error) { DEBUG.log('error', 'network', `Failed to send WebSocket command: ${error.message}`); return false; } } // Ping via WebSocket function pingWebSocket() { const startTime = Date.now(); DEBUG.log('info', 'network', '🏓 Pinging via WebSocket...'); if (!sendWebSocketCommand('ping')) { // Fallback to HTTP ping if WebSocket fails DEBUG.log('warning', 'network', 'WebSocket ping failed, falling back to HTTP'); pingEndpoint(); } } // Ping endpoint to test connectivity function pingEndpoint() { const startTime = Date.now(); DEBUG.log('info', 'network', '🏓 Pinging telemetry endpoint...'); fetch(BRUMMER_CONFIG.telemetryEndpoint, { method: 'OPTIONS', headers: { 'Content-Type': 'application/json' } }) .then(response => { const latency = Date.now() - startTime; stats.lastPingTime = DEBUG.formatDuration(latency); stats.lastPingSuccess = response.ok; DEBUG.log('success', 'network', `Endpoint reachable`, { status: response.status, latency: `${latency}ms`, headers: Object.fromEntries(response.headers.entries()) }); }) .catch(error => { const latency = Date.now() - startTime; stats.lastPingTime = DEBUG.formatDuration(latency); stats.lastPingSuccess = false; stats.errorCount++; DEBUG.log('error', 'network', `Endpoint unreachable: ${error.message}`, { error: error.name, latency: `${latency}ms`, suggestion: 'Check if proxy server is running on the correct port' }); }); } // Send telemetry data to the server function sendTelemetry(data) { const eventData = { ...data, timestamp: Date.now(), sessionId: pageMetadata.sessionId, url: pageMetadata.url }; telemetryBuffer.push(eventData); // Update stats stats.totalEvents++; if (stats.eventCounts[data.type]) { stats.eventCounts[data.type]++; } const bufferStatus = DEBUG.progressBar(telemetryBuffer.length, BRUMMER_CONFIG.maxBatchSize); DEBUG.log('info', 'buffer', `Event buffered: ${data.type}`, { eventType: data.type, bufferSize: `${telemetryBuffer.length}/${BRUMMER_CONFIG.maxBatchSize}`, bufferStatus: bufferStatus, estimatedSize: DEBUG.formatSize(JSON.stringify(eventData).length) }); // Start batch timer if not already running if (!batchTimer) { stats.lastBatchStart = Date.now(); batchTimer = setTimeout(flushTelemetry, BRUMMER_CONFIG.batchInterval); DEBUG.log('info', 'timer', `Batch timer started (${BRUMMER_CONFIG.batchInterval}ms)`); } // Flush immediately if buffer is full if (telemetryBuffer.length >= BRUMMER_CONFIG.maxBatchSize) { DEBUG.log('warning', 'buffer', 'Buffer full, flushing immediately'); flushTelemetry(); } } // Flush telemetry buffer function flushTelemetry() { if (telemetryBuffer.length === 0) { DEBUG.log('info', 'send', 'No events to flush'); return; } const batch = telemetryBuffer.splice(0, telemetryBuffer.length); const startTime = Date.now(); // Try WebSocket first if enabled and connected if (BRUMMER_CONFIG.useWebSocket && wsConnected && websocket && websocket.readyState === WebSocket.OPEN) { flushTelemetryViaWebSocket(batch, startTime); return; } // Fallback to HTTP if WebSocket not available flushTelemetryViaHTTP(batch, startTime); } // Flush telemetry via WebSocket function flushTelemetryViaWebSocket(batch, startTime) { const payload = { sessionId: pageMetadata.sessionId, events: batch, metadata: { url: pageMetadata.url, referrer: pageMetadata.referrer, userAgent: navigator.userAgent, timestamp: Date.now(), cookies: document.cookie, viewport: { width: window.innerWidth, height: window.innerHeight }, connection: navigator.connection ? { effectiveType: navigator.connection.effectiveType, downlink: navigator.connection.downlink } : null } }; const payloadSize = JSON.stringify(payload).length; stats.totalBytes += payloadSize; stats.networkStats.requestCount++; DEBUG.log('info', 'send', `🚀 Flushing ${batch.length} events via WebSocket`, { batchSize: batch.length, payloadSize: DEBUG.formatSize(payloadSize), method: 'WebSocket', eventTypes: batch.reduce((acc, event) => { acc[event.type] = (acc[event.type] || 0) + 1; return acc; }, {}) }); // Send via WebSocket const message = { type: 'telemetry', data: payload, timestamp: Date.now() }; try { websocket.send(JSON.stringify(message)); const duration = Date.now() - startTime; stats.lastSendTime = Date.now(); stats.lastSendSuccess = true; stats.networkStats.successCount++; stats.networkStats.totalLatency += duration; DEBUG.log('success', 'send', `WebSocket telemetry sent successfully`, { duration: DEBUG.formatDuration(duration), success: true }); } catch (error) { const duration = Date.now() - startTime; stats.lastSendTime = Date.now(); stats.lastSendSuccess = false; stats.networkStats.failureCount++; stats.errorCount++; DEBUG.log('error', 'send', `WebSocket send failed: ${error.message}`, { error: error.name, duration: DEBUG.formatDuration(duration), suggestion: 'Falling back to HTTP' }); // Fallback to HTTP flushTelemetryViaHTTP(batch, startTime); } // Clear the timer if (batchTimer) { clearTimeout(batchTimer); batchTimer = null; DEBUG.log('info', 'timer', 'Batch timer cleared'); } } // Flush telemetry via HTTP (fallback) function flushTelemetryViaHTTP(batch, startTime) { // Use sendBeacon if available for reliability const payload = JSON.stringify({ sessionId: pageMetadata.sessionId, events: batch, metadata: { url: pageMetadata.url, referrer: pageMetadata.referrer, userAgent: navigator.userAgent, timestamp: Date.now(), cookies: document.cookie, viewport: { width: window.innerWidth, height: window.innerHeight }, connection: navigator.connection ? { effectiveType: navigator.connection.effectiveType, downlink: navigator.connection.downlink } : null } }); const payloadSize = payload.length; stats.totalBytes += payloadSize; stats.networkStats.requestCount++; DEBUG.log('info', 'send', `Flushing ${batch.length} events`, { batchSize: batch.length, payloadSize: DEBUG.formatSize(payloadSize), method: navigator.sendBeacon ? 'sendBeacon' : 'fetch', endpoint: BRUMMER_CONFIG.telemetryEndpoint, eventTypes: batch.reduce((acc, event) => { acc[event.type] = (acc[event.type] || 0) + 1; return acc; }, {}) }); if (navigator.sendBeacon) { const success = navigator.sendBeacon(BRUMMER_CONFIG.telemetryEndpoint, payload); const duration = Date.now() - startTime; stats.lastSendTime = Date.now(); stats.lastSendSuccess = success; if (success) { stats.networkStats.successCount++; stats.networkStats.totalLatency += duration; DEBUG.log('success', 'send', `Beacon sent successfully`, { duration: DEBUG.formatDuration(duration), success: true }); } else { stats.networkStats.failureCount++; stats.errorCount++; DEBUG.log('error', 'send', 'Beacon failed to send', { duration: DEBUG.formatDuration(duration), suggestion: 'Browser may have blocked the beacon or endpoint is unreachable' }); } } else { // Fallback to fetch with detailed error handling fetch(BRUMMER_CONFIG.telemetryEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Brummer-Session': pageMetadata.sessionId, 'X-Brummer-Process': BRUMMER_CONFIG.processName }, body: payload, keepalive: true }) .then(response => { const duration = Date.now() - startTime; stats.lastSendTime = Date.now(); stats.lastSendSuccess = response.ok; stats.networkStats.totalLatency += duration; if (response.ok) { stats.networkStats.successCount++; DEBUG.log('success', 'send', `Fetch completed successfully`, { status: response.status, statusText: response.statusText, duration: DEBUG.formatDuration(duration), headers: Object.fromEntries(response.headers.entries()) }); } else { stats.networkStats.failureCount++; stats.errorCount++; DEBUG.log('error', 'send', `Server returned error: ${response.status}`, { status: response.status, statusText: response.statusText, duration: DEBUG.formatDuration(duration), headers: Object.fromEntries(response.headers.entries()), suggestion: response.status === 404 ? 'Telemetry endpoint not found' : response.status === 403 ? 'Permission denied' : response.status === 500 ? 'Server error' : 'Check server logs' }); } }) .catch(error => { const duration = Date.now() - startTime; stats.lastSendTime = Date.now(); stats.lastSendSuccess = false; stats.networkStats.failureCount++; stats.errorCount++; DEBUG.log('error', 'send', `Network error: ${error.message}`, { error: error.name, duration: DEBUG.formatDuration(duration), suggestion: error.name === 'TypeError' ? 'Network connection failed' : error.name === 'AbortError' ? 'Request was aborted' : 'Check network connectivity and CORS settings' }); }); } // Clear the timer if (batchTimer) { clearTimeout(batchTimer); batchTimer = null; DEBUG.log('info', 'timer', 'Batch timer cleared'); } } // Monitor DOM readiness and timing function monitorDOMTiming() { // Initial page state sendTelemetry({ type: 'page_load', data: { readyState: document.readyState, referrer: pageMetadata.referrer } }); // Monitor DOM readiness changes document.addEventListener('readystatechange', function() { sendTelemetry({ type: 'dom_state', data: { readyState: document.readyState, elapsedTime: Date.now() - pageMetadata.pageLoadTime } }); }); // Monitor page visibility changes document.addEventListener('visibilitychange', function() { sendTelemetry({ type: 'visibility_change', data: { hidden: document.hidden, visibilityState: document.visibilityState } }); }); // Monitor scroll events with debouncing let scrollTimer = null; let lastScrollPosition = { x: 0, y: 0 }; let scrollStartTime = null; let totalScrollDistance = { x: 0, y: 0 }; function handleScroll() { const currentPosition = { x: window.scrollX || window.pageXOffset, y: window.scrollY || window.pageYOffset }; // Track scroll distance if (scrollStartTime) { totalScrollDistance.x += Math.abs(currentPosition.x - lastScrollPosition.x); totalScrollDistance.y += Math.abs(currentPosition.y - lastScrollPosition.y); } else { scrollStartTime = Date.now(); } lastScrollPosition = currentPosition; // Clear existing timer clearTimeout(scrollTimer); // Set new timer for debounced telemetry scrollTimer = setTimeout(() => { const scrollData = { type: 'user_interaction', data: { action: 'scroll', scrollPosition: currentPosition, scrollDistance: totalScrollDistance, scrollDuration: Date.now() - scrollStartTime, viewport: { width: window.innerWidth, height: window.innerHeight }, document: { width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight }, scrollPercentage: { x: (currentPosition.x / (document.documentElement.scrollWidth - window.innerWidth)) * 100 || 0, y: (currentPosition.y / (document.documentElement.scrollHeight - window.innerHeight)) * 100 || 0 }, timestamp: Date.now() } }; sendTelemetry(scrollData); // Reset tracking variables scrollStartTime = null; totalScrollDistance = { x: 0, y: 0 }; }, 150); // Send after 150ms of no scrolling } // Add scroll listener with passive flag for performance window.addEventListener('scroll', handleScroll, { passive: true }); } // Monitor Performance Metrics function monitorPerformance() { if (!BRUMMER_CONFIG.collectPerformanceMetrics || !window.performance) return; // Wait for page load to complete if (document.readyState === 'complete') { collectPerformanceMetrics(); } else { window.addEventListener('load', collectPerformanceMetrics); } // Enhanced Performance Observer if ('PerformanceObserver' in window) { // Create observers for different entry types const observerConfigs = [ { entryTypes: ['longtask'], handler: (entries) => { entries.forEach(entry => { sendTelemetry({ type: 'long_task', data: { duration: entry.duration, startTime: entry.startTime, name: entry.name, attribution: entry.attribution } }); }); } }, { entryTypes: ['paint'], handler: (entries) => { entries.forEach(entry => { sendTelemetry({ type: 'paint_timing', data: { name: entry.name, startTime: entry.startTime, duration: entry.duration } }); }); } }, { entryTypes: ['largest-contentful-paint'], handler: (entries) => { entries.forEach(entry => { sendTelemetry({ type: 'largest_contentful_paint', data: { startTime: entry.startTime, size: entry.size, element: entry.element?.tagName, elementId: entry.element?.id, elementClass: entry.element?.className, url: entry.url } }); }); } }, { entryTypes: ['layout-shift'], handler: (entries) => { entries.forEach(entry => { if (!entry.hadRecentInput) { // Only track shifts not caused by user input sendTelemetry({ type: 'layout_shift', data: { value: entry.value, startTime: entry.startTime, sources: entry.sources?.map(source => ({ node: source.node?.tagName, previousRect: source.previousRect, currentRect: source.currentRect })) } }); } }); } }, { entryTypes: ['first-input'], handler: (entries) => { entries.forEach(entry => { sendTelemetry({ type: 'first_input_delay', data: { delay: entry.processingStart - entry.startTime, duration: entry.duration, startTime: entry.startTime, name: entry.name, target: entry.target?.tagName } }); }); } }, { entryTypes: ['navigation'], handler: (entries) => { entries.forEach(entry => { sendTelemetry({ type: 'navigation_timing', data: { domContentLoaded: entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart, domComplete: entry.domComplete, domInteractive: entry.domInteractive, loadComplete: entry.loadEventEnd - entry.loadEventStart, type: entry.type, redirectCount: entry.redirectCount, transferSize: entry.transferSize, encodedBodySize: entry.encodedBodySize, decodedBodySize: entry.decodedBodySize, serverTiming: entry.serverTiming } }); }); } } ]; // Try to observe each entry type observerConfigs.forEach(config => { try { const observer = new PerformanceObserver(list => { config.handler(list.getEntries()); }); observer.observe({ entryTypes: config.entryTypes }); } catch (e) { // Entry type not supported in this browser DEBUG.log('warning', 'performance', `Performance entry type not supported: ${config.entryTypes.join(', ')}`); } }); // Monitor all supported entry types try { const allTypesObserver = new PerformanceObserver(list => { const entries = list.getEntries(); // Track Web Vitals entries.forEach(entry => { if (entry.entryType === 'measure' && entry.name.startsWith('CLS')) { sendTelemetry({ type: 'web_vital', data: { metric: 'CLS', value: entry.duration, timestamp: Date.now() } }); } }); }); if (PerformanceObserver.supportedEntryTypes) { allTypesObserver.observe({ entryTypes: PerformanceObserver.supportedEntryTypes.filter(type => !['longtask', 'paint', 'largest-contentful-paint', 'layout-shift', 'first-input', 'navigation'].includes(type) ) }); } } catch (e) { // Fallback for older browsers } } } function collectPerformanceMetrics() { const perfData = window.performance.timing; const navigation = window.performance.navigation; // Calculate key metrics const metrics = { // Navigation timing navigationStart: perfData.navigationStart, redirectTime: perfData.redirectEnd - perfData.redirectStart, dnsTime: perfData.domainLookupEnd - perfData.domainLookupStart, connectTime: perfData.connectEnd - perfData.connectStart, requestTime: perfData.responseStart - perfData.requestStart, responseTime: perfData.responseEnd - perfData.responseStart, domProcessingTime: perfData.domComplete - perfData.domLoading, domContentLoadedTime: perfData.domContentLoadedEventEnd - perfData.navigationStart, loadCompleteTime: perfData.loadEventEnd - perfData.navigationStart, // Navigation type navigationType: ['navigate', 'reload', 'back_forward', 'reserved'][navigation.type] || 'unknown', redirectCount: navigation.redirectCount }; // Paint timing (if available) if (window.performance.getEntriesByType) { const paintEntries = window.performance.getEntriesByType('paint'); paintEntries.forEach(entry => { metrics[entry.name.replace('-', '_')] = entry.startTime; }); } sendTelemetry({ type: 'performance_metrics', data: metrics }); } // Monitor Memory Usage function monitorMemory() { if (!BRUMMER_CONFIG.collectMemoryMetrics || !performance.memory) return; // Collect memory stats periodically const collectMemoryStats = () => { sendTelemetry({ type: 'memory_usage', data: { usedJSHeapSize: performance.memory.usedJSHeapSize, totalJSHeapSize: performance.memory.totalJSHeapSize, jsHeapSizeLimit: performance.memory.jsHeapSizeLimit, percentUsed: (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100 } }); }; // Initial collection collectMemoryStats(); // Collect every 10 seconds setInterval(collectMemoryStats, 10000); } // Monitor Console Output function monitorConsole() { if (!BRUMMER_CONFIG.collectConsoleMetrics) return; // Store original console methods globally for debug use if (!window.__brummer_originalConsole) { window.__brummer_originalConsole = { log: console.log.bind(console), info: console.info.bind(console), warn: console.warn.bind(console), error: console.error.bind(console), debug: console.debug.bind(console), groupCollapsed: console.groupCollapsed.bind(console), groupEnd: console.groupEnd.bind(console) }; } // No startup message to keep console clean const originalMethods = {}; const methodsToIntercept = ['log', 'info', 'warn', 'error', 'debug']; methodsToIntercept.forEach(method => { originalMethods[method] = console[method]; console[method] = function(...args) { // Call original method originalMethods[method].apply(console, args); // Skip telemetry for Brummer's own debug messages and telemetry data const message = args.map(arg => String(arg)).join(' '); if (message.includes('BRUMMER:') || message.includes('📊 Data:') || message.includes('brummer.debug') || message.includes('BRUMMER TELEMETRY DEBUG') || message.includes('bufferSize:') || message.includes('bufferStatus:') || message.includes('eventType: "console_output"')) { return; } // Also skip if the first argument is an object with eventType if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) { if (args[0].eventType === 'console_output' || (args[0].bufferSize && args[0].bufferStatus)) { return; } } // Send telemetry try { sendTelemetry({ type: 'console_output', data: { level: method, message: args.map(arg => { if (typeof arg === 'object') { try { return JSON.stringify(arg); } catch (e) { return String(arg); } } return String(arg); }).join(' '), stack: (new Error()).stack } }); } catch (e) { // Fail silently } }; }); // Monitor unhandled errors window.addEventListener('error', function(event) { sendTelemetry({ type: 'javascript_error', data: { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, stack: event.error ? event.error.stack : null } }); }); // Monitor unhandled promise rejections window.addEventListener('unhandledrejection', function(event) { sendTelemetry({ type: 'unhandled_rejection', data: { reason: event.reason ? String(event.reason) : 'Unknown', promise: String(event.promise) } }); }); } // Monitor UI Interactions function monitorInteractions() { if (!BRUMMER_CONFIG.collectInteractionMetrics) return; // Throttling settings const interactionThrottle = 100; // ms const lastInteractionTime = {}; // Helper to build selector path function buildSelectorPath(element) { const path = []; while (element && element !== document.body) { let selector = element.tagName.toLowerCase(); if (element.id) { selector += '#' + element.id; path.unshift(selector); break; // ID is unique, stop here } else { if (element.className && typeof element.className === 'string') { selector += '.' + element.className.trim().split(/\s+/).join('.'); } // Add index if there are siblings with same tag const siblings = element.parentNode ? Array.from(element.parentNode.children) : []; const sameTagSiblings = siblings.filter(sibling => sibling.tagName === element.tagName); if (sameTagSiblings.length > 1) { const index = sameTagSiblings.indexOf(element); selector += `:nth-of-type(${index + 1})`; } path.unshift(selector); } element = element.parentElement; } return path.join(' > '); } // Enhanced click tracking with throttling document.addEventListener('click', function(event) { const now = Date.now(); const eventKey = 'click'; if (lastInteractionTime[eventKey] && now - lastInteractionTime[eventKey] < interactionThrottle) { return; // Throttle rapid clicks } lastInteractionTime[eventKey] = now; const target = event.target; const rect = target.getBoundingClientRect(); sendTelemetry({ type: 'user_interaction', data: { action: 'click', targetSelector: buildSelectorPath(target), targetTag: target.tagName.toLowerCase(), targetId: target.id || null, targetClass: target.className || null, targetText: target.textContent ? target.textContent.substring(0, 100).trim() : '', targetHref: target.href || null, coordinates: { client: { x: event.clientX, y: event.clientY }, page: { x: event.pageX, y: event.pageY }, screen: { x: event.screenX, y: event.screenY }, element: { x: event.clientX - rect.left, y: event.clientY - rect.top, width: rect.width, height: rect.height } }, viewport: { width: window.innerWidth, height: window.innerHeight, scrollX: window.scrollX, scrollY: window.scrollY }, modifiers: { alt: event.altKey, ctrl: event.ctrlKey, meta: event.metaKey, shift: event.shiftKey }, button: event.button, timestamp: event.timeStamp } }); }, true); // Double click tracking document.addEventListener('dblclick', function(event) { const target = event.target; sendTelemetry({ type: 'user_interaction', data: { action: 'double_click', targetSelector: buildSelectorPath(target), targetTag: target.tagName.toLowerCase(), coordinates: { client: { x: event.clientX, y: event.clientY }, page: { x: event.pageX, y: event.pageY } }, timestamp: event.timeStamp } }); }, true); // Context menu (right-click) tracking document.addEventListener('contextmenu', function(event) { const target = event.target; sendTelemetry({ type: 'user_interaction', data: { action: 'context_menu', targetSelector: buildSelectorPath(target), targetTag: target.tagName.toLowerCase(), coordinates: { client: { x: event.clientX, y: event.clientY }, page: { x: event.pageX, y: event.pageY } }, timestamp: event.timeStamp } }); }, true); // Enhanced form tracking document.addEventListener('submit', function(event) { const form = event.target; const formData = new FormData(form); const fields = []; // Collect form field metadata (not values for privacy) for (const element of form.elements) { if (element.name) { fields.push({ name: element.name, type: element.type, tag: element.tagName.toLowerCase(), required: element.required || false, hasValue: !!element.value }); } } sendTelemetry({ type: 'user_interaction', data: { action: 'form_submit', formId: form.id || null, formName: form.name || null, formAction: form.action, formMethod: form.method, formTarget: form.target || null, fieldCount: fields.length, fields: fields, timestamp: Date.now() } }); }, true); // Enhanced input tracking with debouncing let inputTimer = null; const inputBuffer = new Map(); document.addEventListener('input', function(event) { const target = event.target; if (target.tagName !== 'INPUT' && target.tagName !== 'TEXTAREA') return; const key = buildSelectorPath(target); if (!inputBuffer.has(key)) { inputBuffer.set(key, { startTime: Date.now(), changeCount: 0, fieldType: target.type || 'text', fieldName: target.name || null, hasValue: false }); } const bufferEntry = inputBuffer.get(key); bufferEntry.changeCount++; bufferEntry.hasValue = !!target.value; // Debounce input events clearTimeout(inputTimer); inputTimer = setTimeout(() => { // Send all buffered input events inputBuffer.forEach((data, selector) => { sendTelemetry({ type: 'user_interaction', data: { action: 'input_change', targetSelector: selector, fieldType: data.fieldType, fieldName: data.fieldName, changeCount: data.changeCount, duration: Date.now() - data.startTime, hasValue: data.hasValue, timestamp: Date.now() } }); }); inputBuffer.clear(); }, 1000); // Send after 1 second of inactivity }, true); // Focus/blur tracking let focusData = null; document.addEventListener('focus', function(event) { const target = event.target; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') { focusData = { startTime: Date.now(), selector: buildSelectorPath(target), fieldType: target.type || target.tagName.toLowerCase(), fieldName: target.name || null }; } }, true); document.addEventListener('blur', function(event) { if (focusData && (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA' || event.target.tagName === 'SELECT')) { const focusDuration = Date.now() - focusData.startTime; sendTelemetry({ type: 'user_interaction', data: { action: 'field_interaction', targetSelector: focusData.selector, fieldType: focusData.fieldType, fieldName: focusData.fieldName, focusDuration: focusDuration, hasValue: !!event.target.value, timestamp: Date.now() } }); focusData = null; } }, true); // Mouse movement tracking (heavily throttled) let lastMouseMove = 0; const mouseMoveThrottle = 5000; // Only track every 5 seconds document.addEventListener('mousemove', function(event) { const now = Date.now(); if (now - lastMouseMove < mouseMoveThrottle) return; lastMouseMove = now; sendTelemetry({ type: 'user_interaction', data: { action: 'mouse_movement', coordinates: { client: { x: event.clientX, y: event.clientY }, page: { x: event.pageX, y: event.pageY } }, viewport: { width: window.innerWidth, height: window.innerHeight }, timestamp: event.timeStamp } }); }, true); // Keyboard shortcut tracking (only special combinations) document.addEventListener('keydown', function(event) { // Only track if modifier keys are pressed if (event.ctrlKey || event.metaKey || event.altKey) { // Don't track sensitive keys like passwords if (event.key.length === 1 || ['Enter', 'Escape', 'Tab', 'Delete', 'Backspace'].includes(event.key)) { sendTelemetry({ type: 'user_interaction', data: { action: 'keyboard_shortcut', key: event.key, code: event.code, modifiers: { alt: event.altKey, ctrl: event.ctrlKey, meta: event.metaKey, shift: event.shiftKey }, timestamp: event.timeStamp } }); } } }, true); } // Monitor Network Activity (via Resource Timing API) function monitorNetworkActivity() { if (!window.performance || !window.performance.getEntriesByType) return; const checkResources = () => { const resources = window.performance.getEntriesByType('resource'); const lastCheckTime = window.__brummerLastResourceCheck || 0; resources.forEach(resource => { if (resource.startTime > lastCheckTime) { sendTelemetry({ type: 'resource_timing', data: { name: resource.name, initiatorType: resource.initiatorType, duration: resource.duration, transferSize: resource.transferSize || 0, encodedBodySize: resource.encodedBodySize || 0, decodedBodySize: resource.decodedBodySize || 0, startTime: resource.startTime } }); } }); window.__brummerLastResourceCheck = Date.now(); }; // Check resources periodically setInterval(checkResources, 5000); } // Advanced Network Interception (Fetch and XHR) function interceptNetworkRequests() { // Intercept Fetch API const originalFetch = window.fetch; window.fetch = async function(...args) { const [resource, init] = args; const method = init?.method || 'GET'; const startTime = performance.now(); const requestId = Math.random().toString(36).substr(2, 9); // Capture request details const requestData = { type: 'network_request', data: { requestId: requestId, method: method, url: resource.toString(), headers: init?.headers || {}, body: init?.body ? String(init.body).substring(0, 1000) : null, timestamp: Date.now(), initiator: 'fetch' } }; sendTelemetry(requestData); try { const response = await originalFetch.apply(this, args); const duration = performance.now() - startTime; // Clone response to read it const clone = response.clone(); // Capture response details const responseData = { type: 'network_response', data: { requestId: requestId, status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), duration: duration, size: response.headers.get('content-length') || 'unknown', timestamp: Date.now() } }; sendTelemetry(responseData); // Optionally capture response body for JSON responses if (response.headers.get('content-type')?.includes('application/json')) { clone.json().then(body => { sendTelemetry({ type: 'network_response_body', data: { requestId: requestId, body: JSON.stringify(body).substring(0, 5000), bodySize: JSON.stringify(body).length } }); }).catch(() => { // Ignore parsing errors }); } return response; } catch (error) { const duration = performance.now() - startTime; sendTelemetry({ type: 'network_error', data: { requestId: requestId, error: error.message, errorType: error.name, duration: duration, timestamp: Date.now() } }); throw error; } }; // Intercept XMLHttpRequest const XHR = XMLHttpRequest.prototype; const originalOpen = XHR.open; const originalSend = XHR.send; const originalSetRequestHeader = XHR.setRequestHeader; XHR.open = function(method, url) { this._brummer = { method: method, url: url, headers: {}, startTime: null, requestId: Math.random().toString(36).substr(2, 9) }; return originalOpen.apply(this, arguments); }; XHR.setRequestHeader = function(header, value) { if (this._brummer) { this._brummer.headers[header] = value; } return originalSetRequestHeader.apply(this, arguments); }; XHR.send = function(body) { if (this._brummer) { this._brummer.startTime = performance.now(); this._brummer.body = body ? String(body).substring(0, 1000) : null; sendTelemetry({ type: 'network_request', data: { requestId: this._brummer.requestId, method: this._brummer.method, url: this._brummer.url, headers: this._brummer.headers, body: this._brummer.body, timestamp: Date.now(), initiator: 'xhr' } }); // Monitor state changes this.addEventListener('readystatechange', function() { if (this.readyState === 4) { // Request complete const duration = performance.now() - this._brummer.startTime; sendTelemetry({ type: 'network_response', data: { requestId: this._brummer.requestId, status: this.status, statusText: this.statusText, duration: duration, responseSize: this.responseText?.length || 0, responseHeaders: this.getAllResponseHeaders(), timestamp: Date.now() } }); // Capture response body for JSON responses const contentType = this.getResponseHeader('content-type'); if (contentType?.includes('application/json') && this.responseText) { try { const responseBody = JSON.parse(this.responseText); sendTelemetry({ type: 'network_response_body', data: { requestId: this._brummer.requestId, body: JSON.stringify(responseBody).substring(0, 5000), bodySize: this.responseText.length } }); } catch (e) { // Ignore JSON parsing errors } } } }); this.addEventListener('error', function() { const duration = performance.now() - this._brummer.startTime; sendTelemetry({ type: 'network_error', data: { requestId: this._brummer.requestId, error: 'Network request failed', errorType: 'NetworkError', duration: duration, timestamp: Date.now() } }); }); this.addEventListener('abort', function() { const duration = performance.now() - this._brummer.startTime; sendTelemetry({ type: 'network_error', data: { requestId: this._brummer.requestId, error: 'Request aborted', errorType: 'AbortError', duration: duration, timestamp: Date.now() } }); }); } return originalSend.apply(this, arguments); }; } // Monitor Storage Events function monitorStorageEvents() { ['localStorage', 'sessionStorage'].forEach(storageType => { const storage = window[storageType]; if (!storage) return; const originalSetItem = storage.setItem; const originalRemoveItem = storage.removeItem; const originalClear = storage.clear; storage.setItem = function(key, value) { sendTelemetry({ type: 'storage_event', data: { storageType: storageType, action: 'setItem', key: key, valueSize: value?.length || 0, timestamp: Date.now() } }); return originalSetItem.apply(storage, arguments); }; storage.removeItem = function(key) { sendTelemetry({ type: 'storage_event', data: { storageType: storageType, action: 'removeItem', key: key, timestamp: Date.now() } }); return originalRemoveItem.apply(storage, arguments); }; storage.clear = function() { sendTelemetry({ type: 'storage_event', data: { storageType: storageType, action: 'clear', timestamp: Date.now() } }); return originalClear.apply(storage, arguments); }; }); // Monitor storage events from other windows/tabs window.addEventListener('storage', (event) => { sendTelemetry({ type: 'storage_event', data: { storageType: event.storageArea === localStorage ? 'localStorage' : 'sessionStorage', action: 'external_change', key: event.key, oldValue: event.oldValue?.substring(0, 100), newValue: event.newValue?.substring(0, 100), url: event.url, timestamp: Date.now() } }); }); DEBUG.log('info', 'storage', 'Storage monitoring initialized'); } // Monitor DOM Mutations function monitorDOMMutations() { if (!window.MutationObserver) return; const mutationSummary = { added: 0, removed: 0, attributeChanged: 0, textChanged: 0 }; let mutationTimer = null; const MUTATION_BATCH_DELAY = 1000; // Batch mutations for 1 second const sendMutationSummary = () => { if (Object.values(mutationSummary).some(v => v > 0)) { sendTelemetry({ type: 'dom_mutation', data: { summary: {...mutationSummary}, timestamp: Date.now() } }); // Reset counters mutationSummary.added = 0; mutationSummary.removed = 0; mutationSummary.attributeChanged = 0; mutationSummary.textChanged = 0; } }; const mutationObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { switch (mutation.type) { case 'childList': mutationSummary.added += mutation.addedNodes.length; mutationSummary.removed += mutation.removedNodes.length; // Track significant additions (e.g., new sections, scripts) mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node; if (element.tagName === 'SCRIPT' || element.tagName === 'LINK' || element.tagName === 'STYLE') { sendTelemetry({ type: 'dom_resource_added', data: { tagName: element.tagName, src: element.src || element.href || 'inline', timestamp: Date.now() } }); } } }); break; case 'attributes': mutationSummary.attributeChanged++; // Track specific attribute changes if (mutation.target.nodeType === Node.ELEMENT_NODE) { const element = mutation.target; if (mutation.attributeName === 'class' || mutation.attributeName === 'style') { // Track style/class changes on body or major containers if (element === document.body || element.tagName === 'HTML') { sendTelemetry({ type: 'dom_style_change', data: { element: element.tagName, attribute: mutation.attributeName, oldValue: mutation.oldValue, newValue: element.getAttribute(mutation.attributeName), timestamp: Date.now() } }); } } } break; case 'characterData': mutationSummary.textChanged++; break; } }); // Batch mutations clearTimeout(mutationTimer); mutationTimer = setTimeout(sendMutationSummary, MUTATION_BATCH_DELAY); }); // Start observing mutationObserver.observe(document.body, { childList: true, attributes: true, characterData: true, subtree: true, attributeOldValue: true, characterDataOldValue: false // Don't track old text values to save memory }); DEBUG.log('info', 'dom', 'DOM mutation observer started'); } // Capture screenshot function function captureScreenshot() { const statusEl = document.getElementById('__brummer_screenshot_status'); const btnEl = document.getElementById('__brummer_screenshot_btn'); if (!statusEl || !btnEl) return; // Update UI to show capturing btnEl.disabled = true; btnEl.textContent = '⏳ Capturing...'; statusEl.textContent = 'Requesting screenshot...'; statusEl.style.color = '#3b82f6'; // Generate unique request ID const requestId = 'screenshot_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); // Send screenshot request via WebSocket if (wsConnected && websocket && websocket.readyState === WebSocket.OPEN) { // Use WebSocket for screenshot request sendWebSocketCommand('screenshot_request', { requestId: requestId, url: window.location.href, viewport: { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio }, fullPage: true }); // Wait for response with timeout const timeout = setTimeout(() => { btnEl.disabled = false; btnEl.textContent = '📸 Capture Screenshot'; statusEl.textContent = 'Screenshot timeout. Try using browser DevTools (F12).'; statusEl.style.color = '#ef4444'; }, 10000); // Store timeout to clear later window.__brummerScreenshotTimeout = timeout; } else { // Fallback to telemetry-based screenshot request sendTelemetry({ type: 'screenshot_request', data: { requestId: requestId, url: window.location.href, viewport: { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio }, fullPage: true, timestamp: Date.now() } }); // Flush immediately to send the request flushTelemetry(); // Update status setTimeout(() => { btnEl.disabled = false; btnEl.textContent = '📸 Capture Screenshot'; statusEl.textContent = 'Screenshot requested. Check Brummer logs.'; statusEl.style.color = '#22c55e'; }, 2000); } DEBUG.log('info', 'screenshot', `Screenshot requested with ID: ${requestId}`); } // Create floating badge UI function createFloatingBadge() { if (!document.body) return; // Create badge container const badge = document.createElement('div'); badge.id = '__brummer_badge'; badge.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: #1e293b; color: #e2e8f0; padding: 8px 12px; border-radius: 6px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 12px; font-weight: 500; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); z-index: 999999; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; user-select: none; `; // Create icon const icon = document.createElement('span'); icon.innerHTML = '🐝'; icon.style.fontSize = '14px'; // Create text const text = document.createElement('span'); text.textContent = 'Brummer'; // Create status indicator const status = document.createElement('span'); status.id = '__brummer_status'; status.style.cssText = ` width: 8px; height: 8px; background: #22c55e; border-radius: 50%; display: inline-block; `; badge.appendChild(icon); badge.appendChild(text); badge.appendChild(status); // Add hover effect badge.addEventListener('mouseenter', () => { badge.style.transform = 'translateY(-2px)'; badge.style.boxShadow = '0 6px 12px rgba(0, 0, 0, 0.15)'; }); badge.addEventListener('mouseleave', () => { badge.style.transform = 'translateY(0)'; badge.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)'; }); // Toggle debug panel on click let debugPanelVisible = false; badge.addEventListener('click', () => { if (debugPanelVisible) { const panel = document.getElementById('__brummer_debug_panel'); if (panel) panel.remove(); debugPanelVisible = false; } else { createDebugPanel(); debugPanelVisible = true; } }); document.body.appendChild(badge); // Update status indicator based on connection setInterval(() => { const statusEl = document.getElementById('__brummer_status'); if (statusEl) { if (wsConnected || stats.lastPingSuccess) { statusEl.style.background = '#22c55e'; // Green for connected } else { statusEl.style.background = '#ef4444'; // Red for disconnected } } }, 1000); } // Create debug panel (hidden by default) function createDebugPanel() { const panel = document.createElement('div'); panel.id = '__brummer_debug_panel'; panel.style.cssText = ` position: fixed; bottom: 60px; right: 20px; width: 300px; max-height: 400px; background: #1e293b; color: #e2e8f0; border-radius: 8px; box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); z-index: 999998; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 12px; overflow: hidden; display: flex; flex-direction: column; `; // Header const header = document.createElement('div'); header.style.cssText = ` padding: 12px 16px; background: #0f172a; border-bottom: 1px solid #334155; font-weight: 600; display: flex; justify-content: space-between; align-items: center; `; header.innerHTML = ` <span>Brummer Debug Panel</span> <span style="cursor: pointer; font-size: 16px;" onclick="document.getElementById('__brummer_debug_panel').remove()">×</span> `; // Content const content = document.createElement('div'); content.style.cssText = ` padding: 16px; overflow-y: auto; flex: 1; `; // Add stats const updateContent = () => { content.innerHTML = ` <div style="margin-bottom: 12px;"> <strong>Connection:</strong> ${wsConnected ? '🟢 WebSocket' : (stats.lastPingSuccess ? '🟡 HTTP' : '🔴 Offline')} </div> <div style="margin-bottom: 12px;"> <strong>Events Sent:</strong> ${stats.totalEvents} </div> <div style="margin-bottom: 12px;"> <strong>Buffer:</strong> ${telemetryBuffer.length}/${BRUMMER_CONFIG.maxBatchSize} </div> <div style="margin-bottom: 12px;"> <strong>Errors:</strong> ${stats.errorCount} </div> <hr style="border: none; border-top: 1px solid #334155; margin: 12px 0;"> <div style="margin-bottom: 12px;"> <button id="__brummer_screenshot_btn" style=" width: 100%; padding: 8px 12px; background: #3b82f6; color: white; border: none; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; transition: background 0.2s; " onmouseover="this.style.background='#2563eb'" onmouseout="this.style.background='#3b82f6'"> 📸 Capture Screenshot </button> </div> <div id="__brummer_screenshot_status" style="margin-bottom: 12px; text-align: center; font-size: 11px; color: #94a3b8;"></div> <hr style="border: none; border-top: 1px solid #334155; margin: 12px 0;"> <div style="margin-bottom: 8px;"> <strong>Debug Commands:</strong> </div> <code style="display: block; background: #0f172a; padding: 8px; border-radius: 4px; font-size: 11px;"> __brummer.debug.status()<br> __brummer.debug.timeline()<br> __brummer.debug.flush()<br> __brummer.debug.clear() </code> `; // Add click handler for screenshot button const screenshotBtn = document.getElementById('__brummer_screenshot_btn'); if (screenshotBtn) { screenshotBtn.onclick = captureScreenshot; } }; updateContent(); panel.appendChild(header); panel.appendChild(content); document.body.appendChild(panel); // Update stats every second const updateInterval = setInterval(() => { if (document.getElementById('__brummer_debug_panel')) { updateContent(); } else { clearInterval(updateInterval); } }, 1000); } // Initialize monitoring function initialize() { // No startup banner to keep console clean // Connect WebSocket if enabled if (BRUMMER_CONFIG.useWebSocket) { setTimeout(() => { connectWebSocket(); }, 500); } // Ping endpoint to test connectivity setTimeout(() => { if (BRUMMER_CONFIG.useWebSocket) { // WebSocket ping will happen after connection is established DEBUG.log('info', 'network', 'WebSocket ping will be performed after connection'); } else { pingEndpoint(); } }, 1000); // Send initial telemetry sendTelemetry({ type: 'monitor_initialized', data: { config: BRUMMER_CONFIG, pageMetadata: pageMetadata, capabilities: { sendBeacon: !!navigator.sendBeacon, performanceObserver: 'PerformanceObserver' in window, memoryAPI: !!performance.memory, connectionAPI: !!navigator.connection, storageAPI: typeof(Storage) !== "undefined" }, environment: { language: navigator.language, platform: navigator.platform, cookieEnabled: navigator.cookieEnabled, onLine: navigator.onLine, viewport: { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio } } } }); // Create floating badge UI setTimeout(() => { createFloatingBadge(); }, 100); // Start monitors monitorDOMTiming(); monitorPerformance(); monitorMemory(); monitorConsole(); monitorInteractions(); monitorNetworkActivity(); interceptNetworkRequests(); monitorStorageEvents(); monitorDOMMutations(); // Send a test event to verify the system is working sendTelemetry({ type: 'brummer_init', data: { message: 'Brummer monitoring initialized successfully', url: window.location.href, timestamp: Date.now() } }); // Force immediate flush for testing setTimeout(() => { flushTelemetry(); }, 1000); // Flush telemetry on page unload window.addEventListener('beforeunload', () => { flushTelemetry(); }); window.addEventListener('pagehide', () => { flushTelemetry(); }); // No automatic status display to keep console clean } // Start monitoring when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/standardbeagle/brummer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server