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
This commit is contained in:
756
admin-ui/js/core/browser-logger.js
Normal file
756
admin-ui/js/core/browser-logger.js
Normal file
@@ -0,0 +1,756 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user