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
757 lines
23 KiB
JavaScript
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;
|