Files
dss/admin-ui/js/core/browser-logger.js
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
2025-12-09 18:45:48 -03:00

757 lines
23 KiB
JavaScript

/**
* Browser Logger - Captures all browser-side activity
*
* Records:
* - Console logs (log, warn, error, info, debug)
* - Uncaught errors and exceptions
* - Network requests (via fetch/XMLHttpRequest)
* - Performance metrics
* - Memory usage
* - User interactions
*
* Can be exported to server or retrieved from sessionStorage
*/
class BrowserLogger {
constructor(maxEntries = 1000) {
this.maxEntries = maxEntries;
this.entries = [];
this.startTime = Date.now();
this.sessionId = this.generateSessionId();
this.lastSyncedIndex = 0; // Track which logs have been sent to server
this.autoSyncInterval = 30000; // 30 seconds
this.apiEndpoint = '/api/browser-logs';
this.lastUrl = window.location.href; // Track URL for navigation detection
// Storage key for persistence across page reloads
this.storageKey = `dss-browser-logs-${this.sessionId}`;
// Core Web Vitals tracking
this.lcp = null; // Largest Contentful Paint
this.cls = 0; // Cumulative Layout Shift
this.axeLoadingPromise = null; // Promise for axe-core script loading
// Try to load existing logs
this.loadFromStorage();
// Start capturing
this.captureConsole();
this.captureErrors();
this.captureNetworkActivity();
this.capturePerformance();
this.captureMemory();
this.captureWebVitals();
// Initialize Shadow State capture
this.setupSnapshotCapture();
// Start auto-sync to server
this.startAutoSync();
}
/**
* Generate unique session ID
*/
generateSessionId() {
return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Add log entry
*/
log(level, category, message, data = {}) {
const entry = {
timestamp: Date.now(),
relativeTime: Date.now() - this.startTime,
level,
category,
message,
data,
url: window.location.href,
userAgent: navigator.userAgent,
};
this.entries.push(entry);
// Keep size manageable
if (this.entries.length > this.maxEntries) {
this.entries.shift();
}
// Persist to storage
this.saveToStorage();
return entry;
}
/**
* Capture console methods
*/
captureConsole() {
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
const originalInfo = console.info;
const originalDebug = console.debug;
console.log = (...args) => {
this.log('log', 'console', args.join(' '), { args });
originalLog.apply(console, args);
};
console.error = (...args) => {
this.log('error', 'console', args.join(' '), { args });
originalError.apply(console, args);
};
console.warn = (...args) => {
this.log('warn', 'console', args.join(' '), { args });
originalWarn.apply(console, args);
};
console.info = (...args) => {
this.log('info', 'console', args.join(' '), { args });
originalInfo.apply(console, args);
};
console.debug = (...args) => {
this.log('debug', 'console', args.join(' '), { args });
originalDebug.apply(console, args);
};
}
/**
* Capture uncaught errors
*/
captureErrors() {
// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.log('error', 'unhandledRejection', event.reason?.message || String(event.reason), {
reason: event.reason,
stack: event.reason?.stack,
});
});
// Global error handler
window.addEventListener('error', (event) => {
this.log('error', 'uncaughtError', event.message, {
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
});
});
}
/**
* Capture network activity using PerformanceObserver
* This is non-invasive and doesn't monkey-patch fetch or XMLHttpRequest
*/
captureNetworkActivity() {
// Use PerformanceObserver to monitor network requests (modern approach)
if ('PerformanceObserver' in window) {
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// resource entries are generated automatically for fetch/xhr
if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
const method = entry.name.split('?')[0]; // Extract method from name if available
this.log('network', entry.initiatorType, `${entry.initiatorType.toUpperCase()} ${entry.name}`, {
url: entry.name,
initiatorType: entry.initiatorType,
duration: entry.duration,
transferSize: entry.transferSize,
encodedBodySize: entry.encodedBodySize,
decodedBodySize: entry.decodedBodySize,
});
}
}
});
// Observe resource entries (includes fetch/xhr)
observer.observe({ entryTypes: ['resource'] });
} catch (e) {
// PerformanceObserver might not support resource entries in some browsers
// Gracefully degrade - network logging simply won't work
}
}
}
/**
* Capture performance metrics
*/
capturePerformance() {
// Wait for page load
window.addEventListener('load', () => {
setTimeout(() => {
try {
const perfData = window.performance.getEntriesByType('navigation')[0];
if (perfData) {
this.log('metric', 'performance', 'Page load completed', {
domContentLoaded: perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart,
loadComplete: perfData.loadEventEnd - perfData.loadEventStart,
totalTime: perfData.loadEventEnd - perfData.fetchStart,
dnsLookup: perfData.domainLookupEnd - perfData.domainLookupStart,
tcpConnection: perfData.connectEnd - perfData.connectStart,
requestTime: perfData.responseStart - perfData.requestStart,
responseTime: perfData.responseEnd - perfData.responseStart,
renderTime: perfData.domInteractive - perfData.domLoading,
});
}
} catch (e) {
// Performance API might not be available
}
}, 0);
});
// Monitor long tasks
if ('PerformanceObserver' in window) {
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
// Log tasks that take >50ms
this.log('metric', 'longTask', 'Long task detected', {
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
});
}
}
});
observer.observe({ entryTypes: ['longtask'] });
} catch (e) {
// Long task API might not be available
}
}
}
/**
* Capture memory usage
*/
captureMemory() {
if ('memory' in performance) {
// Check memory every 10 seconds
setInterval(() => {
const memory = performance.memory;
const usagePercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
if (usagePercent > 80) {
this.log('warn', 'memory', 'High memory usage detected', {
usedJSHeapSize: memory.usedJSHeapSize,
jsHeapSizeLimit: memory.jsHeapSizeLimit,
usagePercent: usagePercent.toFixed(2),
});
}
}, 10000);
}
}
/**
* Capture Core Web Vitals (LCP, CLS) using PerformanceObserver
* These observers run in the background to collect metrics as they occur.
*/
captureWebVitals() {
try {
// Capture Largest Contentful Paint (LCP)
const lcpObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
if (entries.length > 0) {
// The last entry is the most recent LCP candidate
this.lcp = entries[entries.length - 1].startTime;
}
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
// Capture Cumulative Layout Shift (CLS)
const clsObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// Only count shifts that were not caused by recent user input.
if (!entry.hadRecentInput) {
this.cls += entry.value;
}
}
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
} catch (e) {
this.log('warn', 'performance', 'Could not initialize Web Vitals observers.', { error: e.message });
}
}
/**
* Get Core Web Vitals and other key performance metrics.
* Retrieves metrics collected by observers or from the Performance API.
* @returns {object} An object containing the collected metrics.
*/
getCoreWebVitals() {
try {
const navEntry = window.performance.getEntriesByType('navigation')[0];
const paintEntries = window.performance.getEntriesByType('paint');
const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
const ttfb = navEntry ? navEntry.responseStart - navEntry.requestStart : null;
return {
ttfb: ttfb,
fcp: fcpEntry ? fcpEntry.startTime : null,
lcp: this.lcp,
cls: this.cls,
};
} catch (e) {
return { error: 'Failed to retrieve Web Vitals.' };
}
}
/**
* Dynamically injects and runs an axe-core accessibility audit.
* @returns {Promise<object|null>} A promise that resolves with the axe audit results.
*/
async runAxeAudit() {
// Check if axe is already available
if (typeof window.axe === 'undefined') {
// If not, and we are not already loading it, inject it
if (!this.axeLoadingPromise) {
this.axeLoadingPromise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.8.4/axe.min.js';
script.onload = () => {
this.log('info', 'accessibility', 'axe-core loaded successfully.');
resolve();
};
script.onerror = () => {
this.log('error', 'accessibility', 'Failed to load axe-core script.');
this.axeLoadingPromise = null; // Allow retry
reject(new Error('Failed to load axe-core.'));
};
document.head.appendChild(script);
});
}
await this.axeLoadingPromise;
}
try {
// Configure axe to run on the entire document
const results = await window.axe.run(document.body);
this.log('metric', 'accessibility', 'Accessibility audit completed.', {
violations: results.violations.length,
passes: results.passes.length,
incomplete: results.incomplete.length,
results, // Store full results
});
return results;
} catch (error) {
this.log('error', 'accessibility', 'Error running axe audit.', { error: error.message });
return null;
}
}
/**
* Captures a comprehensive snapshot including DOM, accessibility, and performance data.
* @returns {Promise<void>}
*/
async captureAccessibilitySnapshot() {
const domSnapshot = await this.captureDOMSnapshot();
const accessibility = await this.runAxeAudit();
const performance = this.getCoreWebVitals();
this.log('metric', 'accessibilitySnapshot', 'Full accessibility snapshot captured.', {
snapshot: domSnapshot,
accessibility,
performance,
});
return { snapshot: domSnapshot, accessibility, performance };
}
/**
* Save logs to sessionStorage
*/
saveToStorage() {
try {
const data = {
sessionId: this.sessionId,
entries: this.entries,
savedAt: Date.now(),
};
sessionStorage.setItem(this.storageKey, JSON.stringify(data));
} catch (e) {
// Storage might be full or unavailable
}
}
/**
* Load logs from sessionStorage
*/
loadFromStorage() {
try {
const data = sessionStorage.getItem(this.storageKey);
if (data) {
const parsed = JSON.parse(data);
this.entries = parsed.entries || [];
}
} catch (e) {
// Storage might be unavailable
}
}
/**
* Get all logs
*/
getLogs(options = {}) {
let entries = [...this.entries];
// Filter by level
if (options.level) {
entries = entries.filter(e => e.level === options.level);
}
// Filter by category
if (options.category) {
entries = entries.filter(e => e.category === options.category);
}
// Filter by time range
if (options.minTime) {
entries = entries.filter(e => e.timestamp >= options.minTime);
}
if (options.maxTime) {
entries = entries.filter(e => e.timestamp <= options.maxTime);
}
// Search in message
if (options.search) {
const searchLower = options.search.toLowerCase();
entries = entries.filter(e =>
e.message.toLowerCase().includes(searchLower) ||
JSON.stringify(e.data).toLowerCase().includes(searchLower)
);
}
// Limit results
const limit = options.limit || 100;
if (options.reverse) {
entries.reverse();
}
return entries.slice(-limit);
}
/**
* Get errors only
*/
getErrors() {
return this.getLogs({ level: 'error', limit: 50, reverse: true });
}
/**
* Get network requests
*/
getNetworkRequests() {
return this.getLogs({ category: 'fetch', limit: 100, reverse: true });
}
/**
* Get metrics
*/
getMetrics() {
return this.getLogs({ category: 'metric', limit: 100, reverse: true });
}
/**
* Get diagnostic summary
*/
getDiagnostic() {
return {
sessionId: this.sessionId,
uptime: Date.now() - this.startTime,
totalLogs: this.entries.length,
errorCount: this.entries.filter(e => e.level === 'error').length,
warnCount: this.entries.filter(e => e.level === 'warn').length,
networkRequests: this.entries.filter(e => e.category === 'fetch').length,
memory: performance.memory ? {
usedJSHeapSize: performance.memory.usedJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
usagePercent: ((performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100).toFixed(2),
} : null,
url: window.location.href,
userAgent: navigator.userAgent,
recentErrors: this.getErrors().slice(0, 5),
recentNetworkRequests: this.getNetworkRequests().slice(0, 5),
};
}
/**
* Export logs as JSON
*/
exportJSON() {
return {
sessionId: this.sessionId,
exportedAt: new Date().toISOString(),
logs: this.entries,
diagnostic: this.getDiagnostic(),
};
}
/**
* Print formatted logs to console
*/
printFormatted(options = {}) {
const logs = this.getLogs(options);
console.group(`📋 Browser Logs (${logs.length} entries)`);
console.table(logs.map(e => ({
Time: new Date(e.timestamp).toLocaleTimeString(),
Level: e.level.toUpperCase(),
Category: e.category,
Message: e.message,
})));
console.groupEnd();
}
/**
* Clear logs
*/
clear() {
this.entries = [];
this.lastSyncedIndex = 0;
this.saveToStorage();
}
/**
* Start auto-sync to server
*/
startAutoSync() {
// Sync immediately on startup (after a delay to let the page load)
setTimeout(() => this.syncToServer(), 5000);
// Then sync every 30 seconds
this.syncTimer = setInterval(() => this.syncToServer(), this.autoSyncInterval);
// Sync before page unload
window.addEventListener('beforeunload', () => this.syncToServer());
}
/**
* Sync logs to server
*/
async syncToServer() {
// Only sync if there are new logs
if (this.lastSyncedIndex >= this.entries.length) {
return;
}
try {
const data = this.exportJSON();
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (response.ok) {
this.lastSyncedIndex = this.entries.length;
console.debug(`[BrowserLogger] Synced ${this.entries.length} logs to server`);
} else {
console.warn(`[BrowserLogger] Failed to sync logs: ${response.statusText}`);
}
} catch (error) {
console.warn('[BrowserLogger] Failed to sync logs:', error.message);
}
}
/**
* Stop auto-sync
*/
stopAutoSync() {
if (this.syncTimer) {
clearInterval(this.syncTimer);
this.syncTimer = null;
}
}
/**
* Capture DOM Snapshot (Shadow State)
* Returns the current state of the DOM and viewport for remote debugging.
* Can optionally include accessibility and performance data.
* @param {object} [options={}] - Options for the snapshot.
* @param {boolean} [options.includeAccessibility=false] - Whether to run an axe audit.
* @param {boolean} [options.includePerformance=false] - Whether to include Core Web Vitals.
* @returns {Promise<object>} A promise that resolves with the snapshot data.
*/
async captureDOMSnapshot(options = {}) {
const snapshot = {
timestamp: Date.now(),
url: window.location.href,
html: document.documentElement.outerHTML,
viewport: {
width: window.innerWidth,
height: window.innerHeight,
devicePixelRatio: window.devicePixelRatio,
},
title: document.title,
};
if (options.includeAccessibility) {
snapshot.accessibility = await this.runAxeAudit();
}
if (options.includePerformance) {
snapshot.performance = this.getCoreWebVitals();
}
return snapshot;
}
/**
* Setup Shadow State Capture
* Monitors navigation and errors to create state checkpoints.
*/
setupSnapshotCapture() {
// Helper to capture state and log it.
const handleSnapshot = async (trigger, details) => {
try {
const snapshot = await this.captureDOMSnapshot();
this.log(details.level || 'info', 'snapshot', `State Capture (${trigger})`, {
trigger,
details,
snapshot,
});
// If it was a critical error, attempt to flush logs immediately.
if (details.level === 'error') {
this.flushViaBeacon();
}
} catch (e) {
this.log('error', 'snapshot', 'Failed to capture snapshot.', { error: e.message });
}
};
// 1. Capture on Navigation (Periodic check for SPA support)
setInterval(async () => {
const currentUrl = window.location.href;
if (currentUrl !== this.lastUrl) {
const previousUrl = this.lastUrl;
this.lastUrl = currentUrl;
await handleSnapshot('navigation', { from: previousUrl, to: currentUrl });
}
}, 1000);
// 2. Capture on Critical Errors
window.addEventListener('error', (event) => {
handleSnapshot('uncaughtError', {
level: 'error',
error: {
message: event.message,
filename: event.filename,
lineno: event.lineno,
},
});
});
window.addEventListener('unhandledrejection', (event) => {
handleSnapshot('unhandledRejection', {
level: 'error',
error: {
reason: event.reason?.message || String(event.reason),
},
});
});
}
/**
* Flush logs via Beacon API
* Used for critical events where fetch might be cancelled (e.g. page unload/crash)
*/
flushViaBeacon() {
if (!navigator.sendBeacon) return;
// Save current state first
this.saveToStorage();
// Prepare payload
const data = this.exportJSON();
// Create Blob for proper Content-Type
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
// Send beacon
const success = navigator.sendBeacon(this.apiEndpoint, blob);
if (success) {
this.lastSyncedIndex = this.entries.length;
console.debug('[BrowserLogger] Critical logs flushed via Beacon');
}
}
}
// Create global instance
const dssLogger = new BrowserLogger();
// Expose to window ONLY in development mode
// This is for debugging purposes only. Production should not expose this.
if (typeof window !== 'undefined' && (
(typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development') ||
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1'
)) {
// Only expose debugging interface with warning
window.__DSS_BROWSER_LOGS = {
all: () => dssLogger.getLogs({ limit: 1000 }),
errors: () => dssLogger.getErrors(),
network: () => dssLogger.getNetworkRequests(),
metrics: () => dssLogger.getMetrics(),
diagnostic: () => dssLogger.getDiagnostic(),
export: () => dssLogger.exportJSON(),
print: (options) => dssLogger.printFormatted(options),
clear: () => dssLogger.clear(),
// Accessibility and performance auditing
audit: () => dssLogger.captureAccessibilitySnapshot(),
vitals: () => dssLogger.getCoreWebVitals(),
axe: () => dssLogger.runAxeAudit(),
// Auto-sync controls
sync: () => dssLogger.syncToServer(),
stopSync: () => dssLogger.stopAutoSync(),
startSync: () => dssLogger.startAutoSync(),
// Quick helpers
help: () => {
console.log('%c📋 DSS Browser Logger Commands', 'font-weight: bold; font-size: 14px; color: #4CAF50');
console.log('%c __DSS_BROWSER_LOGS.errors()', 'color: #FF5252', '- Show all errors');
console.log('%c __DSS_BROWSER_LOGS.diagnostic()', 'color: #2196F3', '- System diagnostic');
console.log('%c __DSS_BROWSER_LOGS.all()', 'color: #666', '- All captured logs');
console.log('%c __DSS_BROWSER_LOGS.network()', 'color: #9C27B0', '- Network requests');
console.log('%c __DSS_BROWSER_LOGS.print()', 'color: #FF9800', '- Print formatted table');
console.log('%c __DSS_BROWSER_LOGS.audit()', 'color: #673AB7', '- Run full accessibility audit');
console.log('%c __DSS_BROWSER_LOGS.vitals()', 'color: #009688', '- Get Core Web Vitals (LCP, CLS, FCP, TTFB)');
console.log('%c __DSS_BROWSER_LOGS.axe()', 'color: #E91E63', '- Run axe-core accessibility scan');
console.log('%c __DSS_BROWSER_LOGS.export()', 'color: #00BCD4', '- Export all data (copy this!)');
console.log('%c __DSS_BROWSER_LOGS.clear()', 'color: #F44336', '- Clear all logs');
console.log('%c __DSS_BROWSER_LOGS.share()', 'color: #4CAF50', '- Generate shareable JSON');
console.log('%c __DSS_BROWSER_LOGS.sync()', 'color: #2196F3', '- Sync logs to server now');
console.log('%c __DSS_BROWSER_LOGS.stopSync()', 'color: #FF9800', '- Stop auto-sync');
console.log('%c __DSS_BROWSER_LOGS.startSync()', 'color: #4CAF50', '- Start auto-sync (30s)');
},
// Generate shareable JSON for debugging with Claude
share: () => {
const data = dssLogger.exportJSON();
const json = JSON.stringify(data, null, 2);
console.log('%c📤 Copy this and share with Claude:', 'font-weight: bold; color: #4CAF50');
console.log(json);
return data;
}
};
console.info('%c🔍 DSS Browser Logger Active', 'color: #4CAF50; font-weight: bold;');
console.info('%c📡 Auto-sync enabled - logs sent to server every 30s', 'color: #2196F3; font-style: italic;');
console.info('%cType: %c__DSS_BROWSER_LOGS.help()%c for commands', 'color: #666', 'color: #2196F3; font-family: monospace', 'color: #666');
}
export default dssLogger;