UNPKG

debug-time-machine

Version:
746 lines (743 loc) 27.6 kB
// Debug Time Machine Unified Package // src/useDebugTimeMachine.ts import { useEffect, useRef, useState, useCallback } from "react"; // src/types.ts var MessageType = /* @__PURE__ */ ((MessageType2) => { MessageType2["CONNECTION"] = "CONNECTION"; MessageType2["USER_ACTION"] = "USER_ACTION"; MessageType2["STATE_CHANGE"] = "STATE_CHANGE"; MessageType2["ERROR"] = "ERROR"; MessageType2["PING"] = "PING"; MessageType2["PONG"] = "PONG"; return MessageType2; })(MessageType || {}); // src/useDebugTimeMachine.ts var DEFAULT_CONFIG = { websocketUrl: "ws://localhost:4000/ws", debugMode: true, // 🔥 기본값을 true로 변경 captureUserActions: true, captureErrors: true, captureStateChanges: true, maxReconnectAttempts: 5, reconnectInterval: 5e3, developmentOnly: false, autoConnect: true, enableTimeTravelEngine: true, enableMemoryManagement: true, enableDOMSnapshots: false, enableMetrics: true, timeTravelConfig: {}, memoryConfig: {}, domSnapshotConfig: {} }; var CONSOLE_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;" }; function useDebugTimeMachine(userConfig = {}) { const config = { ...DEFAULT_CONFIG, ...userConfig }; const wsRef = useRef(null); const isInitializedRef = useRef(false); const isConnectingRef = useRef(false); const reconnectAttemptsRef = useRef(0); const reconnectTimeoutRef = useRef(null); const [isConnected, setIsConnected] = useState(false); const [clientId, setClientId] = useState(null); const [connectionInfo, setConnectionInfo] = useState({ connectedAt: null, reconnectAttempts: 0, lastError: null }); const [performanceMetrics, setPerformanceMetrics] = useState(null); const [systemMetrics, setSystemMetrics] = useState(null); const [memoryUsage, setMemoryUsage] = useState(null); const [networkRequests, setNetworkRequests] = useState([]); const [networkResponses, setNetworkResponses] = useState([]); const writeLog = useCallback( (message, type = "info", data) => { if (!config.debugMode) return; const style = CONSOLE_STYLES[type]; const timestamp = (/* @__PURE__ */ new Date()).toISOString(); if (data) { console.groupCollapsed(`%c[Debug Time Machine ${timestamp}] ${message}`, style); console.log("Data:", data); console.groupEnd(); } else { console.log(`%c[Debug Time Machine ${timestamp}] ${message}`, style); } }, [config.debugMode] ); const sendMessage = useCallback( (type, data) => { if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { writeLog(`\u274C Cannot send message: WebSocket not ready`, "warning"); return false; } const message = { type, payload: data, timestamp: Date.now(), clientId: clientId || void 0 }; try { const messageStr = JSON.stringify(message); wsRef.current.send(messageStr); writeLog(`\u2705 Message sent successfully: ${type}`, "success"); return true; } catch (error) { writeLog(`\u274C Failed to send message: ${type}`, "error"); return false; } }, [clientId, writeLog] ); const captureError = useCallback( (error, errorInfo) => { writeLog(`\u{1F534} Manual error capture: ${error.message}`, "error"); const isWsConnected = wsRef.current?.readyState === WebSocket.OPEN; writeLog(`Debug: isWsConnected=${isWsConnected}, captureErrors=${config.captureErrors}`, "info"); if (!isWsConnected || !config.captureErrors) { writeLog("Cannot capture error: WebSocket not connected or errors disabled", "warning"); return; } const errorData = { message: error.message || "Unknown error", stack: error.stack || "No stack trace available", timestamp: Date.now(), url: window.location.href, userAgent: navigator.userAgent }; if (errorInfo) { errorData.errorInfo = errorInfo; } const success = sendMessage("ERROR" /* ERROR */, errorData); writeLog(`Error message send result: ${success}`, success ? "success" : "error"); }, [config.captureErrors, sendMessage, writeLog] ); const captureStateChange = useCallback( (componentName, prevState, newState, props) => { const isWsConnected = wsRef.current?.readyState === WebSocket.OPEN; if (!isWsConnected || !config.captureStateChanges) { writeLog("Cannot capture state change: WebSocket not connected or disabled", "warning"); return; } const stateChangeData = { componentName, prevState, newState, props, timestamp: Date.now(), url: window.location.href }; sendMessage("STATE_CHANGE" /* STATE_CHANGE */, stateChangeData); }, [config.captureStateChanges, sendMessage, writeLog] ); const handleMessage = useCallback( (event) => { try { const message = JSON.parse(event.data); if (!message || typeof message !== "object" || !message.type) { return; } const messageData = message.payload || message.data; switch (message.type) { case "CONNECTION" /* CONNECTION */: if (messageData && messageData.type === "welcome" && messageData.clientId) { setClientId(messageData.clientId); writeLog(`Assigned client ID: ${messageData.clientId}`, "success"); } break; case "ping": case "PING": case "PING" /* PING */: sendMessage("PONG" /* PONG */, {}); break; case "ERROR" /* ERROR */: writeLog("Server error:", "error", messageData); break; default: break; } } catch (error) { writeLog("Failed to parse incoming message", "error"); } }, [sendMessage, writeLog] ); const connect = useCallback(() => { if (isConnectingRef.current || wsRef.current?.readyState === WebSocket.OPEN) { return; } try { writeLog("Connecting to Time Machine...", "info"); isConnectingRef.current = true; if (wsRef.current) { wsRef.current.close(); wsRef.current = null; } wsRef.current = new WebSocket(config.websocketUrl); wsRef.current.onopen = () => { isConnectingRef.current = false; setIsConnected(true); reconnectAttemptsRef.current = 0; setConnectionInfo((prev) => ({ ...prev, connectedAt: /* @__PURE__ */ new Date(), reconnectAttempts: 0, lastError: null })); writeLog("Connected to Debug Time Machine!", "success"); const connectionData = { type: "client_ready", url: window.location.href, userAgent: navigator.userAgent, timestamp: Date.now() }; setTimeout(() => { if (wsRef.current?.readyState === WebSocket.OPEN) { try { wsRef.current.send(JSON.stringify({ type: "CONNECTION" /* CONNECTION */, payload: connectionData, timestamp: Date.now(), clientId: clientId || void 0 })); } catch (sendError) { writeLog("Failed to send initial message", "error"); } } }, 300); }; wsRef.current.onmessage = handleMessage; wsRef.current.onclose = (event) => { isConnectingRef.current = false; setIsConnected(false); setClientId(null); if (event.code === 1e3 || event.code === 1001) { return; } if (reconnectAttemptsRef.current < config.maxReconnectAttempts) { reconnectAttemptsRef.current++; setConnectionInfo((prev) => ({ ...prev, reconnectAttempts: reconnectAttemptsRef.current, lastError: null })); reconnectTimeoutRef.current = window.setTimeout(() => { connect(); }, config.reconnectInterval); } }; wsRef.current.onerror = () => { isConnectingRef.current = false; setConnectionInfo((prev) => ({ ...prev, lastError: "WebSocket connection error" })); }; } catch (error) { writeLog("Failed to create WebSocket connection", "error"); isConnectingRef.current = false; } }, [config.websocketUrl, config.maxReconnectAttempts, config.reconnectInterval, handleMessage, writeLog, clientId]); const reconnect = useCallback(() => { if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } if (wsRef.current) { wsRef.current.close(); } reconnectAttemptsRef.current = 0; connect(); }, [connect]); const checkDebugServer = useCallback(async () => { if (!config.autoConnect) { return false; } try { const baseUrl = config.websocketUrl.replace("ws://", "http://").replace("/ws", ""); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 2e3); try { const response = await fetch(`${baseUrl}/health`, { method: "GET", signal: controller.signal }); clearTimeout(timeoutId); if (response.ok) { const data = await response.json(); writeLog(`\u{1F3AF} Debug \uC11C\uBC84 \uBC1C\uACAC!`, "success"); return true; } } catch (fetchError) { clearTimeout(timeoutId); throw fetchError; } } catch (error) { if (config.debugMode) { writeLog(`Debug \uC11C\uBC84 \uC5C6\uC74C`, "info"); } } return false; }, [config.websocketUrl, config.autoConnect, config.debugMode, writeLog]); const setupEnhancedNetworkInterceptor = useCallback(() => { if (typeof window === "undefined") return; writeLog("\u{1F310} Setting up enhanced network interceptor...", "info"); const originalFetch = window.fetch; const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; const generateRequestId = () => `req_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; const shouldCaptureUrl = (url) => { const excludePatterns = [ /localhost:4000/, /localhost:8080/, /127\.0\.0\.1:4000/, /127\.0\.0\.1:8080/, /webpack-hmr/, /hot-update/, /sockjs-node/, /@vite\/client/, /react-devtools/, /chrome-extension:/, /moz-extension:/, /favicon\.ico/, /manifest\.json/, /\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?|$)/ ]; if (excludePatterns.some((pattern) => pattern.test(url))) { return false; } const includePatterns = [ /\/api\//, /\/graphql/, /\/v\d+\//, /jsonplaceholder\.typicode\.com/, /reqres\.in/, /httpbin\.org/, /api\./ ]; return includePatterns.some((pattern) => pattern.test(url)); }; const sanitizeRequestData = (data) => { if (!data) return data; if (typeof data === "string") { try { const parsed = JSON.parse(data); return sanitizeRequestData(parsed); } catch { return data.replace(/"password"\s*:\s*"[^"]*"/gi, '"password":"***"').replace(/"token"\s*:\s*"[^"]*"/gi, '"token":"***"').replace(/"secret"\s*:\s*"[^"]*"/gi, '"secret":"***"'); } } if (typeof data === "object" && data !== null) { const sanitized = { ...data }; Object.keys(sanitized).forEach((key) => { if (/password|token|secret|auth|key/i.test(key)) { sanitized[key] = "***"; } }); return sanitized; } return data; }; window.fetch = async (...args) => { const [input, init = {}] = args; const url = typeof input === "string" ? input : input.url; const method = init.method || "GET"; const requestId = generateRequestId(); const startTime = Date.now(); const shouldCapture = shouldCaptureUrl(url) || ["POST", "PUT", "DELETE", "PATCH"].includes(method.toUpperCase()); if (!shouldCapture) { return originalFetch.apply(window, args); } writeLog(`\u{1F4E4} API Request: ${method} ${url}`, "info"); const requestData = { id: requestId, type: "request", method: method.toUpperCase(), url, timestamp: startTime, headers: init.headers ? Object.fromEntries(new Headers(init.headers).entries()) : {}, body: init.body ? sanitizeRequestData(init.body) : void 0 }; setNetworkRequests((prev) => [...prev, { ...requestData, status: "pending", duration: 0 }].slice(-100)); try { const response = await originalFetch.apply(window, args); const endTime = Date.now(); const duration = endTime - startTime; const responseHeaders = Object.fromEntries(response.headers.entries()); let responseBody = void 0; try { const clonedResponse = response.clone(); const contentType = response.headers.get("content-type") || ""; if (contentType.includes("application/json")) { responseBody = await clonedResponse.json(); } else if (contentType.includes("text/")) { const text = await clonedResponse.text(); responseBody = text.length > 1e3 ? text.substring(0, 1e3) + "..." : text; } } catch (bodyError) { } const responseData = { id: requestId, type: "response", method: method.toUpperCase(), url, status: response.status, statusText: response.statusText, ok: response.ok, duration, timestamp: endTime, requestTimestamp: startTime, headers: responseHeaders, body: responseBody, size: responseHeaders["content-length"] || "unknown" }; setNetworkRequests((prev) => prev.map( (req) => req.id === requestId ? { ...req, ...responseData, status: response.status, success: response.ok } : req )); if (wsRef.current?.readyState === WebSocket.OPEN) { writeLog(`\u{1F525} Sending NETWORK_REQUEST to server`, "info"); const requestSuccess = sendMessage("NETWORK_REQUEST", requestData); const responseSuccess = sendMessage("NETWORK_RESPONSE", responseData); writeLog(`Network message results: request=${requestSuccess}, response=${responseSuccess}`, "info"); } else { writeLog(`\u274C WebSocket not ready for network data: readyState=${wsRef.current?.readyState}`, "warning"); } if (response.ok) { writeLog(`\u2705 API Success: ${method} ${url} (${response.status}, ${duration}ms)`, "success"); } else { writeLog(`\u274C API Error: ${method} ${url} (${response.status}, ${duration}ms)`, "error"); captureError(new Error(`HTTP ${response.status}: ${response.statusText}`), { type: "network_error", url, method, status: response.status, statusText: response.statusText, duration, requestId, responseBody }); } return response; } catch (error) { const endTime = Date.now(); const duration = endTime - startTime; const errorData = { id: requestId, type: "error", method: method.toUpperCase(), url, duration, timestamp: endTime, requestTimestamp: startTime, error: error instanceof Error ? error.message : String(error), errorType: error instanceof Error ? error.name : "Unknown" }; setNetworkRequests((prev) => prev.map( (req) => req.id === requestId ? { ...req, ...errorData, status: "error", success: false } : req )); if (wsRef.current?.readyState === WebSocket.OPEN) { writeLog(`\u{1F525} Sending NETWORK_ERROR to server`, "error"); const requestSuccess = sendMessage("NETWORK_REQUEST", requestData); const errorSuccess = sendMessage("NETWORK_ERROR", errorData); writeLog(`Network error message results: request=${requestSuccess}, error=${errorSuccess}`, "info"); } else { writeLog(`\u274C WebSocket not ready for error data: readyState=${wsRef.current?.readyState}`, "warning"); } captureError(error instanceof Error ? error : new Error(String(error)), { type: "network_error", url, method, duration, requestId }); throw error; } }; XMLHttpRequest.prototype.open = function(method, url, async, username, password) { const urlString = url.toString(); const shouldCapture = shouldCaptureUrl(urlString) || ["POST", "PUT", "DELETE", "PATCH"].includes(method.toUpperCase()); if (shouldCapture) { const requestId = generateRequestId(); this._debugTM = { requestId, method: method.toUpperCase(), url: urlString, startTime: Date.now(), shouldCapture: true }; writeLog(`\u{1F4E4} XHR Request: ${method} ${urlString}`, "info"); } return originalXHROpen.call(this, method, url, async || true, username, password); }; XMLHttpRequest.prototype.send = function(body) { if (this._debugTM && this._debugTM.shouldCapture) { const { requestId, method, url, startTime } = this._debugTM; const requestData = { id: requestId, type: "request", method, url, timestamp: startTime, headers: {}, body: body ? sanitizeRequestData(body) : void 0 }; setNetworkRequests((prev) => [...prev, { ...requestData, status: "pending", duration: 0 }].slice(-100)); this.addEventListener("loadend", () => { const endTime = Date.now(); const duration = endTime - startTime; let responseBody = void 0; try { if (this.responseText && this.getResponseHeader("content-type")?.includes("application/json")) { responseBody = JSON.parse(this.responseText); } else if (this.responseText) { const text = this.responseText; responseBody = text.length > 1e3 ? text.substring(0, 1e3) + "..." : text; } } catch { } const responseData = { id: requestId, type: "response", method, url, status: this.status, statusText: this.statusText, ok: this.status >= 200 && this.status < 300, duration, timestamp: endTime, requestTimestamp: startTime, headers: {}, body: responseBody }; setNetworkRequests((prev) => prev.map( (req) => req.id === requestId ? { ...req, ...responseData, status: this.status, success: this.status >= 200 && this.status < 300 } : req )); if (wsRef.current?.readyState === WebSocket.OPEN) { writeLog(`\u{1F525} Sending XHR data to server`, "info"); const requestSuccess = sendMessage("NETWORK_REQUEST", requestData); if (this.status === 0) { const errorSuccess = sendMessage("NETWORK_ERROR", { id: requestId, type: "error", method, url, duration, timestamp: endTime, requestTimestamp: startTime, error: "Network Error", errorType: "NetworkError" }); writeLog(`XHR error message results: request=${requestSuccess}, error=${errorSuccess}`, "info"); } else { const responseSuccess = sendMessage("NETWORK_RESPONSE", responseData); writeLog(`XHR message results: request=${requestSuccess}, response=${responseSuccess}`, "info"); } } else { writeLog(`\u274C WebSocket not ready for XHR data: readyState=${wsRef.current?.readyState}`, "warning"); } if (this.status >= 200 && this.status < 300) { writeLog(`\u2705 XHR Success: ${method} ${url} (${this.status}, ${duration}ms)`, "success"); } else { writeLog(`\u274C XHR Error: ${method} ${url} (${this.status}, ${duration}ms)`, "error"); if (this.status >= 400) { captureError(new Error(`XHR ${this.status}: ${this.statusText}`), { type: "network_error", url, method, status: this.status, statusText: this.statusText, duration, requestId, responseBody }); } } }); this.addEventListener("error", () => { const endTime = Date.now(); const duration = endTime - startTime; const errorData = { id: requestId, type: "error", method, url, duration, timestamp: endTime, requestTimestamp: startTime, error: "Network Error", errorType: "NetworkError" }; setNetworkRequests((prev) => prev.map( (req) => req.id === requestId ? { ...req, ...errorData, status: "error", success: false } : req )); if (wsRef.current?.readyState === WebSocket.OPEN) { writeLog(`\u{1F525} Sending XHR error to server`, "error"); const requestSuccess = sendMessage("NETWORK_REQUEST", requestData); const errorSuccess = sendMessage("NETWORK_ERROR", errorData); writeLog(`XHR error message results: request=${requestSuccess}, error=${errorSuccess}`, "info"); } else { writeLog(`\u274C WebSocket not ready for XHR error: readyState=${wsRef.current?.readyState}`, "warning"); } captureError(new Error("XHR Network Error"), { type: "network_error", url, method, duration, requestId }); }); } return originalXHRSend.call(this, body); }; writeLog("\u2705 Enhanced network interceptor setup complete", "success"); return () => { window.fetch = originalFetch; XMLHttpRequest.prototype.open = originalXHROpen; XMLHttpRequest.prototype.send = originalXHRSend; writeLog("\u{1F9F9} Enhanced network interceptor cleaned up", "info"); }; }, [writeLog, sendMessage, captureError, isConnected]); const autoConnectToServer = useCallback(async () => { if (!config.autoConnect) { return null; } writeLog("\u{1F50D} Debug Time Machine \uC11C\uBC84 \uAC80\uC0C9 \uC911...", "info"); const serverExists = await checkDebugServer(); if (serverExists) { writeLog("\u2705 Debug Time Machine \uC11C\uBC84 \uBC1C\uACAC! \uC5F0\uACB0\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4.", "success"); connect(); return null; } else { writeLog("\u{1F937} Debug Time Machine \uC11C\uBC84\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC77C\uBC18 \uBAA8\uB4DC\uB85C \uC2E4\uD589\uB429\uB2C8\uB2E4.", "info"); return null; } }, [config.autoConnect, checkDebugServer, connect, writeLog]); useEffect(() => { if (isInitializedRef.current || wsRef.current) { return; } isInitializedRef.current = true; writeLog("\u{1F680} Initializing Debug Time Machine client...", "info"); const cleanupNetworkInterceptor = setupEnhancedNetworkInterceptor(); autoConnectToServer().catch((error) => { writeLog("\uC790\uB3D9 \uC5F0\uACB0 \uC911 \uC624\uB958 \uBC1C\uC0DD", "error", error); }); writeLog("\u2705 Debug Time Machine client initialized!", "success"); return () => { writeLog("\u{1F9F9} Cleaning up Debug Time Machine client...", "info"); isInitializedRef.current = false; if (cleanupNetworkInterceptor) { cleanupNetworkInterceptor(); } if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } if (wsRef.current) { wsRef.current.close(1e3, "Component unmounting"); wsRef.current = null; } }; }, []); useEffect(() => { return () => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.close(); } }; }, []); const createSnapshot = useCallback((state, actionType) => { writeLog(`\u{1F4F8} Snapshot created: ${actionType}`, "success"); return { id: Date.now().toString(), state, actionType, timestamp: Date.now() }; }, [writeLog]); const restoreSnapshot = useCallback((snapshotId) => { writeLog(`\u23EE\uFE0F Snapshot restored: ${snapshotId}`, "success"); return true; }, [writeLog]); const getPerformanceMetrics = useCallback(() => { return performanceMetrics; }, [performanceMetrics]); const getSystemMetrics = useCallback(() => { return systemMetrics; }, [systemMetrics]); const getMemoryUsage = useCallback(() => { return memoryUsage; }, [memoryUsage]); const clearSnapshots = useCallback(() => { writeLog("\u{1F5D1}\uFE0F All snapshots cleared", "info"); }, [writeLog]); const exportMetrics = useCallback(() => { return { exported: true, timestamp: Date.now() }; }, []); const getNetworkMetrics = useCallback(() => { return { totalRequests: networkRequests.length, successfulRequests: networkRequests.filter((req) => req.success === true).length, failedRequests: networkRequests.filter((req) => req.success === false).length, requests: networkRequests }; }, [networkRequests]); const clearNetworkHistory = useCallback(() => { setNetworkRequests([]); setNetworkResponses([]); writeLog("\u{1F5D1}\uFE0F Network history cleared", "info"); }, [writeLog]); const exportNetworkData = useCallback(() => { return { requests: networkRequests, responses: networkResponses, metrics: getNetworkMetrics(), exportedAt: (/* @__PURE__ */ new Date()).toISOString() }; }, [networkRequests, networkResponses, getNetworkMetrics]); return { isConnected, clientId, reconnect, sendMessage, captureError, captureStateChange, connectionInfo, shouldCaptureNetworkRequest: () => true, shouldCaptureNetworkResponse: () => true, createSnapshot, restoreSnapshot, getPerformanceMetrics, getSystemMetrics, getMemoryUsage, clearSnapshots, exportMetrics, getNetworkMetrics, clearNetworkHistory, exportNetworkData, timeTravelEngine: null, metricsCollector: null, domSnapshotOptimizer: null, networkInterceptor: null, performanceMetrics, systemMetrics, memoryUsage, networkRequests, networkResponses }; } export { MessageType, useDebugTimeMachine }; //# sourceMappingURL=index.mjs.map