diff --git a/app/components/@settings/core/AvatarDropdown.tsx b/app/components/@settings/core/AvatarDropdown.tsx index 3f5334fb25..9eb7a34a84 100644 --- a/app/components/@settings/core/AvatarDropdown.tsx +++ b/app/components/@settings/core/AvatarDropdown.tsx @@ -130,6 +130,29 @@ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => { Report Bug + { + try { + const { downloadDebugLog } = await import('~/utils/debugLogger'); + await downloadDebugLog(); + } catch (error) { + console.error('Failed to download debug log:', error); + } + }} + > +
+ Download Debug Log + + } - {/* Bug Report Button */} + {/* Debug Tools */} {shouldShowButtons && (
+
+
)}
diff --git a/app/root.tsx b/app/root.tsx index a7ccb285dd..9b8c692852 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -93,6 +93,24 @@ export default function App() { userAgent: navigator.userAgent, timestamp: new Date().toISOString(), }); + + // Initialize debug logging with improved error handling + import('./utils/debugLogger') + .then(({ debugLogger }) => { + /* + * The debug logger initializes itself and starts disabled by default + * It will only start capturing when enableDebugMode() is called + */ + const status = debugLogger.getStatus(); + logStore.logSystem('Debug logging ready', { + initialized: status.initialized, + capturing: status.capturing, + enabled: status.enabled, + }); + }) + .catch((error) => { + logStore.logError('Failed to initialize debug logging', error); + }); }, []); return ( diff --git a/app/routes/api.git-info.ts b/app/routes/api.git-info.ts new file mode 100644 index 0000000000..f90d02e2b5 --- /dev/null +++ b/app/routes/api.git-info.ts @@ -0,0 +1,69 @@ +import { json } from '@remix-run/cloudflare'; +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; + +export async function loader() { + try { + // Check if we're in a git repository + if (!existsSync('.git')) { + return json({ + branch: 'unknown', + commit: 'unknown', + isDirty: false, + }); + } + + // Get current branch + const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim(); + + // Get current commit hash + const commit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim(); + + // Check if working directory is dirty + const statusOutput = execSync('git status --porcelain', { encoding: 'utf8' }); + const isDirty = statusOutput.trim().length > 0; + + // Get remote URL + let remoteUrl: string | undefined; + + try { + remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim(); + } catch { + // No remote origin, leave as undefined + } + + // Get last commit info + let lastCommit: { message: string; date: string; author: string } | undefined; + + try { + const commitInfo = execSync('git log -1 --pretty=format:"%s|%ci|%an"', { encoding: 'utf8' }).trim(); + const [message, date, author] = commitInfo.split('|'); + lastCommit = { + message: message || 'unknown', + date: date || 'unknown', + author: author || 'unknown', + }; + } catch { + // Could not get commit info + } + + return json({ + branch, + commit, + isDirty, + remoteUrl, + lastCommit, + }); + } catch (error) { + console.error('Error fetching git info:', error); + return json( + { + branch: 'error', + commit: 'error', + isDirty: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 }, + ); + } +} diff --git a/app/utils/debugLogger.ts b/app/utils/debugLogger.ts new file mode 100644 index 0000000000..b47829121d --- /dev/null +++ b/app/utils/debugLogger.ts @@ -0,0 +1,1284 @@ +import { isMac, isWindows, isLinux } from './os'; +import { isMobile } from './mobile'; +import { PROVIDER_LIST, DEFAULT_MODEL } from './constants'; +import { logger } from './logger'; + +// Lazy import to avoid circular dependencies +let logStore: any = null; +const getLogStore = () => { + if (!logStore && typeof window !== 'undefined') { + try { + // Import and set the logStore on first access + import('~/lib/stores/logs') + .then(({ logStore: store }) => { + logStore = store; + }) + .catch(() => { + // Ignore import errors + }); + } catch { + // Ignore errors + } + } + + return logStore; +}; + +// Configuration interface for debug logger +export interface DebugLoggerConfig { + enabled: boolean; + maxEntries: number; + captureConsole: boolean; + captureNetwork: boolean; + captureErrors: boolean; + debounceTerminal: number; // ms +} + +// Circular buffer implementation for memory efficiency +class CircularBuffer { + private _buffer: (T | undefined)[]; + private _head = 0; + private _tail = 0; + private _size = 0; + + constructor(private _capacity: number) { + this._buffer = new Array(_capacity); + } + + push(item: T): void { + this._buffer[this._tail] = item; + this._tail = (this._tail + 1) % this._capacity; + + if (this._size < this._capacity) { + this._size++; + } else { + this._head = (this._head + 1) % this._capacity; + } + } + + toArray(): T[] { + const result: T[] = []; + let current = this._head; + + for (let i = 0; i < this._size; i++) { + const item = this._buffer[current]; + + if (item !== undefined) { + result.push(item); + } + + current = (current + 1) % this._capacity; + } + + return result; + } + + clear(): void { + this._buffer = new Array(this._capacity); + this._head = 0; + this._tail = 0; + this._size = 0; + } + + getSize(): number { + return this._size; + } +} + +export interface DebugLogData { + timestamp: string; + sessionId: string; + systemInfo: SystemInfo; + appInfo: AppInfo; + logs: LogEntry[]; + errors: ErrorEntry[]; + networkRequests: NetworkEntry[]; + performance: PerformanceEntry; + state: StateEntry; + userActions: UserActionEntry[]; + terminalLogs: TerminalEntry[]; +} + +export interface SystemInfo { + platform: string; + userAgent: string; + screenResolution: string; + viewportSize: string; + isMobile: boolean; + timezone: string; + language: string; + cookiesEnabled: boolean; + localStorageEnabled: boolean; + sessionStorageEnabled: boolean; +} + +export interface AppInfo { + version: string; + buildTime: string; + currentModel: string; + currentProvider: string; + projectType: string; + workbenchView: string; + hasActivePreview: boolean; + unsavedFiles: number; + workbenchState?: { + currentView: string; + showWorkbench: boolean; + showTerminal: boolean; + artifactsCount: number; + filesCount: number; + unsavedFiles: number; + hasActivePreview: boolean; + }; + gitInfo?: { + branch: string; + commit: string; + isDirty: boolean; + remoteUrl?: string; + lastCommit?: { + message: string; + date: string; + author: string; + }; + }; +} + +export interface LogEntry { + timestamp: string; + level: 'trace' | 'debug' | 'info' | 'warn' | 'error'; + scope?: string; + message: string; + data?: any; +} + +export interface ErrorEntry { + timestamp: string; + type: 'javascript' | 'react' | 'terminal' | 'network' | 'unknown'; + message: string; + stack?: string; + url?: string; + line?: number; + column?: number; + userAgent?: string; + context?: any; +} + +export interface NetworkEntry { + timestamp: string; + method: string; + url: string; + status?: number; + duration?: number; + requestSize?: number; + responseSize?: number; + error?: string; +} + +export interface PerformanceEntry { + navigationStart: number; + loadTime: number; + domContentLoaded: number; + firstPaint?: number; + firstContentfulPaint?: number; + memoryUsage?: { + used: number; + total: number; + limit: number; + }; + timing: any; // Using any instead of deprecated PerformanceTiming +} + +export interface StateEntry { + currentView: string; + showWorkbench: boolean; + showTerminal: boolean; + artifactsCount: number; + filesCount: number; + alerts: Array<{ + type: string; + title: string; + source?: string; + }>; +} + +export interface UserActionEntry { + timestamp: string; + action: string; + target?: string; + data?: any; +} + +export interface TerminalEntry { + timestamp: string; + type: 'input' | 'output' | 'error'; + content: string; + command?: string; +} + +class DebugLogger { + private _logs: CircularBuffer; + private _errors: CircularBuffer; + private _networkRequests: CircularBuffer; + private _userActions: CircularBuffer; + private _terminalLogs: CircularBuffer; + private _config: DebugLoggerConfig; + private _isCapturing = false; + private _isInitialized = false; + + // Store original functions + private _originalConsoleLog: typeof console.log; + private _originalConsoleError: typeof console.error; + private _originalConsoleWarn: typeof console.warn; + private _originalFetch: typeof window.fetch | null = null; + + // Store bound event handlers for proper cleanup + private _boundErrorHandler: (event: ErrorEvent) => void; + private _boundRejectionHandler: (event: PromiseRejectionEvent) => void; + private _boundUnloadHandler: () => void; + + // Debouncing for terminal logs + private _terminalLogQueue: TerminalEntry[] = []; + private _terminalLogTimer: NodeJS.Timeout | null = null; + + // Helper for JSON replacer with seen tracking + private _seenObjects = new WeakSet(); + + constructor(config: Partial = {}) { + // Default configuration + this._config = { + enabled: false, // Start disabled for performance + maxEntries: 1000, + captureConsole: true, + captureNetwork: true, + captureErrors: true, + debounceTerminal: 100, + ...config, + }; + + // Initialize circular buffers + this._logs = new CircularBuffer(this._config.maxEntries); + this._errors = new CircularBuffer(this._config.maxEntries); + this._networkRequests = new CircularBuffer(this._config.maxEntries); + this._userActions = new CircularBuffer(this._config.maxEntries); + this._terminalLogs = new CircularBuffer(this._config.maxEntries); + + // Store original functions + this._originalConsoleLog = console.log; + this._originalConsoleError = console.error; + this._originalConsoleWarn = console.warn; + + // Bind event handlers once to prevent memory leaks + this._boundErrorHandler = this._handleError.bind(this); + this._boundRejectionHandler = this._handleUnhandledRejection.bind(this); + this._boundUnloadHandler = this._cleanup.bind(this); + + // Setup cleanup on page unload + if (typeof window !== 'undefined') { + window.addEventListener('beforeunload', this._boundUnloadHandler); + } + } + + // Initialize the debug logger (lazy initialization for performance) + initialize(): void { + if (this._isInitialized) { + return; + } + + try { + // Only initialize if we're in a browser environment + if (typeof window === 'undefined') { + return; + } + + this._isInitialized = true; + + // Start capturing if enabled + if (this._config.enabled) { + this.startCapture(); + } + + logger.info('Debug logger initialized'); + } catch (error) { + logger.error('Failed to initialize debug logger:', error); + } + } + + startCapture(): void { + if (this._isCapturing) { + return; + } + + try { + this._isCapturing = true; + this._config.enabled = true; + + if (this._config.captureConsole) { + this._interceptConsole(); + } + + if (this._config.captureErrors) { + this._interceptErrors(); + } + + if (this._config.captureNetwork) { + this._interceptNetwork(); + } + + logger.info('Debug logging started'); + } catch (error) { + logger.error('Failed to start debug capture:', error); + this._isCapturing = false; + } + } + + stopCapture(): void { + if (!this._isCapturing) { + return; + } + + try { + this._isCapturing = false; + this._config.enabled = false; + + this._restoreConsole(); + this._restoreErrors(); + this._restoreNetwork(); + + // Clear terminal log timer + if (this._terminalLogTimer) { + clearTimeout(this._terminalLogTimer); + this._terminalLogTimer = null; + this._flushTerminalLogs(); + } + + logger.info('Debug logging stopped'); + } catch (error) { + logger.error('Failed to stop debug capture:', error); + } + } + + // Public method to enable debug logging on demand + enableDebugMode(): void { + this._config.enabled = true; + + if (!this._isInitialized) { + this.initialize(); + } else if (!this._isCapturing) { + this.startCapture(); + } + } + + // Public method to disable debug logging + disableDebugMode(): void { + this.stopCapture(); + } + + // Get current status + getStatus(): { initialized: boolean; capturing: boolean; enabled: boolean } { + return { + initialized: this._isInitialized, + capturing: this._isCapturing, + enabled: this._config.enabled, + }; + } + + // Update configuration + updateConfig(newConfig: Partial): void { + const wasCapturing = this._isCapturing; + + if (wasCapturing) { + this.stopCapture(); + } + + this._config = { ...this._config, ...newConfig }; + + // Recreate buffers if maxEntries changed + if (newConfig.maxEntries && newConfig.maxEntries !== this._config.maxEntries) { + const oldLogs = this._logs.toArray(); + const oldErrors = this._errors.toArray(); + const oldNetworkRequests = this._networkRequests.toArray(); + const oldUserActions = this._userActions.toArray(); + const oldTerminalLogs = this._terminalLogs.toArray(); + + this._logs = new CircularBuffer(this._config.maxEntries); + this._errors = new CircularBuffer(this._config.maxEntries); + this._networkRequests = new CircularBuffer(this._config.maxEntries); + this._userActions = new CircularBuffer(this._config.maxEntries); + this._terminalLogs = new CircularBuffer(this._config.maxEntries); + + // Re-add existing data + oldLogs.forEach((log) => this._logs.push(log)); + oldErrors.forEach((error) => this._errors.push(error)); + oldNetworkRequests.forEach((request) => this._networkRequests.push(request)); + oldUserActions.forEach((action) => this._userActions.push(action)); + oldTerminalLogs.forEach((log) => this._terminalLogs.push(log)); + } + + if (wasCapturing && this._config.enabled) { + this.startCapture(); + } + } + + // Cleanup method + private _cleanup(): void { + this.stopCapture(); + + if (typeof window !== 'undefined') { + window.removeEventListener('beforeunload', this._boundUnloadHandler); + } + } + + private _interceptConsole(): void { + const self = this; + + console.log = function (...args: any[]) { + self.captureLog('info', undefined, args); + self._originalConsoleLog.apply(console, args); + }; + + console.error = function (...args: any[]) { + self.captureLog('error', undefined, args); + self._originalConsoleError.apply(console, args); + }; + + console.warn = function (...args: any[]) { + self.captureLog('warn', undefined, args); + self._originalConsoleWarn.apply(console, args); + }; + } + + private _restoreConsole(): void { + console.log = this._originalConsoleLog; + console.error = this._originalConsoleError; + console.warn = this._originalConsoleWarn; + } + + private _interceptErrors(): void { + try { + window.addEventListener('error', this._boundErrorHandler); + window.addEventListener('unhandledrejection', this._boundRejectionHandler); + } catch (error) { + logger.error('Failed to intercept errors:', error); + } + } + + private _restoreErrors(): void { + try { + window.removeEventListener('error', this._boundErrorHandler); + window.removeEventListener('unhandledrejection', this._boundRejectionHandler); + } catch (error) { + logger.error('Failed to restore error handlers:', error); + } + } + + private _interceptNetwork(): void { + try { + // Store original fetch if not already stored + if (!this._originalFetch && typeof window !== 'undefined') { + this._originalFetch = window.fetch; + } + + if (!this._originalFetch) { + return; + } + + const originalFetch = this._originalFetch; + const self = this; + + window.fetch = async function (...args: Parameters) { + // Quick path for non-capturing mode + if (!self._isCapturing) { + return originalFetch.apply(this, args); + } + + const startTime = performance.now(); + const [resource, config] = args; + + try { + const response = await originalFetch.apply(this, args); + const duration = Math.round(performance.now() - startTime); + + // Only capture if still capturing (could have changed during request) + if (self._isCapturing) { + self.captureNetworkRequest({ + timestamp: new Date().toISOString(), + method: config?.method || 'GET', + url: typeof resource === 'string' ? resource : (resource as Request).url, + status: response.status, + duration, + }); + } + + return response; + } catch (error) { + const duration = Math.round(performance.now() - startTime); + + if (self._isCapturing) { + self.captureNetworkRequest({ + timestamp: new Date().toISOString(), + method: config?.method || 'GET', + url: typeof resource === 'string' ? resource : (resource as Request).url, + duration, + error: error instanceof Error ? error.message : 'Network error', + }); + } + + throw error; + } + }; + } catch (error) { + logger.error('Failed to intercept network requests:', error); + } + } + + private _restoreNetwork(): void { + try { + if (this._originalFetch && typeof window !== 'undefined') { + window.fetch = this._originalFetch; + } + } catch (error) { + logger.error('Failed to restore network fetch:', error); + } + } + + private _handleError(event: ErrorEvent): void { + this.captureError({ + timestamp: new Date().toISOString(), + type: 'javascript', + message: event.message, + stack: event.error?.stack, + url: event.filename, + line: event.lineno, + column: event.colno, + userAgent: navigator.userAgent, + }); + } + + private _handleUnhandledRejection(event: PromiseRejectionEvent): void { + this.captureError({ + timestamp: new Date().toISOString(), + type: 'javascript', + message: event.reason?.message || 'Unhandled promise rejection', + stack: event.reason?.stack, + userAgent: navigator.userAgent, + }); + } + + captureLog(level: LogEntry['level'], scope?: string, args: any[] = []): void { + if (!this._isCapturing) { + return; + } + + try { + const entry: LogEntry = { + timestamp: new Date().toISOString(), + level, + scope, + + /* Lazy stringification - only convert to string when needed */ + message: this._formatMessage(args), + data: args.length === 1 && typeof args[0] === 'object' ? args[0] : undefined, + }; + + this._logs.push(entry); + } catch (error) { + // Fallback - don't let logging errors break the app + console.error('Debug logger failed to capture log:', error); + } + } + + private _formatMessage(args: any[]): string { + this._seenObjects = new WeakSet(); // Reset for each message + + return args + .map((arg) => { + if (typeof arg === 'object' && arg !== null) { + try { + // Prevent circular reference errors and limit depth + return JSON.stringify(arg, this._jsonReplacer.bind(this), 2); + } catch { + return '[Object]'; + } + } + + return String(arg); + }) + .join(' '); + } + + private _jsonReplacer(_key: string, value: any): any { + // Prevent circular references and limit object depth + if (typeof value === 'object' && value !== null) { + if (this._seenObjects.has(value)) { + return '[Circular]'; + } + + this._seenObjects.add(value); + } + + return value; + } + + captureError(error: ErrorEntry): void { + try { + this._errors.push(error); + } catch (err) { + console.error('Debug logger failed to capture error:', err); + } + } + + captureNetworkRequest(request: NetworkEntry): void { + try { + this._networkRequests.push(request); + } catch (error) { + console.error('Debug logger failed to capture network request:', error); + } + } + + captureUserAction(action: string, target?: string, data?: any): void { + if (!this._isCapturing) { + return; + } + + try { + const entry: UserActionEntry = { + timestamp: new Date().toISOString(), + action, + target, + data, + }; + + this._userActions.push(entry); + } catch (error) { + console.error('Debug logger failed to capture user action:', error); + } + } + + captureTerminalLog(entry: TerminalEntry): void { + try { + // Debounce terminal logs to prevent spam + if (this._config.debounceTerminal > 0) { + this._terminalLogQueue.push(entry); + + if (this._terminalLogTimer) { + clearTimeout(this._terminalLogTimer); + } + + this._terminalLogTimer = setTimeout(() => { + this._flushTerminalLogs(); + }, this._config.debounceTerminal); + } else { + this._terminalLogs.push(entry); + } + } catch (error) { + console.error('Debug logger failed to capture terminal log:', error); + } + } + + private _flushTerminalLogs(): void { + try { + while (this._terminalLogQueue.length > 0) { + const entry = this._terminalLogQueue.shift(); + + if (entry) { + this._terminalLogs.push(entry); + } + } + this._terminalLogTimer = null; + } catch (error) { + console.error('Debug logger failed to flush terminal logs:', error); + } + } + + async generateDebugLog(): Promise { + try { + // Enable debug mode temporarily if not already enabled + const wasEnabled = this._config.enabled; + + if (!wasEnabled) { + this.enableDebugMode(); + } + + // Flush any pending terminal logs + if (this._terminalLogTimer) { + clearTimeout(this._terminalLogTimer); + this._flushTerminalLogs(); + } + + const [systemInfo, appInfo, performanceInfo, state] = await Promise.all([ + this._collectSystemInfo(), + this._collectAppInfo(), + Promise.resolve(this._collectPerformanceInfo()), + Promise.resolve(this._collectStateInfo()), + ]); + + // Get logs from logStore with proper error handling + const logStoreLogs = await this._getLogStoreLogs(); + + const debugData: DebugLogData = { + timestamp: new Date().toISOString(), + sessionId: this._generateSessionId(), + systemInfo, + appInfo, + logs: [...this._logs.toArray(), ...logStoreLogs], + errors: this._errors.toArray(), + networkRequests: this._networkRequests.toArray(), + performance: performanceInfo, + state, + userActions: this._userActions.toArray(), + terminalLogs: this._terminalLogs.toArray(), + }; + + // Restore previous state + if (!wasEnabled) { + this.disableDebugMode(); + } + + return debugData; + } catch (error) { + logger.error('Failed to generate debug log:', error); + throw error; + } + } + + private async _getLogStoreLogs(): Promise { + try { + const store = getLogStore(); + + if (!store) { + // Try to load the store if not already loaded + try { + const { logStore: storeModule } = await import('~/lib/stores/logs'); + logStore = storeModule; + + return this._getLogStoreLogs(); + } catch { + return []; + } + } + + const logs = store.getLogs?.() || []; + + return logs.slice(0, 500).map((log: any) => ({ + timestamp: log.timestamp, + level: log.level as LogEntry['level'], + scope: log.category, + message: log.message, + data: log.details, + })); + } catch (error) { + logger.warn('Failed to get logStore logs:', error); + return []; + } + } + + private async _collectSystemInfo(): Promise { + let platform = 'Unknown'; + + if (isMac) { + platform = 'macOS'; + } else if (isWindows) { + platform = 'Windows'; + } else if (isLinux) { + platform = 'Linux'; + } + + return { + platform, + userAgent: navigator.userAgent, + screenResolution: `${screen.width}x${screen.height}`, + viewportSize: `${window.innerWidth}x${window.innerHeight}`, + isMobile: isMobile(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + language: navigator.language, + cookiesEnabled: navigator.cookieEnabled, + localStorageEnabled: this._testLocalStorage(), + sessionStorageEnabled: this._testSessionStorage(), + }; + } + + private async _collectAppInfo(): Promise { + let workbenchInfo = { + currentView: 'code', + showWorkbench: false, + showTerminal: true, + artifactsCount: 0, + filesCount: 0, + unsavedFiles: 0, + hasActivePreview: false, + }; + + // Try to get workbench information + try { + if (typeof window !== 'undefined') { + // Access stores if available + const workbenchStore = (window as any).__bolt_workbench_store; + + if (workbenchStore) { + const state = workbenchStore.get?.() || {}; + workbenchInfo = { + currentView: state.currentView || 'code', + showWorkbench: state.showWorkbench || false, + showTerminal: state.showTerminal !== undefined ? state.showTerminal : true, + artifactsCount: Object.keys(state.artifacts || {}).length, + filesCount: Object.keys(state.files || {}).length, + unsavedFiles: state.unsavedFiles?.size || 0, + hasActivePreview: (state.previews || []).length > 0, + }; + } + } + } catch { + // Ignore errors when accessing stores + } + + return { + version: this._getAppVersion(), + buildTime: new Date().toISOString(), + currentModel: this._getCurrentModel(), + currentProvider: this._getCurrentProvider(), + projectType: this._getProjectType(), + workbenchView: workbenchInfo.currentView, + hasActivePreview: workbenchInfo.hasActivePreview, + unsavedFiles: workbenchInfo.unsavedFiles, + workbenchState: workbenchInfo, + gitInfo: await this._getGitInfo(), + }; + } + + private _getAppVersion(): string { + try { + // Try to get version from environment or default + return import.meta.env?.VITE_APP_VERSION || '1.0.0'; + } catch { + return '1.0.0'; + } + } + + private _getCurrentModel(): string { + try { + // Try to get from localStorage or environment + if (typeof window !== 'undefined') { + const stored = localStorage.getItem('bolt_current_model'); + + if (stored) { + return stored; + } + } + + return DEFAULT_MODEL; + } catch { + return DEFAULT_MODEL; + } + } + + private _getCurrentProvider(): string { + try { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem('bolt_current_provider'); + + if (stored) { + return stored; + } + } + + return PROVIDER_LIST[0]?.name || 'unknown'; + } catch { + return PROVIDER_LIST[0]?.name || 'unknown'; + } + } + + private _getProjectType(): string { + try { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem('bolt_project_type'); + + if (stored) { + return stored; + } + } + + return 'unknown'; + } catch { + return 'unknown'; + } + } + + private async _getGitInfo(): Promise { + try { + // Try to fetch git info from existing API endpoint + const response = await fetch('/api/system/git-info'); + + if (response.ok) { + const gitInfo = await response.json(); + + // Transform the API response to match our interface + const gitInfoTyped = gitInfo as any; + + // Type assertion for API response + return { + branch: gitInfoTyped.local?.branch || 'unknown', + commit: gitInfoTyped.local?.commitHash || 'unknown', + isDirty: false, // The existing API doesn't provide this info + remoteUrl: gitInfoTyped.local?.remoteUrl, + lastCommit: gitInfoTyped.local + ? { + message: 'Latest commit', + date: gitInfoTyped.local.commitTime, + author: gitInfoTyped.local.author, + } + : undefined, + }; + } + } catch { + // API not available, try client-side fallback + console.warn('Git info API not available, using fallback'); + } + + // Fallback: try to get basic git info from localStorage or known values + return this._getGitInfoFallback(); + } + + private _getGitInfoFallback(): AppInfo['gitInfo'] { + try { + // Try to get from localStorage (could be set by the app) + const stored = localStorage.getItem('bolt_git_info'); + + if (stored) { + return JSON.parse(stored); + } + + // Try to get from environment/build variables + const branch = import.meta.env?.VITE_GIT_BRANCH || 'unknown'; + const commit = import.meta.env?.VITE_GIT_COMMIT || 'unknown'; + + return { + branch, + commit, + isDirty: false, // Assume clean if we don't know + }; + } catch { + return { + branch: 'unknown', + commit: 'unknown', + isDirty: false, + }; + } + } + + private _collectPerformanceInfo(): PerformanceEntry { + const timing = performance.timing as any; + const paintEntries = performance.getEntriesByType('paint'); + + return { + navigationStart: timing.navigationStart, + loadTime: timing.loadEventEnd - timing.navigationStart, + domContentLoaded: timing.domContentLoadedEventEnd - timing.navigationStart, + firstPaint: paintEntries.find((entry) => entry.name === 'first-paint')?.startTime, + firstContentfulPaint: paintEntries.find((entry) => entry.name === 'first-contentful-paint')?.startTime, + memoryUsage: (performance as any).memory + ? { + used: (performance as any).memory.usedJSHeapSize, + total: (performance as any).memory.totalJSHeapSize, + limit: (performance as any).memory.jsHeapSizeLimit, + } + : undefined, + timing, + }; + } + + private _collectStateInfo(): StateEntry { + const store = getLogStore(); + let alerts: StateEntry['alerts'] = []; + + // Get recent alerts from logStore + if (store) { + try { + const logs = store.getLogs?.() || []; + alerts = logs + .filter((log: any) => ['error', 'warning'].includes(log.level)) + .slice(0, 10) + .map((log: any) => ({ + type: log.level, + title: log.message.substring(0, 100), + source: log.category, + })); + } catch { + // Ignore errors + } + } + + // Get workbench state + let workbenchState = { + currentView: 'code', + showWorkbench: false, + showTerminal: true, + artifactsCount: 0, + filesCount: 0, + }; + + try { + if (typeof window !== 'undefined') { + const workbenchStore = (window as any).__bolt_workbench_store; + + if (workbenchStore) { + const state = workbenchStore.get?.() || {}; + workbenchState = { + currentView: state.currentView || 'code', + showWorkbench: state.showWorkbench || false, + showTerminal: state.showTerminal !== undefined ? state.showTerminal : true, + artifactsCount: Object.keys(state.artifacts || {}).length, + filesCount: Object.keys(state.files || {}).length, + }; + } + } + } catch { + // Ignore errors + } + + return { + currentView: workbenchState.currentView, + showWorkbench: workbenchState.showWorkbench, + showTerminal: workbenchState.showTerminal, + artifactsCount: workbenchState.artifactsCount, + filesCount: workbenchState.filesCount, + alerts, + }; + } + + private _testLocalStorage(): boolean { + try { + localStorage.setItem('test', 'test'); + localStorage.removeItem('test'); + + return true; + } catch { + return false; + } + } + + private _testSessionStorage(): boolean { + try { + sessionStorage.setItem('test', 'test'); + sessionStorage.removeItem('test'); + + return true; + } catch { + return false; + } + } + + private _generateSessionId(): string { + return `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; + } + + clearLogs(): void { + try { + this._logs.clear(); + this._errors.clear(); + this._networkRequests.clear(); + this._userActions.clear(); + this._terminalLogs.clear(); + + // Clear any pending terminal logs + this._terminalLogQueue = []; + + if (this._terminalLogTimer) { + clearTimeout(this._terminalLogTimer); + this._terminalLogTimer = null; + } + + logger.info('Debug logs cleared'); + } catch (error) { + logger.error('Failed to clear logs:', error); + } + } + + // Get current memory usage statistics + getMemoryStats(): { + logs: number; + errors: number; + networkRequests: number; + userActions: number; + terminalLogs: number; + total: number; + } { + const stats = { + logs: this._logs.getSize(), + errors: this._errors.getSize(), + networkRequests: this._networkRequests.getSize(), + userActions: this._userActions.getSize(), + terminalLogs: this._terminalLogs.getSize(), + total: 0, + }; + + stats.total = stats.logs + stats.errors + stats.networkRequests + stats.userActions + stats.terminalLogs; + + return stats; + } +} + +// Export singleton instance with default configuration +export const debugLogger = new DebugLogger({ + enabled: false, // Start disabled for performance + maxEntries: 1000, + captureConsole: true, + captureNetwork: true, + captureErrors: true, + debounceTerminal: 100, +}); + +// Helper function to download debug log +export async function downloadDebugLog(filename?: string): Promise { + try { + const debugData = await debugLogger.generateDebugLog(); + + // Create a formatted summary + const summary = createDebugSummary(debugData); + const fullContent = `${summary}\n\n=== DETAILED DEBUG DATA ===\n\n${JSON.stringify(debugData, null, 2)}`; + + const blob = new Blob([fullContent], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = filename || `bolt-debug-${new Date().toISOString().split('T')[0]}.txt`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + URL.revokeObjectURL(url); + + logger.info('Debug log downloaded successfully'); + } catch (error) { + logger.error('Failed to download debug log:', error); + } +} + +// Create a human-readable summary of the debug data +function createDebugSummary(data: DebugLogData): string { + const summary = [ + '=== BOLT DIY DEBUG LOG SUMMARY ===', + `Generated: ${new Date(data.timestamp).toLocaleString()}`, + `Session ID: ${data.sessionId}`, + '', + '=== SYSTEM INFORMATION ===', + `Platform: ${data.systemInfo.platform}`, + `Browser: ${data.systemInfo.userAgent.split(' ').slice(0, 2).join(' ')}`, + `Screen: ${data.systemInfo.screenResolution}`, + `Mobile: ${data.systemInfo.isMobile ? 'Yes' : 'No'}`, + `Timezone: ${data.systemInfo.timezone}`, + '', + '=== APPLICATION INFORMATION ===', + `Version: ${data.appInfo.version}`, + `Current Model: ${data.appInfo.currentModel}`, + `Current Provider: ${data.appInfo.currentProvider}`, + `Project Type: ${data.appInfo.projectType}`, + `Workbench View: ${data.appInfo.workbenchView}`, + `Active Preview: ${data.appInfo.hasActivePreview ? 'Yes' : 'No'}`, + `Unsaved Files: ${data.appInfo.unsavedFiles}`, + '', + '=== GIT INFORMATION ===', + data.appInfo.gitInfo + ? [ + `Branch: ${data.appInfo.gitInfo.branch}`, + `Commit: ${data.appInfo.gitInfo.commit.substring(0, 8)}`, + `Working Directory: ${data.appInfo.gitInfo.isDirty ? 'Dirty' : 'Clean'}`, + data.appInfo.gitInfo.remoteUrl ? `Remote: ${data.appInfo.gitInfo.remoteUrl}` : '', + data.appInfo.gitInfo.lastCommit + ? `Last Commit: ${data.appInfo.gitInfo.lastCommit.message.substring(0, 50)}...` + : '', + ] + .filter(Boolean) + .join('\n') + : 'Git information not available', + '', + '=== SESSION STATISTICS ===', + `Total Logs: ${data.logs.length}`, + `Errors: ${data.errors.length}`, + `Network Requests: ${data.networkRequests.length}`, + `User Actions: ${data.userActions.length}`, + `Terminal Logs: ${data.terminalLogs.length}`, + '', + '=== RECENT ALERTS ===', + ...data.state.alerts.slice(0, 5).map((alert) => `${alert.type.toUpperCase()}: ${alert.title}`), + '', + '=== PERFORMANCE ===', + `Page Load Time: ${data.performance.loadTime}ms`, + `DOM Content Loaded: ${data.performance.domContentLoaded}ms`, + data.performance.memoryUsage + ? `Memory Usage: ${(data.performance.memoryUsage.used / 1024 / 1024).toFixed(2)} MB` + : 'Memory Usage: N/A', + '', + '=== WORKBENCH STATE ===', + `Current View: ${data.state.currentView}`, + `Show Workbench: ${data.state.showWorkbench}`, + `Show Terminal: ${data.state.showTerminal}`, + `Artifacts: ${data.state.artifactsCount}`, + `Files: ${data.state.filesCount}`, + ]; + + return summary.join('\n'); +} + +// Utility functions for capturing additional data +export function captureTerminalLog( + content: string, + type: 'input' | 'output' | 'error' = 'output', + command?: string, +): void { + // Only capture if content is meaningful (not just whitespace or control characters) + if (!content || content.trim().length === 0) { + return; + } + + try { + debugLogger.captureTerminalLog({ + timestamp: new Date().toISOString(), + type, + content: content.trim(), + command, + }); + } catch (error) { + console.error('Failed to capture terminal log:', error); + } +} + +export function captureUserAction(action: string, target?: string, data?: any): void { + try { + debugLogger.captureUserAction(action, target, data); + } catch (error) { + console.error('Failed to capture user action:', error); + } +} + +export function getDebugLogger(): DebugLogger { + return debugLogger; +} + +// Utility function to enable debug mode on demand +export function enableDebugMode(): void { + debugLogger.enableDebugMode(); +} + +// Utility function to disable debug mode +export function disableDebugMode(): void { + debugLogger.disableDebugMode(); +} + +// Utility function to get debug logger status +export function getDebugStatus(): { initialized: boolean; capturing: boolean; enabled: boolean } { + return debugLogger.getStatus(); +} + +// Utility function to update debug configuration +export function updateDebugConfig(config: Partial): void { + debugLogger.updateConfig(config); +} + +// Initialize debug logger when this module is imported +if (typeof window !== 'undefined') { + // Defer initialization to avoid blocking + setTimeout(() => { + debugLogger.initialize(); + }, 0); +} diff --git a/app/utils/logger.ts b/app/utils/logger.ts index 2cdfd0dc62..43f7887ffa 100644 --- a/app/utils/logger.ts +++ b/app/utils/logger.ts @@ -17,21 +17,21 @@ interface Logger { let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL || (import.meta.env.DEV ? 'debug' : 'info'); export const logger: Logger = { - trace: (...messages: any[]) => log('trace', undefined, messages), - debug: (...messages: any[]) => log('debug', undefined, messages), - info: (...messages: any[]) => log('info', undefined, messages), - warn: (...messages: any[]) => log('warn', undefined, messages), - error: (...messages: any[]) => log('error', undefined, messages), + trace: (...messages: any[]) => logWithDebugCapture('trace', undefined, messages), + debug: (...messages: any[]) => logWithDebugCapture('debug', undefined, messages), + info: (...messages: any[]) => logWithDebugCapture('info', undefined, messages), + warn: (...messages: any[]) => logWithDebugCapture('warn', undefined, messages), + error: (...messages: any[]) => logWithDebugCapture('error', undefined, messages), setLevel, }; export function createScopedLogger(scope: string): Logger { return { - trace: (...messages: any[]) => log('trace', scope, messages), - debug: (...messages: any[]) => log('debug', scope, messages), - info: (...messages: any[]) => log('info', scope, messages), - warn: (...messages: any[]) => log('warn', scope, messages), - error: (...messages: any[]) => log('error', scope, messages), + trace: (...messages: any[]) => logWithDebugCapture('trace', scope, messages), + debug: (...messages: any[]) => logWithDebugCapture('debug', scope, messages), + info: (...messages: any[]) => logWithDebugCapture('info', scope, messages), + warn: (...messages: any[]) => logWithDebugCapture('warn', scope, messages), + error: (...messages: any[]) => logWithDebugCapture('error', scope, messages), setLevel, }; } @@ -123,3 +123,40 @@ function getColorForLevel(level: DebugLevel): string { } export const renderLogger = createScopedLogger('Render'); + +// Debug logging integration +let debugLogger: any = null; + +// Lazy load debug logger to avoid circular dependencies +const getDebugLogger = () => { + if (!debugLogger && typeof window !== 'undefined') { + try { + // Use dynamic import asynchronously but don't block the function + import('./debugLogger') + .then(({ debugLogger: loggerInstance }) => { + debugLogger = loggerInstance; + }) + .catch(() => { + // Debug logger not available, skip integration + }); + } catch { + // Debug logger not available, skip integration + } + } + + return debugLogger; +}; + +// Override the log function to also capture to debug logger + +function logWithDebugCapture(level: DebugLevel, scope: string | undefined, messages: any[]) { + // Call original log function (the one that does the actual console logging) + log(level, scope, messages); + + // Also capture to debug logger if available + const debug = getDebugLogger(); + + if (debug) { + debug.captureLog(level, scope, messages); + } +} diff --git a/app/utils/shell.ts b/app/utils/shell.ts index 9b9d02e491..d80d0d00f0 100644 --- a/app/utils/shell.ts +++ b/app/utils/shell.ts @@ -36,6 +36,24 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer } terminal.write(data); + + // Capture terminal output for debugging + try { + import('~/utils/debugLogger') + .then(({ captureTerminalLog }) => { + // Clean the data by removing ANSI escape sequences for logging + const cleanData = data.replace(/\x1b\[[0-9;]*[mG]/g, '').trim(); + + if (cleanData) { + captureTerminalLog(cleanData, 'output'); + } + }) + .catch(() => { + // Ignore if debug logger is not available + }); + } catch { + // Ignore errors in debug logging + } }, }), ); @@ -45,6 +63,24 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer if (isInteractive) { input.write(data); + + // Capture terminal input for debugging + try { + import('~/utils/debugLogger') + .then(({ captureTerminalLog }) => { + // Clean the data and check if it's a command (not just cursor movement) + const cleanData = data.replace(/\x1b\[[0-9;]*[A-Z]/g, '').trim(); + + if (cleanData && cleanData !== '\r' && cleanData !== '\n') { + captureTerminalLog(cleanData, 'input'); + } + }) + .catch(() => { + // Ignore if debug logger is not available + }); + } catch { + // Ignore errors in debug logging + } } });