debug-time-machine-cli
Version:
π Debug Time Machine CLI - μμ μλνλ React λλ²κΉ λꡬ
521 lines (441 loc) β’ 16.1 kB
JavaScript
/**
* 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();
})();