UNPKG

debug-time-machine-cli

Version:

πŸš€ Debug Time Machine CLI - μ™„μ „ μžλ™ν™”λœ React 디버깅 도ꡬ

521 lines (441 loc) β€’ 16.1 kB
/** * Debug Time Machine Auto Injector * * μ‚¬μš©μž 앱에 μžλ™μœΌλ‘œ μ£Όμž…λ˜μ–΄ Hook 없이도 이벀트 캑처λ₯Ό ν™œμ„±ν™”ν•©λ‹ˆλ‹€. * 이 μŠ€ν¬λ¦½νŠΈλŠ” νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ μžλ™μœΌλ‘œ μ‹€ν–‰λ˜μ–΄ Debug Time Machineκ³Ό μ—°κ²°ν•©λ‹ˆλ‹€. */ (function() { 'use strict'; console.log('πŸš€ Debug Time Machine Auto Injector μ‹œμž‘'); // 이미 μ‹€ν–‰λ˜μ—ˆλŠ”μ§€ 확인 if (window.debugTimeMachineInjected) { console.log('⚠️ Debug Time Machine이 이미 μ£Όμž…λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); return; } window.debugTimeMachineInjected = true; // μ„€μ • const CONFIG = { websocketUrl: 'ws://localhost:4000/ws', debugMode: true, captureUserActions: true, captureErrors: true, captureStateChanges: true, maxReconnectAttempts: 5, reconnectInterval: 3000, }; // μƒνƒœ let ws = null; let isConnected = false; let clientId = null; let reconnectAttempts = 0; let actionCount = 0; let lastSecond = 0; // μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜ function log(message, type = 'info', data) { if (!CONFIG.debugMode) return; const styles = { info: 'color: #2196F3; font-weight: bold;', success: 'color: #4CAF50; font-weight: bold;', warning: 'color: #FF9800; font-weight: bold;', error: 'color: #F44336; font-weight: bold;', }; const timestamp = new Date().toISOString(); const style = styles[type] || styles.info; if (data) { console.groupCollapsed(`%c[Debug Auto-Injector ${timestamp}] ${message}`, style); console.log('Data:', data); console.groupEnd(); } else { console.log(`%c[Debug Auto-Injector ${timestamp}] ${message}`, style); } } function generateId() { return `${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; } function sendMessage(type, data) { if (!ws || ws.readyState !== WebSocket.OPEN) { log(`❌ Cannot send message: WebSocket not ready (state: ${ws?.readyState})`, 'warning'); return false; } const message = { type: type, payload: data, timestamp: Date.now(), clientId: clientId || undefined, }; try { ws.send(JSON.stringify(message)); log(`βœ… Message sent: ${type}`, 'success', { type, dataKeys: Object.keys(data || {}) }); return true; } catch (error) { log(`❌ Failed to send message: ${type}`, 'error', error); return false; } } // WebSocket μ—°κ²° function connect() { if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) { log('이미 μ—°κ²° μ€‘μ΄κ±°λ‚˜ μ—°κ²°λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.', 'warning'); return; } try { log('Debug Time Machine μ„œλ²„μ— μ—°κ²° 쀑...', 'info'); ws = new WebSocket(CONFIG.websocketUrl); ws.onopen = () => { isConnected = true; reconnectAttempts = 0; log('βœ… Debug Time Machine μ„œλ²„μ— 연결됨!', 'success'); // μ—°κ²° λ©”μ‹œμ§€ 전솑 setTimeout(() => { sendMessage('CONNECTION', { type: 'client_ready', url: window.location.href, userAgent: navigator.userAgent, timestamp: Date.now(), injected: true, // μžλ™ μ£Όμž…λ˜μ—ˆμŒμ„ ν‘œμ‹œ }); }, 300); }; ws.onmessage = (event) => { try { const message = JSON.parse(event.data); switch (message.type) { case 'CONNECTION': if (message.payload?.clientId) { clientId = message.payload.clientId; log(`ν΄λΌμ΄μ–ΈνŠΈ ID 할당됨: ${clientId}`, 'success'); } break; case 'PING': sendMessage('PONG', {}); break; case 'PONG': log('μ„œλ²„λ‘œλΆ€ν„° PONG μˆ˜μ‹ ', 'info'); break; default: log(`μ•Œ 수 μ—†λŠ” λ©”μ‹œμ§€: ${message.type}`, 'warning'); } } catch (error) { log('λ©”μ‹œμ§€ νŒŒμ‹± μ‹€νŒ¨', 'error', error); } }; ws.onclose = (event) => { isConnected = false; log(`μ—°κ²° μ’…λ£Œ (μ½”λ“œ: ${event.code})`, 'warning'); // μžλ™ μž¬μ—°κ²° if (reconnectAttempts < CONFIG.maxReconnectAttempts) { reconnectAttempts++; log(`μž¬μ—°κ²° μ‹œλ„ 쀑... (${reconnectAttempts}/${CONFIG.maxReconnectAttempts})`, 'info'); setTimeout(connect, CONFIG.reconnectInterval); } }; ws.onerror = () => { log('WebSocket μ—λŸ¬ λ°œμƒ', 'error'); }; } catch (error) { log('WebSocket μ—°κ²° 생성 μ‹€νŒ¨', 'error', error); } } // μ‚¬μš©μž μ•‘μ…˜ 캑처 function setupUserActionCapture() { log('μ‚¬μš©μž μ•‘μ…˜ 캑처 μ„€μ • 쀑...', 'info'); const excludeSelectors = [ '.debug-time-machine', '[data-debug-ignore]', 'script', 'style', 'meta', 'link', ]; function shouldCaptureEvent(target) { return !excludeSelectors.some(selector => { try { return target.matches && target.matches(selector); } catch { return false; } }); } function checkRateLimit() { const currentSecond = Math.floor(Date.now() / 1000); if (currentSecond !== lastSecond) { lastSecond = currentSecond; actionCount = 0; } if (actionCount >= 10) { // μ΄ˆλ‹Ή μ΅œλŒ€ 10개 μ•‘μ…˜ return false; } actionCount++; return true; } function getElementSelector(element) { if (element.id) return `#${element.id}`; const path = []; let current = element; while (current && current.nodeType === Node.ELEMENT_NODE && path.length < 4) { let selector = current.tagName.toLowerCase(); if (current.className) { const classes = current.className.split(' ').filter(c => c.trim()); if (classes.length > 0) { selector += '.' + classes.slice(0, 2).join('.'); } } path.unshift(selector); current = current.parentElement; } return path.join(' > '); } function captureUserAction(event) { const target = event.target; if (!target || !shouldCaptureEvent(target) || !checkRateLimit()) { return; } log(`πŸ–±οΈ μ‚¬μš©μž μ•‘μ…˜ 캑처: ${event.type} on ${target.tagName}`, 'info'); const actionData = { actionType: event.type, element: target.tagName.toLowerCase(), coordinates: { x: event.clientX || 0, y: event.clientY || 0, }, target: { id: target.id || '', className: target.className || '', tagName: target.tagName, textContent: (target.textContent || '').substring(0, 100), }, timestamp: Date.now(), url: window.location.href, selector: getElementSelector(target), injected: true, }; sendMessage('USER_ACTION', actionData); } // 이벀트 λ¦¬μŠ€λ„ˆ 등둝 const eventTypes = ['click', 'submit', 'change', 'input', 'focus', 'blur']; eventTypes.forEach(eventType => { document.addEventListener(eventType, captureUserAction, true); log(` βœ… ${eventType} 이벀트 λ¦¬μŠ€λ„ˆ 등둝`, 'info'); }); log('βœ… μ‚¬μš©μž μ•‘μ…˜ 캑처 μ„€μ • μ™„λ£Œ', 'success'); } // μ—λŸ¬ 캑처 function setupErrorCapture() { log('μ—λŸ¬ 캑처 μ„€μ • 쀑...', 'info'); // Global error handler const originalOnError = window.onerror; window.onerror = function(message, source, lineno, colno, error) { log(`πŸ”΄ μ—λŸ¬ 캑처: ${message}`, 'error'); sendMessage('ERROR', { message: String(message), stack: error?.stack || 'No stack available', timestamp: Date.now(), url: window.location.href, userAgent: navigator.userAgent, source: source, lineNumber: lineno, columnNumber: colno, injected: true, }); // μ›λž˜ ν•Έλ“€λŸ¬ 호좜 if (originalOnError) { return originalOnError.apply(this, arguments); } return false; }; // Unhandled promise rejection const originalOnUnhandledRejection = window.onunhandledrejection; window.onunhandledrejection = function(event) { const reason = event.reason; const message = reason instanceof Error ? reason.message : String(reason); log(`πŸ”΄ Promise rejection 캑처: ${message}`, 'error'); sendMessage('ERROR', { message: `Unhandled Promise Rejection: ${message}`, stack: reason instanceof Error ? reason.stack : undefined, timestamp: Date.now(), url: window.location.href, userAgent: navigator.userAgent, source: 'promise', injected: true, }); // μ›λž˜ ν•Έλ“€λŸ¬ 호좜 if (originalOnUnhandledRejection) { return originalOnUnhandledRejection.apply(this, arguments); } }; log('βœ… μ—λŸ¬ 캑처 μ„€μ • μ™„λ£Œ', 'success'); } // API 호좜 캑처 function setupApiCapture() { log('API 호좜 캑처 μ„€μ • 쀑...', 'info'); // Fetch API μΈν„°μ…‰νŠΈ const originalFetch = window.fetch; window.fetch = async function(...args) { const [url, options] = args; const urlString = typeof url === 'string' ? url : url.toString(); // Debug Time Machine 자체 μš”μ²­μ€ μ œμ™Έ if (urlString.includes('localhost:4000') || urlString.includes('localhost:8080')) { return originalFetch.apply(this, args); } const startTime = Date.now(); const requestId = generateId(); log(`🌐 API 호좜: ${urlString}`, 'info'); try { const response = await originalFetch.apply(this, args); const duration = Date.now() - startTime; sendMessage('API_CALL', { id: requestId, url: urlString, method: options?.method || 'GET', status: response.status, statusText: response.statusText, duration: duration, success: true, timestamp: Date.now(), injected: true, }); log(`βœ… API 호좜 성곡: ${urlString} (${response.status}, ${duration}ms)`, 'success'); return response; } catch (error) { const duration = Date.now() - startTime; sendMessage('API_CALL', { id: requestId, url: urlString, method: options?.method || 'GET', error: error.message, duration: duration, success: false, timestamp: Date.now(), injected: true, }); log(`❌ API 호좜 μ‹€νŒ¨: ${urlString} (${error.message}, ${duration}ms)`, 'error'); throw error; } }; log('βœ… API 호좜 캑처 μ„€μ • μ™„λ£Œ', 'success'); } // React μƒνƒœ λ³€κ²½ 감지 (μ‹€ν—˜μ ) function setupReactStateDetection() { log('React μƒνƒœ λ³€κ²½ 감지 μ„€μ • 쀑...', 'info'); // React DevTools 이벀트 감지 μ‹œλ„ if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) { const reactDevTools = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; // React Fiber μ—…λ°μ΄νŠΈ 감지 const originalOnCommit = reactDevTools.onCommitFiberRoot; reactDevTools.onCommitFiberRoot = function(id, root, priorityLevel) { log('πŸ”„ React μ»΄ν¬λ„ŒνŠΈ μ—…λ°μ΄νŠΈ 감지', 'info'); sendMessage('STATE_CHANGE', { type: 'react_commit', timestamp: Date.now(), url: window.location.href, injected: true, }); if (originalOnCommit) { return originalOnCommit.apply(this, arguments); } }; log('βœ… React DevTools 연동 μ™„λ£Œ', 'success'); } else { log('⚠️ React DevToolsλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€', 'warning'); } } // μ΄ˆκΈ°ν™” function init() { log('πŸš€ Debug Time Machine Auto Injector μ΄ˆκΈ°ν™” μ‹œμž‘', 'info'); // DOM이 쀀비될 λ•ŒκΉŒμ§€ λŒ€κΈ° if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); return; } // μ„œλ²„ μ—°κ²° 확인 ν›„ μ΄ˆκΈ°ν™” (νƒ€μž„μ•„μ›ƒ μΆ”κ°€) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 3000); // 3초 νƒ€μž„μ•„μ›ƒ fetch('http://localhost:4000/health', { signal: controller.signal }) .then(response => { clearTimeout(timeoutId); return response.json(); }) .then(data => { log(`βœ… Debug Time Machine μ„œλ²„ 발견! (ν΄λΌμ΄μ–ΈνŠΈ: ${data.clients || 0}개)`, 'success'); // λͺ¨λ“  κΈ°λŠ₯ ν™œμ„±ν™” connect(); setupUserActionCapture(); setupErrorCapture(); setupApiCapture(); setupReactStateDetection(); // μ „μ—­ μ°Έμ‘° μ„€μ • window.debugTimeMachine = { isConnected: () => isConnected, getClientId: () => clientId, sendMessage: sendMessage, captureError: (error, context) => { log(`πŸ”΄ μˆ˜λ™ μ—λŸ¬ 캑처: ${error.message}`, 'error'); sendMessage('ERROR', { message: error.message || String(error), stack: error.stack || 'No stack available', timestamp: Date.now(), url: window.location.href, userAgent: navigator.userAgent, context: context, manual: true, injected: true, }); }, // μˆ˜λ™ ν…ŒμŠ€νŠΈ ν•¨μˆ˜λ“€ testUserAction: () => { log('πŸ§ͺ μˆ˜λ™ μ‚¬μš©μž μ•‘μ…˜ ν…ŒμŠ€νŠΈ', 'info'); sendMessage('USER_ACTION', { actionType: 'test', element: 'button', timestamp: Date.now(), url: window.location.href, target: { tagName: 'BUTTON', textContent: 'Test Button' }, manual: true, injected: true, }); }, testStateChange: () => { log('πŸ§ͺ μˆ˜λ™ μƒνƒœ λ³€κ²½ ν…ŒμŠ€νŠΈ', 'info'); sendMessage('STATE_CHANGE', { componentName: 'TestComponent', prevState: { test: 'old' }, newState: { test: 'new' }, timestamp: Date.now(), url: window.location.href, manual: true, injected: true, }); } }; log('πŸŽ‰ Debug Time Machine Auto Injector μ΄ˆκΈ°ν™” μ™„λ£Œ!', 'success'); // ν…ŒμŠ€νŠΈ λ©”μ‹œμ§€ 전솑 setTimeout(() => { if (isConnected) { sendMessage('TEST', { message: 'Auto Injectorκ°€ μ„±κ³΅μ μœΌλ‘œ μ΄ˆκΈ°ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€!', timestamp: Date.now(), injected: true, userAgent: navigator.userAgent, url: window.location.href, }); log('πŸ“€ μ΄ˆκΈ°ν™” μ™„λ£Œ λ©”μ‹œμ§€ 전솑됨', 'success'); } else { log('⚠️ WebSocket 연결이 아직 μ€€λΉ„λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€', 'warning'); } }, 2000); }) .catch((error) => { clearTimeout(timeoutId); if (error.name === 'AbortError') { log('Debug Time Machine μ„œλ²„ μ—°κ²° νƒ€μž„μ•„μ›ƒ. 일반 λͺ¨λ“œλ‘œ μ‹€ν–‰λ©λ‹ˆλ‹€.', 'info'); } else { log('Debug Time Machine μ„œλ²„λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. 일반 λͺ¨λ“œλ‘œ μ‹€ν–‰λ©λ‹ˆλ‹€.', 'info'); } log('πŸ’‘ 디버그 λͺ¨λ“œλ₯Ό μ›ν•œλ‹€λ©΄: debug-time-machine-cli start-with-app "npm start"', 'info'); }); } // μ¦‰μ‹œ μ΄ˆκΈ°ν™” μ‹œμž‘ init(); })();